目录
一、 继承与多态概述
继承语法:class Derived : [public|protected|private] Base { … };
虚函数:在基类用 virtual
声明;派生类可 override
(推荐)
多态条件:public
继承 + 基类指针/引用 + 虚函数
析构函数必须是 virtual
,否则 delete 基类指针
会内存泄漏
纯虚函数:virtual void foo() = 0;
→ 基类变抽象类,不可实例化
多态底层:vptr + vtable;一个类带虚函数即有 vtable,对象首地址前 8 字节(64 位)存 vptr
继承解决代码复用与接口统一,多态解决运行期行为差异化。
牢记五个关键字:virtual、override、final、=0、using。
深刻理解对象模型(内存布局、vptr、vtable)才能写出高效、可维护的 C++ 面向对象代码。
二、C++继承的三大方式
在 C++ 中,继承方式(public、protected、private) 决定了基类成员在派生类中的访问权限,以及派生类对象对基类成员的访问能力。
(一)public继承(公有继承)
基类成员在派生类中的权限不变:
public
→ public
protected
→ protected
private
→ 不可访问(基类私有成员始终不可直接访问)。
派生类对象可以访问基类的 public
成员(通过 .
或 ->
)。
is-a 关系:派生类对象可以被视为基类对象(多态的基础)。
示例:
class Base {
public: int pub;
protected: int prot;
private: int priv;
};
class Derived : public Base {
void foo() {
pub = 1; // OK(public 继承后仍是 public)
prot = 2; // OK(public 继承后仍是 protected)
// priv = 3; // 错误(基类 private 成员不可访问)
}
};
int main() {
Derived d;
d.pub = 10; // OK(public 继承后仍是 public)
// d.prot = 20; // 错误(protected 成员对外不可见)
}
(二) protected继承(保护继承)
基类 public
和 protected
成员在派生类中变为 protected
:
public
→ protected
protected
→ protected
private
→ 不可访问。
派生类对象无法直接访问基类的任何成员(即使是基类的 public
成员,因继承后变为 protected
)。
has-a 关系:派生类内部可用基类功能,但对外隐藏。
示例:
class Derived : protected Base {
void foo() {
pub = 1; // OK(继承后变为 protected,派生类内部可访问)
prot = 2; // OK(继承后仍是 protected)
}
};
int main() {
Derived d;
// d.pub = 10; // 错误(protected 继承后 pub 变为 protected,对外不可见)
}
(三)private继承(私有继承)
基类 public
和 protected
成员在派生类中变为 private
:
public
→ private
protected
→ private
private
→ 不可访问。
派生类对象无法直接访问基类的任何成员。
is-implemented-in-terms-of 关系:派生类仅内部复用基类实现,对外完全隐藏。
示例:
class Derived : private Base {
void foo() {
pub = 1; // OK(继承后变为 private,派生类内部可访问)
prot = 2; // OK(继承后变为 private)
}
};
int main() {
Derived d;
// d.pub = 10; // 错误(private 继承后 pub 变为 private,对外不可见)
}
(四)继承小结
继承方式 | 基类 public 成员 | 基类 protected 成员 | 基类 private 成员 | 派生类对象能否访问基类 public 成员? |
---|---|---|---|---|
public | 仍为 public | 仍为 protected | 不可访问 | ✔️ 可以(通过 . 或 -> ) |
protected | 变为 protected | 仍为 protected | 不可访问 | ❌ 不可以(继承后变为 protected ) |
private | 变为 private | 变为 private | 不可访问 | ❌ 不可以(继承后变为 private ) |
三、派生类对象的构造与析构顺序
(一)构造顺序(构造自上而下)
按继承层次自顶向下
从最远的基类开始,一层层向下,直到最终派生类。
同一层中按声明顺序
如果某一层有多个基类(多继承)或成员对象,按它们在类定义中出现的顺序构造。
虚基类最先
若有虚继承,虚基类子对象在“真正”的基类之前构造,且只构造一次(由最派生类负责)。
调用点:每个类的构造函数在进入自己的函数体之前,先调用基类构造函数(或虚基类)成员对象的构造函数然后才执行自己的函数体。
示例:
class A { ... };
class B : virtual public A { ... };
class C : public B {
Member m1, m2;
public:
C() : m2(), m1() { /* 函数体 */ }
};
构造顺序:A(虚基类) → B(基类) → m1 → m2 → C::C() 的函数体
(二)析构顺序(析构自下而上)
严格是 构造的逆序:
先执行派生类自己的析构函数体;
按 与声明相反的顺序 析构成员对象;
按 继承层次的反向 析构基类(多继承时按声明的逆序);
若有虚基类,它在最后被析构(因为最先被构造)。
(三) 虚析构函数的必要性
问题场景:
通过 基类指针/引用 删除 派生类对象 时:
Base* p = new Derived;
delete p; // 若 Base::~Base 不是 virtual
如果 Base::~Base
不是虚函数,只会调用 Base
的析构函数,
导致派生类部分 资源泄漏(派生类的析构函数不会执行)。
如果 Base::~Base
是虚函数,则会按 动态类型 调用 Derived::~Derived
,
再自动沿继承链向上依次调用各基类析构函数,保证彻底清理。
结论:只要类可能被继承,且会通过基类接口多态地销毁对象,基类就必须把析构函数声明为 virtual(即使函数体为空)。确保在通过基类指针或引用删除派生类对象时, 能够正确调用派生类的析构函数,避免资源泄漏 和其他未定义行为。
注意:析构函数可以是纯虚的(= 0
),但必须提供实现体(因为派生类析构时会静态调用它)。C++11 起可用 override
标出派生类析构函数,防止签名写错。
一句话总结:构造自上而下,析构自下而上;多态删除时,虚析构保平安
四、多态的两种形态
(一)静态多态(编译期)
本质:在编译阶段就把“调用哪个函数”完全决定下来,不需要 vtable、RTTI,运行时没有任何额外开销。
常见技术:
函数重载:是 C++ 在同一作用域内允许同名函数存在,只要参数表不同。编译期根据实参列表(个数、类型、顺序)进行静态绑定,选中最佳匹配函数。它与(重写)覆盖 override(运行时多态)和(重定义)隐藏 hiding(名字遮掩)完全不同。
运算符重载:把 C++ 内建的运算符(+、-、*、<<、[] …)扩展到自定义类型,使对象像内置类型一样直观使用。本质是“语法糖”:重载后仍是一个函数,只是调用写法变成了运算符形式
(二)动态多态(运行期)
本质:用基类指针/引用操作一组派生类对象,具体调用哪个函数在运行期根据对象实际类型决定。
原理:每个有虚函数的类都有一个虚表,虚表是一个静 态数组,存储了一个类中所有虚函数的地址。虚函数重写以后,父类对象虚表指针指向的虚函 数表存的是父类的虚函数,子类对象虚表指针指向的虚函 数表存的是子类的虚函数。父类的指针或引用调用虚函数->去指向的对象虚表中查找对应的虚函数进行调用,最终指向了谁 就调用谁
虚函数 virtual:是 C++ 用来实现运行时多态的核心机制。在基类中把成员函数声明为 virtual
,派生类可重写(override)它;通过基类指针/引用调用时,根据对象的实际类型在运行时动态分派。
抽象类(纯虚函数):含有至少一个纯虚函数(pure virtual function)的类;不能实例化,只能用作基类,为派生类规定“接口契约”。强制子类重写,不重写,依旧是抽象类。纯虚函数在基类中无实现,以 = 0
语法标记。
动态多态示例:
struct Shape { virtual void draw() const = 0; };
struct Circle : Shape { void draw() const override { /* ... */ } };
struct Square : Shape { void draw() const override { /* ... */ } };
void render(const Shape& s) { s.draw(); } // 运行期动态分派
必备三要素:
1. 基类声明虚函数
2. 派生类重写
3. 通过基类接口调用
(三)虚函数与vptr/虚函数表
对象布局
编译器在每个含虚函数的类中生成一张虚函数表 vtable(静态数组,类共享)。每个对象首地址前隐式插入一个隐藏指针 vptr,指向所属类的 vtable。
调用流程
Base* p = new Derived;
p->vf(); // 伪代码展开
读取 p
→ 获得 vptr
→ 找到 Derived
的 vtable → vtable[index] → 拿到真正的函数地址 → 调用该地址
关键细节
构造时:对象先设为当前正在构造类的 vtable;派生类构造完成后才指向“最终”vtable。
析构时:沿继承链反向逐层调整 vtable,保证析构过程中虚调用不会跑到已销毁的子类。
多重继承时:一个对象可能有多个 vptr(每个基类子对象各一个)。
虚析构函数:在 vtable 中单独占一个 slot;delete base_ptr
会走该 slot,确保派生类析构函数被调用。
虚函数表本身对代码段只读,不占用对象额外空间(除 vptr)。
(四)override和final
1、override
override
关键字用于显式声明派生类中的函数覆盖了基类的虚函数。它有以下作用:
-
明确意图:告诉编译器,这个函数是覆盖基类的虚函数。
-
编译时检查:如果基类中没有对应的虚函数,编译器会报错,避免意外的覆盖。
-
提高可读性:让代码的意图更清晰,方便维护
2. final
final
关键字用于禁止派生类覆盖某个虚函数,或者禁止某个类被继承。它有以下两种用法:
-
禁止覆盖虚函数:在派生类中,可以使用
final
修饰虚函数,防止进一步覆盖。 -
禁止继承类:可以使用
final
修饰类,防止该类被继承。
(五)多态小结
动态多态 | 静态多态 | |
---|---|---|
绑定时机 | 运行期 | 编译期 |
机制 | vtable + vptr | 模板 / CRTP |
开销 | 一次间接寻址 | 0(可内联) |
灵活性 | 高:可运行时替换实现 | 低:类型需编译期已知 |
典型用途 | 插件、脚本、GUI 命令 | 数值算法、容器、策略 |
静态多态:编译期把路修好,跑车零延迟;
动态多态:运行期看路标(vptr)决定走哪条道;
虚函数 = vtable + vptr + RTTI 基础设施,是 C++ 实现“多态删除、异质容器、插件”的基石。
六、重载、重写(覆盖)、重定义(隐藏)
(一)重载(在同一个作用域中)
-
定义:在同一个作用域内,函数或运算符的名字相同,但参数列表(参数的类型、个数或顺序)不同。
-
特点:
-
参数列表必须不同。
-
返回类型可以相同也可以不同,但不能仅靠返回类型来区分重载。
-
-
用途:提供多种接口,方便调用。
-
示例:
// 重载函数 void print(int num) { cout << "打印整数:" << num << endl; } void print(double num) { cout << "打印浮点数:" << num << endl; }
(二)重写或覆盖(分别在基类和派生类两个作用域中)
-
定义:在派生类中重新定义基类的虚函数,且函数名、参数列表和返回类型完全相同。特例是协变,允许返回类型不同(引用或者指针)
-
特点:
-
必须是虚函数。
-
参数列表和返回类型必须完全一致。
-
两个函数必须是虚函数,基类若是虚函数,派生类的virtual关键字可以省略。
-
-
用途:实现多态,派生类可以改变基类的行为。
-
示例:
class Base { public: virtual void show() { cout << "基类的 show 函数" << endl; } }; class Derived : public Base { public: void show() override { // 重写基类的虚函数 cout << "派生类的 show 函数" << endl; } };
(三)重定义或隐藏(分别在基类和派生类两个作用域中)
-
定义:在派生类中定义了一个与基类同名的函数(可以参数列表不同,也可以相同)。
-
特点:
-
如果参数列表不同,会隐藏基类中所有同名函数(包括重载版本)。
-
如果参数列表相同,会隐藏基类中同名的函数。
-
两个基类和派生类的同名函数不构成重写就是重定义
-
-
用途:派生类可以完全覆盖基类的同名函数,但不会实现多态。
-
示例:
class Base { public: void show() { cout << "基类的 show 函数" << endl; } }; class Derived : public Base { public: void show(int num) { // 隐藏基类的 show 函数 cout << "派生类的 show 函数,参数为:" << num << endl; } };
七、切片问题
产生切片的 3 个典型场景
1. 按值赋值
Derived d;
Base b = d; // 切片
2. 按值传参
void foo(Base b); // 调用 foo(d) 时发生切片
3. 按值返回
Base make() {
Derived d;
return d; // 返回时切片
}
一句话总结:把派生类对象按值赋给基类变量(或直接传值、返回基类值)时,派生类独有的成员被“切掉”,只剩下基类子对象部分,导致信息丢失、行为错乱,且不再有多态能力。
八、多重继承
class D : public A, public B { … };
多重继承存在的问题:
二义性:两个基类都有同名函数,需要 D::foo() 显式指定或用 using 声明。
菱形继承示意图:
问题:D 中有两份 A 子对象 → 空间浪费、二义性。
解决:菱形继承中最先公共继承的类需要虚继承
class A { … };
class B : virtual public A { … };
class C : virtual public A { … };
class D : public B, public C { … };
此时 D 只有一份 A 子对象,B 和 C 共享。
虚继承解决数据冗余和二义性的原理: 虚基表(存的是base对象的偏移量)
虚基表(Vbtable)是一个数组,存储了虚基类对象的偏移量。每个派生类对象都有一个指向其虚基表的指针(Vbptr)。虚基表的结构如下:
-
Vbptr:指向虚基表的指针。
-
Vbtable:存储虚基类对象的偏移量。
九、总结
本文系统讲解了C++继承与多态的核心概念。重点介绍了三种继承方式public/protected/private的权限控制差异,深入剖析了派生类对象的构造/析构顺序,强调虚析构函数在多态删除时的重要性。详细对比了静态多态(重载、模板)和动态多态(虚函数、抽象类)的实现机制,特别分析了虚函数表的底层原理。同时指出了切片问题的产生场景及多重继承中的二义性问题解决方案。通过继承实现代码复用,借助多态实现运行时行为差异化,是C++面向对象编程的关键技术。