转载自:https://blog.twofei.com/496/要想知道c++类对象的内存布局, 可以有多种方式,比如:1)输出成员变量的偏移, 通过offsetof宏来得到2)通过调试器查看, 比如常用的VS1.只有数据成员的对象class Base1{publ...
转载自:https://blog.twofei.com/496/
要想知道c++类对象的内存布局, 可以有多种方式,比如:
1)输出成员变量的偏移, 通过offsetof宏来得到
2)通过调试器查看, 比如常用的VS
1.只有数据成员的对象
class Base1 { public: int base1_1; int base1_2; };
对象大小及偏移:
可知对象布局:
可以看到, 成员变量是按照定义的顺序来保存的, 最先声明的在最上边, 然后依次保存,类对象的大小就是所有成员变量大小之和.
2.没有虚函数的对象
class Base1 { public: int base1_1; int base1_2; void foo(){} };
结果如下:
和前面的结果是一样的,因为成员函数可以被看作是类作用域的全局函数, 不在对象分配的空间里, 只有含有虚函数的类对象里才会有一个指针, 指向虚函数表。成员函数的地址,编译期就已确定,并静态绑定或动态的绑定在对应的对象上。对象调用成员函数时,编译器可以确定这些函数的地址,并通过传入this指针和其他参数,完成函数的调用。
3.拥有仅一个虚函数的类对象
class Base1 { public: int base1_1; int base1_2; virtual void base1_fun1() {} }; Base1 b1;
结果如下:
咦? 多了4个字节? 且 base1_1 和 base1_2 的偏移都各自向后多了4个字节! 说明类对象的最前面被多加了4个字节的"东东". 我们通过VS2013来瞧瞧类Base1的变量b1的内存布局情况:(由于没有写构造函数, 所以变量的数据没有初始化, Debug模式下, 未初始化的变量值为0xCCCCCCCC, 即:-858983460)
base1_1前面多了一个变量 __vfptr(这就是虚函数表vtable指针,简称虚表指针), 其类型为void**, 这说明它是一个指向void*的指针,再看高亮部分,说明它是一个指向指针数组的指针. 再看看[0]元素, 其类型为void*, 其值为 ConsoleApplication2.exe!Base1::base1_fun1(void), 这是什么意思呢? 如果对WinDbg比较熟悉, 那么应该知道这是一种惯用表示手法, 它就是指 Base1::base1_fun1() 函数的地址.
所以,该类的对象大小为12个字节, 大小及偏移信息如下:
现在的类对象布局如下:
注意到__vfptr前面的const修饰. 它修饰的是那个虚函数表, 而不是__vfptr.
4.拥有多个虚函数的类对象
class Base1 { public: int base1_1; int base1_2; virtual void base1_fun1() {} virtual void base1_fun2() {} }; Base1 b1;
大小以及偏移信息如下:
多了一个虚函数, 类对象大小却依然是12个字节! 再来看看VS形象的表现:
现在__vfptr所指向的指针数组中出现了第2个元素, 其值为Base1类的第2个虚函数base1_fun2()的函数地址. 通过上面两张图表, 我们可以得到如下结论:
- 更加肯定前面我们所描述的: __vfptr只是一个指针, 它指向一个函数指针数组(即: 虚函数表)
- 增加一个虚函数, 只是简单地向该类对应的虚函数表中增加一项而已, 并不会影响到类对象的大小以及布局情况
不妨, 我们再定义一个类的变量b2, 现在再来看看__vfptr的指向:
通过Watch 1窗口我们看到:
- b1和b2是类的两个变量, 理所当然, 它们的地址是不同的(见 &b1 和 &b2)
- 虽然b1和b2是类的两个变量, 但是: 它们的__vfptr的指向却是同一个虚函数表
由此我们可以总结出:
同一个类的不同对象共用同一份虚函数表, 它们都通过一个所谓的虚函数表指针__vfptr(定义为void**类型)指向该虚函数表.
是时候该展示一下类对象的内存布局情况了:
结论:
- 虚函数表是编译器在编译时期为我们创建好的, 只存在一份,保存在.rodata只读数据段
- 定义类对象时, 编译器自动将类对象的__vfptr指向这个虚函数表
5.单继承且本身不存在虚函数的继承类的内存布局
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; }; Derive1 d1;
来看看现在的内存布局:
基类在上边, 继承类的成员在下边依次定义! 展开来看看:
现在类的布局情况应该是下面这样:
6.覆盖的基类虚函数的单继承类的内存布局
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() {} };
Derivel d1;
现在的布局:
注意高亮的那一行: 原本是Base1::base1_fun1(), 但由于继承类重写了基类Base1的此方法, 所以现在变成了Derive1::base1_fun1()!
7.定义了基类没有的虚函数的单继承的类对象布局
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 d1;
现在的布局:
表面上看来几乎和第5种情况完全一样, 现在继承类明明定义了自身的虚函数, 但不见了, 那么, 来看看类对象的大小, 以及成员偏移情况吧:
也没有变化, 现在我们只能从汇编入手了, 来看看调用derive1_fun1()时的代码:
Derive1 d1; Derive1* pd1 = &d1; pd1->derive1_fun1();
本文标题为:c++类对象的内存分布
基础教程推荐
- C语言 structural body结构体详解用法 2022-12-06
- C++详细实现完整图书管理功能 2023-04-04
- C利用语言实现数据结构之队列 2022-11-22
- C++使用easyX库实现三星环绕效果流程详解 2023-06-26
- 一文带你了解C++中的字符替换方法 2023-07-20
- 详解c# Emit技术 2023-03-25
- C++中的atoi 函数简介 2023-01-05
- 如何C++使用模板特化功能 2023-03-05
- C/C++编程中const的使用详解 2023-03-26
- C语言基础全局变量与局部变量教程详解 2022-12-31