C++虚函数表深度解析|看懂多态底层汇编
前言
很多同学学多态只背结论:虚函数实现多态,父类指针指向子类对象调用子类虚函数,但是不知道内存里面发生了什么。本文从内存布局、虚表、汇编指令、大小计算,把虚函数表底层讲清楚。
一、带虚函数的类对象内存布局
class Base { public: virtual void Func1() { cout << "Func1()" << endl; } protected: int _b = 1; char _ch = 'x'; };32 位环境下求
sizeof(Base)
内存构成
_vfptr虚函数表指针(虚表指针),32 位占 4 字节,放在对象内存最开头。普通成员变量:
int _b(4 字节),char _ch(1 字节),内存对齐补齐 3 字节。
32 位下总大小:4 + 4 + 1 + 3(对齐) = 12,答案选 D。
重点:只要类里面有 virtual 虚函数,每个对象开头都会多一个虚表指针,不是类共用一份指针,是每个对象都存指针;而虚函数表本身,同类所有对象共享一张。
什么是虚函数表 _vftable
虚函数表本质:函数指针数组,数组里面存放本类所有虚函数的地址。
_vfptr:对象内指针,指向这张函数指针数组。同类型多个对象,
_vfptr都指向同一个虚表,节省内存。不同类(父类、派生类)拥有各自独立虚表。
二、派生类虚表,重写 (override) 发生了什么
派生类先继承父类虚表指针;
如果派生类重写父类虚函数:派生类虚表对应位置,覆盖成自己重写后的函数地址;
没有重写的虚函数,直接拷贝父类虚函数地址;
派生类自己新增虚函数,追加到虚表数组后面。
父类对象 和 子类对象 的虚表指针,指向两张完全不同的虚函数表。
图示:
Person 虚表:
[Person::BuyTicket, Person::Func2...]Student 重写 BuyTicket 之后,Student 虚表:
[Student::BuyTicket, Person::Func2...]
对应位置直接替换,其他虚函数保留父类版本。
三、多态运行:编译 vs 运行(看汇编最直观)
Person* ptr = new Student; ptr->BuyTicket();
✅满足多态条件(虚重写 + 指针 / 引用):运行时寻址
汇编指令流程:
从
ptr拿到对象地址从对象起始位置取出虚表指针_vfptr
根据偏移,去虚函数表数组里面拿对应虚函数地址
call 这个函数地址
真正调用哪个函数,运行的时候才确定。指针本身类型不重要,看指针指向的真实对象的虚表。
❌不满足多态:普通调用,编译期直接定死地址
Person p; p.BuyTicket();
编译阶段直接把Person::BuyTicket的硬编码地址写死到 call 指令,直接调用,不走虚表。
核心区别总结
多态调用:运行时,去对象虚表里找函数地址
普通成员调用:编译期直接确定函数地址
四、虚函数表存放在内存哪个区段
C++ 标准没有规定虚表放在哪里,由编译器实现决定。
VS/MSVC:虚函数表放在常量区 (.rdata 只读段),属于程序常量,运行时不可修改。
g++:同样放置只读数据段。 虚函数本身的函数本体,存放在代码段
.text;虚表只存虚函数的指针地址。
注意区分:
_vfptr:每个对象里面的成员变量,跟着对象走(栈 / 堆看对象在哪创建)虚表_vftable:全局只读常量,程序启动就存在。
五、常见考点总结
只要类有虚函数,对象开头增加虚表指针;虚表多个对象共享一张。
派生类重写虚函数:派生类虚表对应下标位置替换为自己函数地址。
多态三要素缺一不可:
①父类函数 virtual 虚函数
②派生类完成重写 override
③使用父类指针 / 引用接收子类对象
满足多态:运行查表;不满足:编译期直接调用。
虚表属于只读常量区;虚表指针是对象成员,随对象存储位置变化。
六、易踩坑选择题回顾
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; }32 位编译器运行结果:
12
虚表指针 4 + int4 + char1 + 内存对齐填充 3 = 12。
补充:链接报错 LNK2019 和虚函数小提示
截图出现 LNK2019 无法解析外部符号:
如果只写虚函数声明,没有写函数实现就会报链接错误。
virtual void func(int); //只有声明,无定义 → LNK2019
如果不想实现,写成纯虚函数
virtual void func(int) = 0;
