赞
踩
C++ 内存分为 5 个区域:
注意:静态局部变量也存储在全局/静态存储区,作用域为定义它的函数或语句块,生命周期与程序一致。
其中对象数据中存储非静态成员变量、虚函数表指针以及虚基类表指针(如果继承多个)。这里就有一个问题,既然对象里不存储类的成员函数的指针,那类的对象是怎么调用公用函数代码的呢?对象对公用函数代码的调用是在编译阶段就已经决定了的,例如有类对象a,成员函数为show(),如果有代码a.show(),那么在编译阶段会解释为 类名::show(&a)。会给show()传一个对象的指针,即this指针。
从上面的this指针可以说明一个问题:静态成员函数和非静态成员函数都是在类的定义时放在内存的代码区的,但是类为什么只能直接调用静态成员函数,而非静态成员函数(即使函数没有参数)只有类对象能够调用的问题?原因是类的非静态成员函数其实都内含了一个指向类对象的指针型参数(即this指针),因而只有类对象才能调用(此时this指针有实值)。
C++中虚函数是通过一张虚函数表(Virtual Table)来实现的,在这个表中,主要是一个类的虚函数表的地址表;这张表解决了继承、覆盖的问题。在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以当我们用父类的指针来操作一个子类的时候,这张虚函数表就像一张地图一样指明了实际所应该调用的函数。
C++编译器是保证虚函数表的指针存在于对象实例中最前面的位置(是为了保证取到虚函数表的最高的性能),这样我们就能通过已经实例化的对象的地址得到这张虚函数表,再遍历其中的函数指针,并调用相应的函数。
#include<iostream>
class Base1 {
public:
int base1_1;
int base1_2;
};
int main() {
std::cout << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl;
return 0;
}
运行结果
可以看到,成员变量是按照定义的顺序来保存的,最先声明的在最上边,然后依次保存!类对象的大小就是所有成员变量大小之和(严格说是成员变量内存对齐之后的大小之和)。
#include<iostream> class Base1 { public: int base1_1; int base1_2; virtual void base1_fun1() {} }; int main() { std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl; Base1 b1; return 0; }
运行结果:
多了4个字节?且 base1_1 和 base1_2 的偏移都各自向后多了4个字节!说明类对象的最前面被多加了4个字节的东西。
现在, 我们通过VS2022来瞧瞧类Base1的变量b1的内存布局情况:虚函数指针_vfptr位于所有的成员变量之前定义。
base1_1 前面多了一个变量 _vfptr (常说的虚函数表 vtable 指针),其类型为void**,这说明它是一个void*指针(注意不是数组)。
再看看[0]元素, 其类型为void*,其值为 ConsoleApplication2.exe!Base1::base1_fun1(void),这是什么意思呢?如果对 WinDbg 比较熟悉,那么应该知道这是一种惯用表示手法,她就是指 Base1::base1_fun1() 函数的地址。
可得,__vfptr的定义伪代码大概如下:
void* __fun[1] = { &Base1::base1_fun1 };
const void** __vfptr = &__fun[0];
大家有没有留意这个__vfptr?为什么它被定义成一个 指向指针数组的指针,而不是直接定义成一个 指针数组呢?我为什么要提这样一个问题?因为如果仅是一个指针的情况,您就无法轻易地修改那个数组里面的内容,因为她并不属于类对象的一部分。属于类对象的, 仅是一个指向虚函数表的一个指针_vfptr而已,注意到_vfptr前面的const修饰,她修饰的是那个虚函数表, 而不是__vfptr。
我们来用代码调用一下:
/** * x86 */ #include<iostream> class Base1 { public: int base1_1; int base1_2; virtual void base1_fun1() { std::cout << "Base1::base1_fun1" << std::endl; } virtual void base1_fun2() { std::cout << "Base1::base1_fun2" << std::endl; } }; /* 对(int*)*(int*)(&b)可以这样理解,(int*)(&b)就是对象b的地址,只不过被强制转换成了int*了,如果直接调用*(int*)(&b)则是指向对象b地址所指向的数据,但是此处是个虚函数表呀,所以指不过去,必须通过(int*)将其转换成函数指针来进行指向就不一样了,它的指向就变成了对象b中第一个函数的地址,所以(int*)*(int*)(&b)就是独享b中第一个函数的地址; 又因为pFun是由Fun这个函数声明的函数指针,所以相当于是Fun的实体,必须再将这个地址转换成pFun认识的,即加上(Fun)*进行强制转换:简要概括就是从b地址开始 读取四个字节的内容,然后将这个内容解释成一个内存地址,然后访问这个地址,然后将这个地址中存放的值再解释成一个函数的地址. */ typedef void(*Fn)(void); int main() { std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl; Base1 b1; Fn fn = nullptr; std::cout << "虚函数表的地址为:" << (int*)(&b1) << std::endl; std::cout << "虚函数表的第一个函数地址为:" << (int*)*(int*)(&b1) << std::endl; fn = (Fn) * ((int*)*(int*)(&b1) + 0); fn(); fn = (Fn) * ((int*)*(int*)(&b1) + 1); fn(); return 0; }
在上个代码调用虚函数的日子中你有没有注意到,多了一个虚函数, 类对象大小却依然是12个字节!
再看看VS形象的表现,_vfptr所指向的函数指针数组中出现了第2个元素,其值为Base1类的第2个虚函数base1_fun2()的函数地址。
现在, 虚函数指针以及虚函数表的伪定义大概如下:
void* __fun[] = { &Base1::base1_fun1, &Base1::base1_fun2 };
const void** __vfptr = __fun[0];
通过上面图表, 我们可以得到如下结论:
前面已经提到过: __vfptr只是一个指针,她指向一个数组,并且:这个数组没有包含到类定义内部,那么她们之间是怎样一个关系呢?
不妨,我们再定义一个类的变量b2,现在再来看看__vfptr的指向:
通过窗口我们看到:
由此我们可以总结出:同一个类的不同实例共用同一份虚函数表, 她们都通过一个所谓的虚函数表指针__vfptr(定义为void**类型)指向该虚函数表。
那么问题就来了! 这个虚函数表保存在哪里呢?
根据以上特征,虚函数表类似于类中静态成员变量。静态成员变量也是全局共享,大小确定。
所以我推测虚函数表和静态成员变量一样,存放在全局数据区。其实,我们无需过分追究她位于哪里,重点是:
#include<iostream> class Base1 { public: int base1_1; int base1_2; virtual void base1_fun1() {} virtual void base1_fun2() {} }; class Derive1 : public Base1 { public: int derive1_1; int derive1_2; }; int main() { std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl; Derive1 d1; return 0; }
现在类的布局情况应该是下面这样:
class Base1 { public: int base1_1; int base1_2; virtual void base1_fun1() {} virtual void base1_fun2() {} }; class Derive1 : public Base1 { public: int derive1_1; int derive1_2; // 覆盖基类函数 virtual void base1_fun1() {} }; int main() { std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl; std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl; Derive1 d1; return 0; }
特别注意那一行:原本是Base1::base1_fun1(),但由于继承类重写了基类Base1的此方法,所以现在变成了Derive1::base1_fun1()!
那么, 无论是通过Derive1的指针还是Base1的指针来调用此方法,调用的都将是被继承类重写后的那个方法(函数),多态发生了!!!
那么新的布局图:
class Base1 { public: int base1_1; int base1_2; virtual void base1_fun1() {} virtual void base1_fun2() {} }; class Derive1 : public Base1 { public: int derive1_1; int derive1_2; //和上上个类不同的是多了一个自身定义的虚函数. 和上个类不同的是没有基类虚函数的覆盖. virtual void derive1_fun1() {} };
为嘛呢?现在继承类明明定义了自身的虚函数,但不见了?类对象的大小,以及成员偏移情况居然没有变化!
既然表面上没办法了, 我们就只能从汇编入手了, 来看看调用derive1_fun1()时的代码:
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun1();
要注意:我为什么使用指针的方式调用?说明一下:因为如果不使用指针调用,虚函数调用是不会发生动态绑定的哦!你若直接 d1.derive1_fun1();是不可能会发生动态绑定的,但如果使用指针:pd1->derive1_fun1(); 那么 pd1就无从知道她所指向的对象到底是Derive1 还是继承于Derive1的对象,虽然这里我们并没有对象继承于Derive1,但是她不得不这样做,毕竟继承类不管你如何继承,都不会影响到基类,对吧?
pd1->derive1_fun1();
004A2233 mov eax,dword ptr [pd1]
004A2236 mov edx,dword ptr [eax]
004A2238 mov esi,esp
004A223A mov ecx,dword ptr [pd1]
004A223D mov eax,dword ptr [edx+8]
004A2240 call eax
汇编代码解释:
结果:
我们试着调用一下:
#include<iostream> class Base1 { public: int base1_1; int base1_2; virtual void base1_fun1() { std::cout << "base1_fun1" << std::endl; } virtual void base1_fun2() { std::cout << "base1_fun2" << std::endl; } }; class Derive1 : public Base1 { public: int derive1_1; int derive1_2; //和上上个类不同的是多了一个自身定义的虚函数. 和上个类不同的是没有基类虚函数的覆盖. virtual void derive1_fun1() { std::cout << "derive1_fun1" << std::endl; } }; typedef void(*Fn)(void); int main() { std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl; Base1 b1; Fn fn = nullptr; std::cout << "虚函数表的地址为:" << (int*)(&b1) << std::endl; std::cout << "虚函数表的第一个函数地址为:" << (int*)*(int*)(&b1) << std::endl; fn = (Fn) * ((int*)*(int*)(&b1) + 0); fn(); fn = (Fn) * ((int*)*(int*)(&b1) + 1); fn(); Derive1 d1; fn = (Fn) * ((int*)*(int*)(&d1) + 2); fn(); return 0; }
内存布局
#include<iostream> class Base1 { public: int base1_1; int base1_2; virtual void base1_fun1() {} virtual void base1_fun2() {} }; class Base2 { public: int base2_1; int base2_2; virtual void base2_fun1() {} virtual void base2_fun2() {} }; // 多继承 class Derive1 : public Base1, public Base2 { public: int derive1_1; int derive1_2; // 基类虚函数覆盖 virtual void base1_fun1() {} virtual void base2_fun2() {} // 自身定义的虚函数 virtual void derive1_fun1() {} virtual void derive1_fun2() {} }; int main() { std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl; std::cout << "地址偏移:" << sizeof(Base2) << " " << offsetof(Base2, base2_1) << " " << offsetof(Base2, base2_2) << std::endl; std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl; Derive1 d1; return 0; }
结论:
继承反汇编,这次的调用代码如下:
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();
反汇编代码如下:
pd1->derive1_fun2();
0008631E mov eax,dword ptr [pd1]
00086321 mov edx,dword ptr [eax]
00086323 mov esi,esp
00086325 mov ecx,dword ptr [pd1]
00086328 mov eax,dword ptr [edx+0Ch]
0008632B call eax
解释:
结论:Derive1的虚函数表依然是保存到第1个拥有虚函数表的那个基类的后面的.
类对象布局图:
#include<iostream> class Base1 { public: int base1_1; int base1_2; }; class Base2 { public: int base2_1; int base2_2; virtual void base2_fun1() {} virtual void base2_fun2() {} }; // 多继承 class Derive1 : public Base1, public Base2 { public: int derive1_1; int derive1_2; // 自身定义的虚函数 virtual void derive1_fun1() {} virtual void derive1_fun2() {} }; int main() { std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl; std::cout << "地址偏移:" << sizeof(Base2) << " " << offsetof(Base2, base2_1) << " " << offsetof(Base2, base2_2) << std::endl; std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl; Derive1 d1; Derive1* pd1 = &d1; pd1->derive1_fun2(); return 0; }
Base1已经没有虚函数表了吗?重点是看虚函数的位置,进入函数调用(和前一次是一样的):
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();
反汇编调用代码:
pd1->derive1_fun2();
008F667E mov eax,dword ptr [pd1]
008F6681 mov edx,dword ptr [eax]
008F6683 mov esi,esp
008F6685 mov ecx,dword ptr [pd1]
008F6688 mov eax,dword ptr [edx+0Ch]
008F668B call eax
这段汇编代码和前面一个完全一样,那么问题就来了,Base1 已经没有虚函数表了,为什么还是把Base1的第1个元素当作__vfptr呢?
不难猜测: 当前的布局已经发生了变化, 有虚函数表的基类放在对象内存前面?不过事实是否属实?需要仔细斟酌。
我们可以通过对基类成员变量求偏移来观察:
所以不难验证: 我们前面的推断是正确的, 谁有虚函数表, 谁就放在前面!
现在类的布局情况:
#include<iostream> class Base1 { public: int base1_1; int base1_2; }; class Base2 { public: int base2_1; int base2_2; }; // 多继承 class Derive1 : public Base1, public Base2 { public: int derive1_1; int derive1_2; // 自身定义的虚函数 virtual void derive1_fun1() {} virtual void derive1_fun2() {} }; int main() { std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl; std::cout << "地址偏移:" << sizeof(Base2) << " " << offsetof(Base2, base2_1) << " " << offsetof(Base2, base2_2) << std::endl; std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl; Derive1 d1; Derive1* pd1 = &d1; pd1->derive1_fun2(); return 0; }
可以看到, 现在__vfptr已经独立出来了, 不再属于Base1和Base2!
再看看偏移:
&d1==&d1.__vfptr 说明虚函数始终在最前面!
内存布局:
#include<iostream> class Base1 { public: int base1_1; int base1_2; virtual void base1_fun1() {} virtual void base1_fun2() {} }; class Base2 { public: int base2_1; int base2_2; }; class Base3 { public: int base3_1; int base3_2; virtual void base3_fun1() {} virtual void base3_fun2() {} }; // 多继承 class Derive1 : public Base1, public Base2, public Base3 { public: int derive1_1; int derive1_2; // 自身定义的虚函数 virtual void derive1_fun1() {} virtual void derive1_fun2() {} }; int main() { std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl; std::cout << "地址偏移:" << sizeof(Base2) << " " << offsetof(Base2, base2_1) << " " << offsetof(Base2, base2_2) << std::endl; std::cout << "地址偏移:" << sizeof(Base3) << " " << offsetof(Base3, base3_1) << " " << offsetof(Base3, base3_2) << std::endl; std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl; Derive1 d1; Derive1* pd1 = &d1; pd1->derive1_fun2(); return 0; }
内存布局:
只需知道: 谁有虚函数表, 谁就往前靠!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。