c++对象模型-虚函数总结
一.虚函数指针位置分析:
1. 基本概念
- 虚函数表生成机制:当类中包含虚函数时,编译器会为该类生成一个虚函数表(vtbl)
- 对象内存布局:每个对象实例会包含一个虚函数表指针(vptr),指向该类的虚函数表起始地址
- 指针大小:在VS2017环境下,vptr占用4字节空间(Linux环境下为8字节)
#include "pch.h" #include <iostream> using namespace std; class A { public: int i; //4字节 virtual void testfunc() {} //虚函数,vptr4字节。 }; int main() { //虚函数表指针位置分析 //类:有虚函数,这个类会产生一个虚函数表。 //类对象,有一个指针,指针(vptr)会指向这个虚函数表的开始地址。 A aobj; int ilen = sizeof(aobj); cout << ilen << endl; //8字节 char *p1 = reinterpret_cast<char *>(&aobj); //类型转换,硬转 &aobj这是对象aobj的首地址。 char *p2 = reinterpret_cast<char *>(&(aobj.i)); if (p1 == p2) //说明aobj.i和aobj的位置相同,说明i在对象aobj内存布局的上边。虚函数表指针vptr在下边 { cout << "虚函数表指针位于对象内存的末尾" << endl; } else { cout << "虚函数表指针位于对象内存的开头" << endl; } return 1; }
2. 指针位置验证实验
- 实验设计:
- 创建包含虚函数和成员变量的测试类
- 通过比较对象首地址和成员变量地址判断vptr位置
- 使用sizeof验证对象总大小(VS中为8字节:4字节vptr+4字节int)
- 关键代码:
3. 内存布局分析
- 验证方法:
- 将对象地址强制转换为char*获取首地址(p1)
- 获取成员变量地址强制转换为char*(p2)
- 比较p1和p2的地址关系
- 结论:
- 当p1 != p2时,说明vptr位于对象内存布局的开头
- 在VS2017和Linux环境下验证结果一致
- 成员变量位于vptr之后的内存空间
4. 跨平台差异
- 指针大小差异:
- Windows平台:指针4字节(32/64位程序可能不同)
- Linux平台:指针8字节(64位系统)
- 布局一致性:
- 虽然指针大小不同,但vptr位置规律相同
- 都位于对象内存布局的开头位置
- 注意事项:
- 实际开发中需要考虑平台差异带来的对象大小变化
- 内存对齐规则可能影响最终对象大小
5. 关键结论
![]()
- 核心发现:
- 虚函数表指针始终位于对象内存布局的最前端
- 成员变量紧随其后排列
- 图示中的虚函数表位置需要修正为上方的正确布局
- 应用价值:
- 理解此布局有助于调试内存相关问题
- 为后续多重继承、虚继承的内存布局学习奠定基础
- 解释多态调用的底层机制
二、知识小结
|
知识点 |
核心内容 |
考试重点/易混淆点 |
难度系数 |
|
虚函数表指针的存在 |
类中包含虚函数时,编译器会为该类生成虚函数表(vtable),对象中存储指向该表的指针(vptr)。 |
vptr的存储位置(对象内存开头/末尾)因编译器而异,需通过调试验证。 |
⭐⭐ |
|
对象内存布局分析 |
通过sizeof和地址转换验证对象内存结构: - vptr(4字节,Windows) - 成员变量(如int i) |
Windows vs Linux指针大小差异: - Windows:4字节 - Linux:8字节 |
⭐⭐⭐ |
|
虚函数表指针位置验证 |
实验方法: 1. 比较对象首地址与成员变量地址 2. 若地址相同→vptr在末尾;若不同→vptr在开头 |
结论: 在Windows/Linux平台,vptr均位于对象内存开头。 |
⭐⭐⭐⭐ |
|
跨平台差异 |
指针大小差异导致sizeof结果不同: - Windows:vptr+int=8字节 - Linux:vptr+int=16字节 |
易混淆点: 内存对齐规则不影响vptr位置,仅影响总大小。 |
⭐⭐ |
二. 继承关系作用下虚函数的手工调用
#include "pch.h" #include <iostream> using namespace std; //父类 class Base { public: virtual void f() { cout << "Base::f()" << endl; } virtual void g() { cout << "Base::g()" << endl; } virtual void h() { cout << "Base::h()" << endl; } }; class Derive :public Base { virtual void g() { cout << "Derive::g()" << endl; } }; int main() { //继承关系作用下虚函数的手工调用 cout << sizeof(Base) << endl; cout << sizeof(Derive) << endl; Derive *d = new Derive(); //派生类指针。 long *pvptr = (long *)d; //指向对象的指针d转成了long *类型。 long *vptr = (long *)(*pvptr); //(*pvptr) 表示pvptr指向的对象,也就是Derive本身。Derive对象是4字节的,代表的是虚函数表指针地址。 for (int i = 0; i <= 4; i++) //循环5次; { printf("vptr[%d] = 0x:%p\n", i, vptr[i]); } typedef void(*Func)(void); //定义一个函数指针类型 Func f = (Func)vptr[0]; //f就是函数指针变量。 vptr[0]是指向第一个虚函数的。 Func g = (Func)vptr[1]; Func h = (Func)vptr[2]; /*Func i = (Func)vptr[3]; Func j = (Func)vptr[4];*/ f(); g(); h(); //i(); Base *dpar = new Base(); long *pvptrpar = (long *)dpar; long *vptrpar = (long *)(*pvptrpar); for (int i = 0; i <= 4; i++) //循环5次; { printf("vptr Base[%d] = 0x:%p\n", i, vptrpar[i]); } Func fpar = (Func)vptrpar[0]; Func gpar = (Func)vptrpar[1]; Func hpar = (Func)vptrpar[2]; cout << "--------------------" << endl; fpar(); gpar(); hpar(); return 1; }
|
知识点 |
核心内容 |
关键代码/操作 |
内存布局观察 |
|
虚函数表指针验证 |
通过sizeof验证含虚函数的类对象包含4字节虚表指针(32位系统) |
sizeof(Base)/sizeof(Derive)输出4 |
父类/子类对象首4字节存储虚表指针地址 |
|
手工调用虚函数 |
通过指针转换获取虚表地址,并直接调用虚函数条目 |
long* vptr = (long*)*(long*)d; ((void(*)())vptr[0])(); |
虚表条目按声明顺序排列(f→g→h) |
|
继承下的虚函数覆盖 |
子类覆盖g()时,虚表中对应条目替换为子类实现地址 |
Derive::g()覆盖Base::g() |
父类虚表g()地址≠子类虚表g()地址 |
|
非法调用风险 |
越界访问虚表条目(如vptr[3])导致程序崩溃 |
尝试调用vptr[3]触发访问异常 |
虚表末尾以0x00000000标记结束 |
|
父类/子类虚表对比 |
未覆盖的虚函数(f/h)在父子类虚表中地址相同 |
Base::f地址=Derive::f地址 |
子类虚表复用父类实现的条目 |
三.虚函数表分析
#include "pch.h" #include <iostream> using namespace std; //父类 class Base { public: virtual void f() { cout << "Base::f()" << endl; } virtual void g() { cout << "Base::g()" << endl; } virtual void h() { cout << "Base::h()" << endl; } }; class Derive :public Base { virtual void g() { cout << "Derive::g()" << endl; } /* void f() { cout << "Derive::f()" << endl; } void g() { cout << "Derive::g()" << endl; } void h() { cout << "Derive::h()" << endl; }*/ }; int main() { ////继承关系作用下虚函数的手工调用 //cout << sizeof(Base) << endl; //cout << sizeof(Derive) << endl; //Derive *d = new Derive(); //派生类指针。 //Derive *d2 = new Derive(); //派生类指针。 //long *pvptr = (long *)d; //指向对象的指针d转成了long *类型。 //long *vptr = (long *)(*pvptr); //(*pvptr) 表示pvptr指向的对象,也就是Derive本身。Derive对象是4字节的,代表的是虚函数表指针地址。 //long *pvptr2 = (long *)d2; //long *vptr2 = (long *)(*pvptr2); //for (int i = 0; i <= 4; i++) //循环5次; //{ // printf("vptr[%d] = 0x:%p\n", i, vptr[i]); //} //typedef void(*Func)(void); //定义一个函数指针类型 //Func f = (Func)vptr[0]; //f就是函数指针变量。 vptr[0]是指向第一个虚函数的。 //Func g = (Func)vptr[1]; //Func h = (Func)vptr[2]; ///*Func i = (Func)vptr[3]; //Func j = (Func)vptr[4];*/ //f(); //g(); //h(); ////i(); //Base *dpar = new Base(); //long *pvptrpar = (long *)dpar; //long *vptrpar = (long *)(*pvptrpar); //for (int i = 0; i <= 4; i++) //循环5次; //{ // printf("vptr Base[%d] = 0x:%p\n", i, vptrpar[i]); //} //Func fpar = (Func)vptrpar[0]; //Func gpar = (Func)vptrpar[1]; //Func hpar = (Func)vptrpar[2]; //cout << "--------------------" << endl; //fpar(); //gpar(); //hpar(); //(1)一个类只有包含虚函数才会存在虚函数表,同属于一个类的对象共享虚函数表,但是有各自的vptr(虚函数表指针),当然所指向的地址(虚函数表首地址)相同。 //(2)父类中有虚函数就等于子类中有虚函数。话句话来说,父类中有虚函数表,则子类中肯定有虚函数表。因为你是继承父类的。 //也有人认为,如果子类中把父类的虚函数的virtual去掉,是不是这些函数就不再是虚函数了? //只要在父类中是虚函数,那么子类中即便不写virtual,也依旧是虚函数。 //但不管是父类还是子类,都只会有一个虚函数表,不能认为子类中有一个虚函数表+父类中有一个虚函数表, //得到一个结论:子类中有两个虚函数表。 //子类中是否可能会有多个虚函数表呢?后续我们讲解这个事; //(3)如果子类中完全没有新的虚函数,则我们可以认为子类的虚函数表和父类的虚函数表内容相同。 //但,仅仅是内容相同,这两个虚函数表在内存中处于不同位置,换句话来说,这是内容相同的两张表。 //虚函数表中每一项,保存着一个虚函数的首地址,但如果子类的虚函数表某项和父类的虚函数表某项代表同一个函数(这表示子类没有覆盖父类的虚函数), //则该表项所执行的该函数的地址应该相同。 //(4)超出虚函数表部分内容不可知; typedef void(*Func)(void); //定义一个函数指针类型 Derive derive; long *pvptrderive = (long *)(&derive); long *vptrderive = (long *)(*pvptrderive); //0x00b09b6c {project100.exe!void(* Derive::`vftable'[4])()} {11538847} Func f1 = (Func)vptrderive[0]; //0x00b0119f {project100.exe!Base::f(void)} Func f2 = (Func)vptrderive[1]; //0x00b0150f {project100.exe!Derive::g(void)} Func f3 = (Func)vptrderive[2]; //0x00b01325 {project100.exe!Base::h(void)} Func f4 = (Func)vptrderive[3]; //0x69726544 Func f5 = (Func)vptrderive[4]; //0x3a3a6576 Derive derive2 = derive; //调用拷贝构造函数 long *pvptrderive2 = (long *)(&derive2); long *vptrderive2 = (long *)(*pvptrderive2); Base base = derive; //直接用子类对象给父类对象值,子类中的属于父类那部分内容会被编译器自动区分(切割)出来并拷贝给了父类对象。 //所以Base base = derive;实际干了两个事情: //第一个事情:生成一个base对象 //第二个事情:用derive来初始化base对象的值。 //这里编译器给咱们做了一个选择,显然derive初始化base对象的时候, //derive的虚函数表指针值并没有覆盖base对象的虚函数表指针值,编译器帮我们做到了这点; long *pvptrbase = (long *)(&base); long *vptrbase = (long *)(*pvptrbase); //0x00b09b34 {project100.exe!void(* Base::`vftable'[4])()} {11538847} Func fb1 = (Func)vptrbase[0]; //0x00b0119f {project100.exe!Base::f(void)} Func fb2 = (Func)vptrbase[1]; //0x00b01177 {project100.exe!Base::g(void)} Func fb3 = (Func)vptrbase[2]; //0x00b01325 {project100.exe!Base::h(void)} Func fb4 = (Func)vptrbase[3]; //0x00000000 Func fb5 = (Func)vptrbase[4]; //0x65736142 //OO(面向对象) 和OB(基于对象)概念: //c++通过类的指针和引用来支持多态,这是一种程序设计风格,这就是我们常说的面向对象。object-oriented model; //OB(object-based),也叫ADT抽象数据模型【abstract datatype model】,不支持多态,执行速度更快,因为 //因为 函数调用的解析不需要运行时决定(没有多态),而是在编译期间就解析完成,内存空间紧凑程度上更紧凑,因为没有虚函数指针和虚函数表这些概念了; //Base *pbase = new Derive(); //Base &base2 = derive2; //但显然,OB的设计灵活性就差; //c++既支持面向对象程序设计(继承,多态)(OO),也支持基于对象(OB)程序设计。 return 1; }
四:多重继承虚函数表分析
#include "pch.h" #include <iostream> using namespace std; //基类1 class Base1 { public: virtual void f() { cout << "base1::f()" << endl; } virtual void g() { cout << "base1::g()" << endl; } }; //基类2 class Base2 { public: virtual void h() { cout << "base2::h()" << endl; } virtual void i() { cout << "base2::i()" << endl; } }; //子类 class Derived :public Base1, public Base2 { public: virtual void f() //覆盖父类1的虚函数 { cout << "derived::f()" << endl; } virtual void i() //覆盖父类2的虚函数 { cout << "derived::i()" << endl; } //如下三个我们自己的虚函数 virtual void mh() { cout << "derived::mh()" << endl; } virtual void mi() { cout << "derived::mi()" << endl; } virtual void mj() { cout << "derived::mj()" << endl; } }; int main() { // 多重继承虚函数表分析 //多重继承 cout << sizeof(Base1) << endl; cout << sizeof(Base2) << endl; cout << sizeof(Derived) << endl; Derived ins; Base1 &b1 = ins; //多态 Base2 &b2 = ins; Derived &d = ins; typedef void(*Func)(void); long *pderived1 = (long *)(&ins); long *vptr1 = (long *)(*pderived1); //取第一个虚函数表指针。 long *pderived2 = pderived1 + 1; //跳过4字。 long *vptr2 = (long *)(*pderived2); //取第二个虚函数表指针。 Func f1 = (Func)vptr1[0]; //0x00ab15d7 {project100.exe!Derived::f(void)} Func f2 = (Func)vptr1[1]; //0x00ab15f0 {project100.exe!Base1::g(void)} Func f3 = (Func)vptr1[2]; //0x00ab15cd {project100.exe!Derived::mh(void)} Func f4 = (Func)vptr1[3]; //0x00ab15ff {project100.exe!Derived::mi(void)} Func f5 = (Func)vptr1[4]; //0x00ab15eb {project100.exe!Derived::mj(void)} Func f6 = (Func)vptr1[5]; //非正常 Func f7 = (Func)vptr1[6]; Func f8 = (Func)vptr1[7]; Func f11 = (Func)vptr2[0]; //0x00ab15af {project100.exe!Base2::h(void)} Func f22 = (Func)vptr2[1]; //0x00ab15b9 {project100.exe!Derived::i(void)} Func f33 = (Func)vptr2[2]; //非正常 Func f44 = (Func)vptr2[3]; b1.f(); b2.i(); d.f(); d.i(); d.mh(); d.g(); //---------------- cout << "-----------------" << endl; f1(); f2(); f3(); f4(); f5(); cout << "-------------" << endl; f11(); f22(); //说明 //(1)一个对象,如果它的类有多个基类则有多个虚函数表指针(注意是两个虚函数表指针,而不是两个虚函数表); //在多继承中,对应各个基类的vptr按继承顺序依次放置在类的内存空间中,且子类与第一个基类共用一个vptr(第二个基类有自己的vptr); //(2)老师画图:适合vs2017。 //(2.1)子类对象ins有里那个虚函数表指针,vptr1,vptr2 //(2.2)类Derived有两个虚函数表,因为它继承自两个基类; //(2.3)子类和第一个基类公用一个vptr(因为vptr指向一个虚函数表,所以也可以说子类和第一个基类共用一个虚函数表vtbl), //因为我们注意到了类Derived的虚函数表1里边的5个函数,而g()正好是base1里边的函数。 //(2.4)子类中的虚函数覆盖了父类中的同名虚函数。比如derived::f(),derived::i(); return 1; }
五:辅助工具:
#include "pch.h" #include <iostream> using namespace std; //基类1 class Base1 { public: virtual void f() { cout << "base1::f()" << endl; } virtual void g() { cout << "base1::g()" << endl; } }; //基类2 class Base2 { public: virtual void h() { cout << "base2::h()" << endl; } virtual void i() { cout << "base2::i()" << endl; } }; //子类 class Derived :public Base1, public Base2 { public: virtual void f() //覆盖父类1的虚函数 { cout << "derived::f()" << endl; } virtual void i() //覆盖父类2的虚函数 { cout << "derived::i()" << endl; } //如下三个我们自己的虚函数 virtual void mh() { cout << "derived::mh()" << endl; } virtual void mi() { cout << "derived::mi()" << endl; } virtual void mj() { cout << "derived::mj()" << endl; } }; int main() { //多重继承 cout << sizeof(Base1) << endl; cout << sizeof(Base2) << endl; cout << sizeof(Derived) << endl; Derived ins; Base1 &b1 = ins; //多态 Base2 &b2 = ins; Derived &d = ins; typedef void(*Func)(void); long *pderived1 = (long *)(&ins); long *vptr1 = (long *)(*pderived1); //取第一个虚函数表指针。 long *pderived2 = pderived1 + 1; //跳过4字。 long *vptr2 = (long *)(*pderived2); //取第二个虚函数表指针。 Func f1 = (Func)vptr1[0]; //0x00ab15d7 {project100.exe!Derived::f(void)} Func f2 = (Func)vptr1[1]; //0x00ab15f0 {project100.exe!Base1::g(void)} Func f3 = (Func)vptr1[2]; //0x00ab15cd {project100.exe!Derived::mh(void)} Func f4 = (Func)vptr1[3]; //0x00ab15ff {project100.exe!Derived::mi(void)} Func f5 = (Func)vptr1[4]; //0x00ab15eb {project100.exe!Derived::mj(void)} Func f6 = (Func)vptr1[5]; //非正常 Func f7 = (Func)vptr1[6]; Func f8 = (Func)vptr1[7]; Func f11 = (Func)vptr2[0]; //0x00ab15af {project100.exe!Base2::h(void)} Func f22 = (Func)vptr2[1]; //0x00ab15b9 {project100.exe!Derived::i(void)} Func f33 = (Func)vptr2[2]; //非正常 Func f44 = (Func)vptr2[3]; b1.f(); b2.i(); d.f(); d.i(); d.mh(); d.g(); //---------------- cout << "-----------------" << endl; f1(); f2(); f3(); f4(); f5(); cout << "-------------" << endl; f11(); f22(); //第五节 辅助工具,vptr、vtbl创建时机 //cl.exe:编译链接工具 // cl /d1 reportSingleClassLayoutDerived project100.cpp // g++ -fdump-class-hierarchy -fsyntax-only 3_4.cpp //vptr(虚函数表指针)什么时候创建出来的? //vptr跟着对象走,所以对象什么时候创建出来,vptr就什么时候创建出来。运行的时候; //实际上,对于这种有虚函数的类,在编译的时候,编译器会往相关的构造函数中增加 为vptr赋值的代码,这是在编译期间编译器为构造函数增加的。 //这属于编译器默默为我们做的事,我们并不清楚。 //当程序运行的时候,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有 给对象的vptr(成员变量)赋值的语句,自然这个对象的vptr就被赋值了; //虚函数表是什么时候创建的? //实际上,虚函数表是编译器在编译期间(不是运行期间)就为每个类确定好了对应的虚函数表vtbl的内容。 //然后也是在编译器期间在相应的类构造函数中添加给vptr赋值的代码,这样程序运行的时候,当运行到成成类对象的代码时,会调用类的构造函数,执行到类的构造 //函数中的 给vptr赋值的代码,这样这个类对象的vptr(虚函数表指针)就有值了; return 1; }
六:单纯的类不纯时引发的虚函数调用问题
#include "pch.h" #include <iostream> #include <time.h > using namespace std; class X { public: int x; int y; int z; //X() :x(0), y(0), z(0) X() { //编译器角度 伪码; //vptr = vtbl; //下边的memset会把vptr(虚函数表指针)清0 memset(this, 0, sizeof(X)); cout << "构造函数被执行" << endl; } //X(const X &tm) :x(tm.x), y(tm.y), z(tm.z) X(const X &tm) { memcpy(this, &tm, sizeof(X)); cout << "拷贝构造函数被执行" << endl; } virtual ~X() { cout << "析构函数被执行" << endl; } virtual void virfunc() { cout << "虚函数virfunc()被执行" << endl; } void ptfunc() { cout << "普通函数ptfunc()被执行" << endl; } }; int main() { //第六节 单纯的类不纯时引发的虚函数调用问题 // 单纯的类:比较简单的类,尤其不包含 虚函数和虚基类。 //X x0; //调用构造函数 ///*x0.x = 100; //x0.y = 200; //x0.z = 300;*/ //x0.virfunc(); //虚函数表指针为null居然可以成功调用虚函数; //X x1(x0); //调用拷贝构造函数 //cout << "x1.x=" << x1.x << " x1.y=" << x1.y << " x1.z=" << x1.z << endl; //如果类并不单纯,那么在构造函数中使用如上所示的memset或者拷贝构造函数中使用如上所示的memcpy方法,那么就会出现程序崩溃的情形; //那就是某些情况下,编译器会往类内部增加一些我们看不见 但真实存在的成员变量(隐藏成员变量),有了这种变量的类,就不单纯了; //同时,这种隐藏的成员变量的 增加(使用) 或者赋值的时机,往往都是在 执行构造函数或者拷贝构造函数的函数体之前进行。 //那么你如果使用memset,memcpy,很可能把编译器给隐藏变量的值你就给清空了,要么覆盖了; //比如你类中增加了虚函数,系统默认往类对象中增加 虚函数表指针,这个虚函数表指针就是隐藏的成员变量。 //X *px0 = new X(); //px0->ptfunc(); //正常调用 //px0->virfunc(); //无法正常调用 //delete px0; //无法正常调用 //new出来的对象,虚函数变得无法正常执行了; //对多台,虚函数,父类子类。虚函数,主要解决的问题父类指针指向子类对象这种情况。 //只有虚函数,没有继承,那么虚函数和普通函数有啥区别呢? 老师认为此时就没啥时机区别。 int i = 9; printf("i的地址 = %p\n", &i); X x0; printf("ptfunc()的地址=%p\n", &X::ptfunc); //打印正常的成员函数地址。 //long *pvptrpar = (long *)(&x0); //long *vptrpar = (long *)(*pvptrpar); //printf("virfunc的地址 = %p\n", vptrpar[1]);//虚函数virfunc地址 x0.ptfunc(); x0.virfunc(); //不叫多态,属于静态联编 //我们推断:这个函数ptfunc()和virfunc()函数,是在编译的就确定好的; //静态联编 和动态联编。 //静态联编 :我们编译的时候就能确定调用哪个函数。把调用语句和倍调用函数绑定到一起; //动态联编:是在程序运行时,根据时机情况,动态的把调用语句和被调用函数绑定到一起,动态联编一般旨有在多态和虚函数情况下才存在。 X *pX0 = new X(); pX0->ptfunc(); pX0->virfunc(); //通过虚函数表指针,找虚函数表,然后从虚函数表中找到virfunc虚函数的地址并调用。 //更明白:虚函数,多态,这种概念专门给指针或者引用用的; X &xy = *pX0; xy.virfunc(); X &xy2 = x0; xy2.virfunc(); return 1; }


浙公网安备 33010602011771号