C++ 继承与多态:从基础语法到虚函数机制全解析
1. 继承定义继承是面向对象程序设计中最核心的特性之一它允许一个类派生类基于另一个类基类来定义从而自动获得基类的成员变量和成员函数。继承体现了类之间的层次关系是代码复用的重要手段。通过继承派生类可以复用基类中已有的功能同时可以增加新的成员或修改基类成员的行为从而构建出更复杂、更具体的类型体系。2. 原类型与新类型在继承关系中被继承的类称为基类Base Class也叫做父类或原有类型通过继承产生的新类称为派生类Derived Class也叫做子类或新类型。派生类是一种特殊的基类它既拥有基类的全部特性又具备自身新增的特性。这种“是一种”is-a的关系是继承的核心语义。3. 语法格式C 中定义派生类的基本语法格式如下class 派生类名 : 继承方式 基类名 { // 派生类新增的成员 };其中继承方式可以是 public、protected 或 private默认情况下为 private 继承。例如class Animal { public: void eat() { cout 动物在吃东西 endl; } }; class Dog : public Animal { public: void bark() { cout 狗在叫 endl; } };4. 继承过程继承的过程可以理解为编译器在创建派生类对象时会先调用基类的构造函数来初始化基类部分然后再执行派生类自身的构造函数来初始化派生类新增的成员。派生类对象在内存中的布局是基类子对象位于前部派生类新增成员位于其后。从代码复用的角度看继承的过程就是把基类的成员“吸收”进派生类并根据继承方式调整这些成员的访问权限最终形成一个完整的派生类类型。5. 继承方式C 支持三种继承方式public公有继承、protected保护继承和 private私有继承。不同的继承方式决定了基类成员在派生类中的访问权限。5.1 公有继承公有继承是最常用的继承方式它保持了基类成员的原有访问权限基类的 public 成员在派生类中仍为 publicprotected 成员仍为 protectedprivate 成员不可直接访问。公有继承体现了is-a关系。5.2 保护继承保护继承将基类的 public 成员和 protected 成员都变为派生类中的 protected 成员private 成员仍不可直接访问。保护继承常用于不希望外部直接使用基类接口但允许进一步派生的情况。5.3 私有继承私有继承将基类的 public 成员和 protected 成员都变为派生类中的 private 成员private 成员仍不可直接访问。私有继承体现的是has-a关系即组合关系外部无法通过派生类访问基类接口。不同继承方式下基类和派生类关系继承方式基类 public 成员基类 protected 成员基类 private 成员关系语义public 继承publicprotected不可访问is-aprotected 继承protectedprotected不可访问is-a受限private 继承privateprivate不可访问has-a6. 构造函数构造函数不能被继承但派生类构造函数可以显式调用基类构造函数来初始化基类部分。如果派生类构造函数没有显式调用基类构造函数编译器会默认调用基类的无参构造函数。class Animal { public: Animal(string name) : m_name(name) {} protected: string m_name; }; class Dog : public Animal { public: Dog(string name, int age) : Animal(name), m_age(age) {} private: int m_age; };构造函数的调用顺序是先基类后派生类析构函数的调用顺序则相反先派生类后基类。7. 函数隐藏当派生类中定义了与基类同名无论参数列表是否相同的成员函数时基类的同名函数在派生类中会被隐藏。这意味着通过派生类对象调用该名称的函数时只会调用派生类版本基类版本需要通过作用域限定符来访问。class Base { public: void func() { cout Base::func() endl; } void func(int x) { cout Base::func(int) endl; } }; class Derived : public Base { public: void func() { cout Derived::func() endl; } }; int main() { Derived d; d.func(); // 调用 Derived::func() // d.func(10); // 错误基类 func(int) 被隐藏 d.Base::func(10); // 通过作用域限定符访问基类版本 return 0; }函数隐藏与函数重写不同隐藏发生在非虚函数或参数列表不同的情况下而重写要求函数是虚函数且参数列表完全相同。8. 构造/析构函数继承构造函数和析构函数都不能被继承原因如下构造函数名称必须与类名相同派生类名称与基类不同因此基类构造函数无法作为派生类构造函数被继承。构造函数负责初始化本类特有成员基类构造函数无法初始化派生类新增的成员因此必须由派生类自己定义构造函数。析构函数名称也必须与类名相同同样因为类名不同基类析构函数无法被继承。析构顺序要求析构函数需要按照先派生后基类的顺序执行这要求每个类都有自己的析构函数。9. 赋值兼容原则赋值兼容原则赋值兼容规则是指在公有继承下派生类对象可以赋值给基类对象、基类引用可以绑定派生类对象、基类指针可以指向派生类对象。这是因为派生类对象“是一种”基类对象。class Animal {}; class Dog : public Animal {}; Dog dog; Animal a dog; // 派生类对象赋值给基类对象对象切片 Animal ref dog; // 基类引用绑定派生类对象 Animal* ptr dog; // 基类指针指向派生类对象注意对象赋值会发生“对象切片”即派生类新增的成员被丢弃只保留基类部分。而引用和指针不会发生切片它们仍然指向完整的派生类对象。10. 虚函数虚函数是在基类中使用 virtual 关键字声明的成员函数它允许在派生类中被重写从而实现运行时多态。虚函数的定义格式如下class Base { public: virtual void show() // 虚函数 { cout Base::show() endl; } };虚函数必须是非静态成员函数构造函数不能是虚函数但析构函数可以是虚函数。10.1 实现原理与虚函数表虚函数的实现依赖于虚函数表vtable和虚指针vptr。每个包含虚函数的类都会生成一张虚函数表表中存放该类所有虚函数的地址。每个对象内部会有一个隐藏的虚指针指向所属类的虚函数表。当通过基类指针或引用调用虚函数时编译器会通过虚指针找到虚函数表再从表中取出对应的函数地址进行调用。这个过程在运行时完成因此称为动态绑定。class Base { public: virtual void func1() {} virtual void func2() {} }; class Derived : public Base { public: virtual void func1() override {} // 重写 func1 };在上述例子中Base 的虚函数表包含 func1 和 func2 的地址Derived 的虚函数表包含重写后的 func1 地址和继承的 func2 地址。10.2. 函数原型虚函数的函数原型函数签名包括返回类型、函数名和参数列表。要实现函数重写派生类中的虚函数必须与基类虚函数具有完全相同的函数原型返回类型、函数名、参数列表都一致。class Base { public: virtual void print(int x); // 虚函数原型 }; class Derived : public Base { public: virtual void print(int x); // 与基类原型一致构成重写 };C11 引入了 override 关键字用于显式标记重写函数编译器会检查函数原型是否匹配。10.3. 虚函数作用虚函数的主要作用是实现运行时多态即通过基类指针或引用调用虚函数时实际执行的是派生类中重写的版本。这使得程序可以在运行时根据对象的实际类型选择合适的行为增强了代码的灵活性和可扩展性。虚函数也是实现“开闭原则”的重要手段新增功能时只需增加新的派生类并重写虚函数无需修改已有代码。10.4 函数重写函数重写覆盖是指派生类中定义与基类虚函数具有相同函数原型的函数从而替换基类虚函数的实现。重写要求基类函数必须是虚函数。函数原型返回类型、函数名、参数列表必须完全相同。访问权限可以不同但通常保持相同。class Shape { public: virtual double area() { return 0; } }; class Circle : public Shape { public: virtual double area() override { return 3.14 * r * r; } private: double r; };10.5. 纯虚函数与抽象类纯虚函数是在虚函数声明末尾加上 0的虚函数它没有函数体。包含纯虚函数的类称为抽象类抽象类不能实例化对象只能作为基类被继承。class Shape { public: virtual double area() 0; // 纯虚函数 virtual ~Shape() {} }; class Circle : public Shape { public: virtual double area() override { return 3.14 * r * r; } };派生类必须实现基类的所有纯虚函数否则派生类也是抽象类不能实例化。抽象类用于定义接口规范强制派生类实现特定行为。10.6. 虚析构函数当通过基类指针删除派生类对象时如果基类析构函数不是虚函数则只会调用基类析构函数派生类析构函数不会被调用导致派生类资源无法释放造成内存泄漏。将基类析构函数声明为虚函数可以解决这个问题。class Base { public: virtual ~Base() { cout Base 析构 endl; } }; class Derived : public Base { public: ~Derived() { cout Derived 析构 endl; } }; int main() { Base* p new Derived(); delete p; // 先调用 Derived 析构再调用 Base 析构 return 0; }11. 多继承多继承是指一个派生类同时继承多个基类。C 支持多继承派生类会包含所有基类的子对象。class A { public: void funcA() { cout A::funcA() endl; } }; class B { public: void funcB() { cout B::funcB() endl; } }; class C : public A, public B { public: void funcC() { cout C::funcC() endl; } }; int main() { C c; c.funcA(); // 调用 A 的成员函数 c.funcB(); // 调用 B 的成员函数 c.funcC(); // 调用 C 自己的成员函数 return 0; }多继承可能带来二义性问题如果多个基类中有同名成员派生类访问时需要指定基类作用域。12. 虚继承虚继承用于解决多继承中的菱形继承问题。当派生类通过多条路径继承同一个基类时该基类会在派生类中产生多份拷贝造成数据冗余和二义性。虚继承让共同基类只保留一份拷贝。class A { public: int value; }; class B : virtual public A {}; class C : virtual public A {}; class D : public B, public C {}; // D 中只有一份 A 的拷贝虚继承通过虚基类表vbptr实现编译器会调整成员访问路径确保共同基类只有一份实例。13. 多态多态是指同一操作作用于不同对象时产生不同的行为。C 中的多态分为编译时多态和运行时多态。13.1 编译时多态编译时多态是指在程序编译阶段就能确定调用哪个函数的多态形式也称为静态多态或早期绑定。它通过函数重载、运算符重载和模板实现编译器根据函数名、参数类型和参数数量在编译期完成函数匹配运行时不产生额外查找开销因此执行效率较高。下面通过函数重载演示编译时多态#include iostream using namespace std; class Calculator { public: int add(int a, int b) { return a b; } double add(double a, double b) { return a b; } int add(int a, int b, int c) { return a b c; } }; int main() { Calculator calc; cout calc.add(1, 2) endl; // 调用 int add(int, int) cout calc.add(1.5, 2.5) endl; // 调用 double add(double, double) cout calc.add(1, 2, 3) endl; // 调用 int add(int, int, int) return 0; }编译时多态通过函数重载和运算符重载实现编译器在编译阶段根据参数类型和数量确定调用哪个函数。13.2 运行时多态运行时多态是指在程序运行阶段才能确定调用哪个函数的多态形式也称为动态多态或后期绑定。它通过虚函数和继承实现程序在运行时根据对象的实际类型借助虚函数表和虚指针动态确定调用哪个函数版本从而让同一操作在不同对象上表现出不同行为。下面通过虚函数演示运行时多态#include iostream using namespace std; class Shape { public: virtual void draw() { cout 绘制图形 endl; } }; class Circle : public Shape { public: virtual void draw() override { cout 绘制圆形 endl; } }; class Rectangle : public Shape { public: virtual void draw() override { cout 绘制矩形 endl; } }; void render(Shape s) { s.draw(); // 运行时动态绑定 } int main() { Circle c; Rectangle r; render(c); // 输出绘制圆形 render(r); // 输出绘制矩形 return 0; }运行时多态通过虚函数和继承实现程序在运行阶段根据对象的实际类型动态确定调用哪个函数版本。为什么析构函数建议设置为虚函数析构函数建议设置为虚函数的原因当通过基类指针删除派生类对象时如果基类析构函数不是虚函数则只会调用基类析构函数派生类中动态分配的资源无法释放造成内存泄漏。将基类析构函数声明为虚函数后delete 操作会先调用派生类析构函数再调用基类析构函数确保资源完整释放。只要类可能被继承且可能通过基类指针删除派生类对象就应该将析构函数声明为虚函数。构造函数为什么不能设置为虚函数构造函数不能设置为虚函数原因如下虚函数表尚未建立对象在构造过程中虚指针和虚函数表还未初始化完成无法通过虚函数表进行动态绑定。构造顺序要求构造函数必须按照先基类后派生类的顺序执行而虚函数机制要求先确定对象的实际类型这在构造阶段无法完成。语义矛盾虚函数用于实现运行时多态而构造函数的作用是创建对象并初始化此时对象还不存在多态没有意义。14. 总结本文系统梳理了 C 继承与多态的完整知识体系核心要点如下继承的本质继承允许派生类基于基类复用代码体现is-a关系通过 public、protected、private 三种继承方式控制基类成员在派生类中的访问权限。构造与析构构造函数和析构函数不能被继承构造顺序为先基类后派生类析构顺序相反派生类构造函数需显式调用基类构造函数完成基类部分初始化。函数隐藏与重写同名非虚函数构成隐藏需作用域限定符访问基类版本虚函数且原型完全一致才构成重写实现运行时多态。虚函数机制虚函数通过虚函数表vtable和虚指针vptr实现动态绑定纯虚函数定义抽象类接口强制派生类实现基类析构函数建议声明为虚函数避免通过基类指针删除派生类对象时造成资源泄漏。多继承与虚继承多继承可能带来二义性需指定基类作用域虚继承通过虚基类表解决菱形继承问题确保共同基类只有一份拷贝。多态的实现编译时多态通过函数重载、运算符重载和模板在编译期确定调用运行时多态通过虚函数和继承在运行期动态绑定是面向对象设计的核心能力。掌握继承与多态是深入理解 C 面向对象编程的关键。建议读者结合代码示例动手实践重点体会虚函数表机制、构造析构顺序以及多态在实际项目中的应用场景。

相关新闻

逻辑回归下采样三大陷阱:概率失真、标准误膨胀与边界漂移

逻辑回归下采样三大陷阱:概率失真、标准误膨胀与边界漂移

1. 为什么逻辑回归遇上“极偏数据”会直接失效——从一个真实故障说起我去年在某高校实验室参与一个医疗风险预测项目时,遇到过一个至今想起来还觉得后怕的案例:模型在训练集上AUC高达0.92,测试集也有0.89,但部署到临床辅助系统后…

2026/10/9 10:16:36 阅读更多 →
砌墙备砖别拍脑袋,530 块一方才准

砌墙备砖别拍脑袋,530 块一方才准

砌墙算砖,一方墙多少块标准砖、多少砂浆,算少了停工待料,算多了压钱。从砖规格、灰缝、墙厚、组砌方式到砂浆标号,每一步都影响备料量。本文按"从砖到墙"的顺序,把建工计算器方砖用量怎么用讲清楚。一、标准…

2026/10/9 10:15:35 阅读更多 →
AWS SDK for JavaScript v3 凭证提供者完全指南:@aws-sdk/credential-providers 从默认链到 SSO、OIDC 与 Cognito

AWS SDK for JavaScript v3 凭证提供者完全指南:@aws-sdk/credential-providers 从默认链到 SSO、OIDC 与 Cognito

【免费下载链接】context-hub 项目地址: https://gitcode.com/gh_mirrors/co/context-hub 点击查看 免费下载 本篇围绕 AWS SDK for JavaScript v3 的 aws-sdk/credential-providers 包展开,讲清 Node.js 客户端的默认凭证解析行为,以及共享…

2026/10/9 10:15:35 阅读更多 →

最新新闻

基于JWT/JWE的跨系统安全数据透传方案详解

基于JWT/JWE的跨系统安全数据透传方案详解

先说结论:这套“基于JWT/JWE的跨系统安全数据透传方案”,解决的是两个不同域、不同技术栈、甚至不同运维体系的服务之间,如何安全地把一段结构化数据从A端交付到B端——既保证数据在“路上”不被看、不被改,又保证接收方能够验证数…

2026/10/9 10:57:43 阅读更多 →
MySQL慢查询优化实战:索引设计与执行计划调优全攻略

MySQL慢查询优化实战:索引设计与执行计划调优全攻略

接手线上MySQL慢查询优化这类活儿,看着是加几个索引的事,实际上是一整套“读表逻辑”的博弈。索引优化策略不只是“在WHERE条件字段上建索引”这么简单,它背后涉及索引结构、查询执行计划、数据分布、写入成本之间的复杂权衡。这篇就把我在实…

2026/10/9 10:57:43 阅读更多 →
撕开高性能芯片与电竞级体验的包装:实测数据还原真实性能

撕开高性能芯片与电竞级体验的包装:实测数据还原真实性能

上周帮朋友挑新机,宣传页上“高性能芯片”四个字印得比Logo还大,背面还配了一行小字“电竞级稳帧体验”。跑分一拉出来,确实漂亮,安兔兔九十几万,看着就热血。结果朋友拿回家打了两局游戏,第三把还没打完&a…

2026/10/9 10:57:43 阅读更多 →
光伏电站运维模式怎么选?自建、委托、混合与智能运维全解析

光伏电站运维模式怎么选?自建、委托、混合与智能运维全解析

光伏圈里有一句话我特别认同:"电站并网只是起点,运维才是长跑。"做光伏这么多年,见过太多项目,前期的设计、采购、施工都舍得花钱,一到运维环节就开始精打细算,结果呢?组件热斑、逆变…

2026/10/9 10:57:43 阅读更多 →
MySQL提交数归零与123个CVE背后:真相与运维应对

MySQL提交数归零与123个CVE背后:真相与运维应对

最近在数据库圈子里,一个话题被反复讨论,甚至有人说 MySQL 正在“自杀”:代码提交数为 0,123 个 CVE 安全漏洞悬而未决。乍一听确实吓人,但作为常年折腾数据库的人,我第一反应是:得把这些数字拆…

2026/10/9 10:57:43 阅读更多 →
数据产品思维:打破数据所有权困局,让数据真正资产化

数据产品思维:打破数据所有权困局,让数据真正资产化

1. 数据团队和业务团队,为什么总在“谁说了算”上打架先从我最近遇到的一个真实场景说起。某家做零售的公司,数据部门辛苦搭了一套用户画像体系,整合了线上线下十几个系统的会员数据,清洗、打标、建模,前前后后折腾了大…

2026/10/9 10:56:40 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →