C++ 多态底层实现机制深度解析
在之前的讨论中,我们初步了解了虚函数、重写、纯虚函数等语法层面的知识。但很多同学在深入理解时仍会有疑问:为什么带虚函数的类 sizeof 会变大?基类指针指向不同派生类对象时,运行时如何找到对应函数?虚表、虚指针到底存在内存的哪个区域?
本文将从内存布局、对象模型及汇编视角出发,彻底讲透 C++ 多态的底层原理。
虚函数与普通函数的区别
我们可以通过一个简单的例子来观察内存差异。
class Base {
public:
virtual void Func1() { cout << "Func1()" << endl; }
protected:
int _b = 1;
char _ch = 'x';
};
int main() {
Base b;
cout << sizeof(b) << endl;
return 0;
}
对于普通类,成员函数不占用对象内存空间。理论上 _b (4 字节) + _ch (1 字节) 经过内存对齐后应为 8 字节。但实际运行结果是 12 字节。
这是因为编译器在含有虚函数的类对象中隐藏添加了一个指针,称为虚函数表指针(vptr)。一个含有虚函数的类至少包含一个 vptr,它指向该类的虚函数表(vtable)。

多态的实现原理
动态绑定与静态绑定
多态的核心在于动态绑定。当满足多态条件(指针或引用 + 调用虚函数)时,编译器不会在编译期确定函数地址,而是生成代码让程序在运行时通过对象的 vptr 找到对应的虚表,再从虚表中获取函数地址进行调用。
如果不满足多态条件(如直接调用非虚函数),则发生静态绑定,编译器直接在编译期确定函数地址。
class Person {
public:
virtual void BuyTicket() { cout << "买票 - 全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票 - 打折" << endl; }
private:
string _id;
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "买票 - 优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr) {
// 虽然都是 Person 指针,但具体调用哪个函数取决于 ptr 指向的对象类型
ptr->BuyTicket();
}
int main() {
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}
从汇编层面看,动态绑定涉及间接寻址。例如 ptr->BuyTicket() 会被编译为类似以下的指令序列:先取出指针,再取出指针指向的虚表地址,最后跳转到虚表中记录的函数地址。
mov eax, dword ptr [ptr]
mov edx, dword ptr [eax]
call dword ptr [edx]
相比之下,静态绑定则是直接调用固定地址。
call Student::Student
虚函数表结构
- 虚表内容:基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表。
- 继承覆盖:派生类重写基类虚函数时,派生类虚表中对应的条目会被覆盖为派生类重写的函数地址。派生类虚表通常包含:(1) 基类未重写的虚函数地址,(2) 派生类重写的虚函数地址,(3) 派生类新增的虚函数地址。
- 虚表位置:虚函数本身是代码段中的指令,而虚函数表(存储函数指针的数组)通常位于代码段的常量区(Read-Only Data Segment)。
- 结束标记:部分编译器(如 VS)会在虚表末尾放置
0x00000000作为结束标记,但这并非 C++ 标准规定,g++ 等编译器可能不包含此标记。
我们可以通过打印地址来验证虚表的存储位置。
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main() {
Base b;
Derive d;
Base* p3 = &b;
Derive* p4 = &d;
printf("栈:%p\n", &b);
printf("Person 虚表地址:%p\n", *(int*)p3);
printf("Student 虚表地址:%p\n", *(int*)p4);
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数地址:%p\n", &Base::func5);
return 0;
}
运行结果通常会显示虚表地址与普通函数地址都在代码段区域,且虚表地址独立于对象实例。

通过上述分析可以看出,多态并非简单的语法糖,而是一套完整的运行时机制。理解 vptr 和 vtable 的交互,有助于我们在编写高性能 C++ 代码时做出更合理的内存与性能权衡。


