三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

C++虚函数表深度解析|看懂多态底层汇编

C++虚函数表深度解析|看懂多态底层汇编

前言

很多同学学多态只背结论:虚函数实现多态,父类指针指向子类对象调用子类虚函数,但是不知道内存里面发生了什么。本文从内存布局、虚表、汇编指令、大小计算,把虚函数表底层讲清楚。

一、带虚函数的类对象内存布局

class Base { public: virtual void Func1() { cout << "Func1()" << endl; } protected: int _b = 1; char _ch = 'x'; };

32 位环境下求sizeof(Base)

内存构成

  1. _vfptr虚函数表指针(虚表指针),32 位占 4 字节,放在对象内存最开头。

  2. 普通成员变量:int _b(4 字节),char _ch(1 字节),内存对齐补齐 3 字节。

32 位下总大小:4 + 4 + 1 + 3(对齐) = 12,答案选 D。

重点:只要类里面有 virtual 虚函数,每个对象开头都会多一个虚表指针,不是类共用一份指针,是每个对象都存指针;而虚函数表本身,同类所有对象共享一张

什么是虚函数表 _vftable

虚函数表本质:函数指针数组,数组里面存放本类所有虚函数的地址。

  • _vfptr:对象内指针,指向这张函数指针数组。

  • 同类型多个对象,_vfptr都指向同一个虚表,节省内存。

  • 不同类(父类、派生类)拥有各自独立虚表。

二、派生类虚表,重写 (override) 发生了什么

  1. 派生类先继承父类虚表指针;

  2. 如果派生类重写父类虚函数:派生类虚表对应位置,覆盖成自己重写后的函数地址

  3. 没有重写的虚函数,直接拷贝父类虚函数地址;

  4. 派生类自己新增虚函数,追加到虚表数组后面。

父类对象 和 子类对象 的虚表指针,指向两张完全不同的虚函数表。

图示:

  • Person 虚表:[Person::BuyTicket, Person::Func2...]

  • Student 重写 BuyTicket 之后,Student 虚表:[Student::BuyTicket, Person::Func2...]

对应位置直接替换,其他虚函数保留父类版本。

三、多态运行:编译 vs 运行(看汇编最直观)

Person* ptr = new Student; ptr->BuyTicket();

✅满足多态条件(虚重写 + 指针 / 引用):运行时寻址

汇编指令流程:

  1. ptr拿到对象地址

  2. 从对象起始位置取出虚表指针_vfptr

  3. 根据偏移,去虚函数表数组里面拿对应虚函数地址

  4. call 这个函数地址

真正调用哪个函数,运行的时候才确定。指针本身类型不重要,看指针指向的真实对象的虚表。

❌不满足多态:普通调用,编译期直接定死地址

Person p; p.BuyTicket();

编译阶段直接把Person::BuyTicket的硬编码地址写死到 call 指令,直接调用,不走虚表。

核心区别总结

  • 多态调用:运行时,去对象虚表里找函数地址

  • 普通成员调用:编译期直接确定函数地址

四、虚函数表存放在内存哪个区段

C++ 标准没有规定虚表放在哪里,由编译器实现决定。

  • VS/MSVC:虚函数表放在常量区 (.rdata 只读段),属于程序常量,运行时不可修改。

  • g++:同样放置只读数据段。 虚函数本身的函数本体,存放在代码段.text虚表只存虚函数的指针地址

注意区分:

  • _vfptr每个对象里面的成员变量,跟着对象走(栈 / 堆看对象在哪创建)

  • 虚表_vftable:全局只读常量,程序启动就存在。

五、常见考点总结

  1. 只要类有虚函数,对象开头增加虚表指针;虚表多个对象共享一张。

  2. 派生类重写虚函数:派生类虚表对应下标位置替换为自己函数地址。

  3. 多态三要素缺一不可:

    1. ①父类函数 virtual 虚函数

    2. ②派生类完成重写 override

    3. ③使用父类指针 / 引用接收子类对象

  4. 满足多态:运行查表;不满足:编译期直接调用。

  5. 虚表属于只读常量区;虚表指针是对象成员,随对象存储位置变化。

六、易踩坑选择题回顾

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;

← 返回列表