在 C++ 中,多态是面向对象的核心特性之一。理解它不能只停留在语法层面,深入到底层机制才能写出更稳健的代码。
纯虚函数与抽象类
当虚函数声明后加上 = 0,它就变成了纯虚函数。这类函数不需要在基类中提供实现(虽然语法允许),主要作用是强制派生类重写。包含纯虚函数的类被称为抽象类,抽象类无法实例化对象,但可以通过指针或引用指向其派生类对象,从而实现多态。如果派生类没有重写所有纯虚函数,它自身也会成为抽象类。
多态的底层原理
虚函数表指针
当一个类包含虚函数时,编译器会在对象内存布局的最起始位置插入一个隐藏的指针成员,称为虚函数表指针(vptr)。
- 大小:32 位系统占 4 字节,64 位系统占 8 字节。
- 作用:指向该类对应的虚函数表(vtable)。
虚函数表是一张只读的函数指针数组,存放着该类所有虚函数的地址。每个包含虚函数的类都有且仅有一张虚表。创建对象时,vptr 会被自动初始化为指向这张表的起始地址。
内存布局与 sizeof
计算含虚函数类的 sizeof 时,除了成员变量和对齐填充,还必须计入 vptr 的大小。
假设有一个 32 位程序中的类:
class Base {
public:
virtual void Func1() { cout << "Func1()" << endl; }
protected:
int _b = 1; // 4 字节
char _ch = 'x'; // 1 字节
};
内存布局如下:
- vptr:占据前 4 字节。
- _b:占据 4 字节。
- _ch:占据 1 字节。
- 对齐填充:由于最大基本类型成员是
int(4 字节),整体大小需为 4 的倍数。当前已用 9 字节,编译器会在_ch后填充 3 个字节。
总大小 = 4 (vptr) + 4 (_b) + 1 (_ch) + 3 (填充) = 12 字节。这就是为什么含虚函数的类通常比预期大。
动态绑定与静态绑定
多态的本质是运行时绑定(动态绑定)。
- 静态绑定:不满足多态条件(如直接调用非虚函数或通过对象名调用)时,编译器在编译阶段就确定了函数地址。
- 动态绑定:通过基类指针或引用调用虚函数时,编译器生成代码,在运行时通过对象的 vptr 找到虚函数表,再根据偏移量获取实际函数地址。
从汇编层面看,普通调用是直接跳转固定地址(如 call Person::BuyTicket),而多态调用则是先读取指针内容,再间接跳转(如 mov eax, [ptr]; call [eax])。
虚函数表的结构
派生类重写基类虚函数时,其虚表中对应位置的地址会被替换为派生类的实现。虚函数表通常以 0x00000000 结尾作为标记(VS 编译器常见,GCC 可能不同)。


