虚函数 & 虚继承 & 虚基类
| 概念 | virtual 写在何处? | 核心作用 | 内存/底层机制 |
|---|---|---|---|
| 虚函数 | 成员函数声明前 virtual void foo(); |
允许子类重写,实现动态多态(运行时确定调用哪个函数)。 | 对象中生成 vptr 指向 vtable(虚函数表)。 |
| 虚继承 | 继承列表的类名前 class B : virtual public A |
解决菱形继承导致的数据冗余与二义性。 | 对象中生成 vbptr 指向 vbtable(虚基类表),记录偏移量。 |
| 虚基类 | 被 virtual 继承的那个基类 在上例中 A 就是虚基类 |
作为多重继承中被共享的基类子对象。 | 在最顶层的派生类(如 D)内存布局中,只保留一份实例。 |
- "虚基类"是一个相对的概念 :
A本身定义时就是一个普通的class A。只有当B以virtual public A继承它时,在B(以及后代D)的视角里,A才被称为A是B的虚基类。 - 两者可以叠加 :一个类可以既有虚函数,又被虚继承。此时对象内部会同时存在
vptr(处理虚函数)和vbptr(处理虚继承偏移)。
虚继承与虚基类
- 应用场景: 解决多继承中的菱形继承问题。
- 核心痛点: 若类 B 和类 C 都继承自类 A,而类 D 同时继承 B 和 C,则类 D 中会包含两份类 A 的数据成员,导致数据冗余与访问二义性。
- 实现原理:
- 使用
class B : virtual public A进行虚继承。 - 编译器会在派生类中加入 虚基类指针(vbptr) ,指向 虚基类表(vbtable),通过偏移量找到唯一的基类 A 成员,确保派生类中只有一份共享的基类子对象。
- 使用
虚函数与多态
- 动态多态: 依靠
virtual关键字实现"运行时绑定"。 - 底层机制:
- 虚函数表指针(vptr): 每一个包含虚函数的类对象内部都隐式持有一个
vptr指针,指向该类的vtable。 - 虚函数表(vtable): 存储类中所有虚函数的入口地址。在运行时,通过指针/引用调用虚函数时,从
vtable中查找对应的实际执行函数。 - 虚析构函数: 当通过基类指针销毁派生类对象时,基类的析构函数必须声明为
virtual,否则只会调用基类析构函数,导致派生类特有资源内存泄漏。
Virtual
- 作用:
- 加在函数前面 :
virtual void foo();→\rightarrow→ 虚函数(实现动态多态)。 - 加在继承方式前面 :
class B : virtual public A→\rightarrow→ 虚继承 (A称为 虚基类)。
- 加在函数前面 :
- 接口
- 普通虚函数(有函数体) :定义默认行为,子类可以重写,也可以直接复用。
- 纯虚函数(
= 0) :专门用来定义规范/接口,强制要求派生类必须实现。
虚函数(加在成员函数前)
cpp
#include <iostream>
class Base {
public:
virtual void show() { // 这里的 virtual 表示"虚函数"
std::cout << "Base show" << std::endl;
}
};
class Derived : public Base { // 普通单继承
public:
void show() override {
std::cout << "Derived show" << std::endl;
}
};
虚继承与虚基类(加在继承列表前)
- 定义:只有在类继承时 使用
virtual关键字,才叫虚继承 ;被虚继承的那个基类,才叫虚基类。 - 虚继承的作用:解决菱形继承问题
virtual的作用:保证的是在这一个D对象 的内存空间里,基类A的成员只保留一份,而不是多个子对象各复制一份
cpp
#include <iostream>
// 1. 祖先类(待会被虚继承,它就是"虚基类")
class A {
public:
int m_data = 10;
};
// 2. 类 B 虚继承类 A(A 是 B 的虚基类)
class B : virtual public A {
// 注意看这里:virtual 加在 public A 前面!
};
// 3. 类 C 同样虚继承类 A(A 也是 C 的虚基类)
class C : virtual public A {
// 同样是 virtual public A
};
// 4. 类 D 多继承 B 和 C(菱形继承的终点)
class D : public B, public C {
// D 继承了 B 和 C
};
int main() {
D d;
// 因为用了虚继承,d 内部只有【一份】唯一的 A 对象!
d.m_data = 100; // ✅ 直接访问,没有歧义!
// 如果没有加 virtual 虚继承:
// d.m_data 会报错:error: request for member 'm_data' is ambiguous
// 因为必须写成 d.B::m_data 或 d.C::m_data
}
纯虚函数 & 抽象类
说明
- 英文:Pure Virtual Function
- 纯虚函数:只要在虚函数后面加上
= 0,它就变成了纯虚函数 - 抽象类:包含至少一个纯虚函数的类,就叫做抽象类(Abstract Class)
cpp
// 1. 抽象基类(相当于 Java 的 interface / abstract class)
class Shape {
public:
virtual void draw() = 0; // 纯虚函数 1
virtual void rotate() = 0; // 纯虚函数 2
virtual ~Shape() {} // 记得虚析构!
};
// Shape s; // ❌ 编译报错!抽象类不能直接实例化对象
特点
- 如果子类继承了抽象类,但没有全部重写 所有的纯虚函数,那么这个子类依然是一个抽象类,同样不能被实例化。
cpp
// 2. 子类 1:只实现了 draw(),没有实现 rotate()
class Polygon : public Shape {
public:
void draw() override {
// 实现了 draw
}
// rotate() 依然是纯虚函数!
};
// Polygon p; // ❌ 编译报错!Polygon 依然是抽象类,不能实例化!
// 3. 孙子类:终于把剩下的 rotate() 也实现了
class Triangle : public Polygon {
public:
void rotate() override {
// 实现了 rotate
}
};
Triangle t; // ✅ 成功!所有纯虚函数都被实现了,Triangle 是具体类
虚函数表
- 英文:Virtual Method Table /
vtable - 本质: 编译器在编译期为每个包含虚函数的类生成的只读函数指针数组。
- 内容: 里面按顺序存着该类所有虚函数的入口地址。如果派生类重写了基类的某个虚函数,
vtable对应位置的地址就会被替换为派生类自己的函数地址;如果没重写,就保留基类的函数地址。
虚函数表指针 (vptr)
- 本质: 存在于对象实例内存开头的一个隐藏指针。
- 作用: 实例化对象时,
vptr会被初始化指向该类对应的vtable。当你调用ptr->foo()时,CPU 实际执行的是:找到ptr→\rightarrow→ 读出vptr→\rightarrow→ 查vtable取第 N 个函数指针 →\rightarrow→ 跳转执行。