浅谈C++对象内存布局
http://yalung929.blog.163.com/blog/static/20389822520123910561654/
最简单的类
先从一个简单的类开始吧。如下,此简单类,非常简单,两个int成员,通过printf很容易了解到它的内存布局,本质就是一个C结构体,两个成员依次排列。
对象:|成员1 | 成员2 |
1: #include <cstdio>
2: class Class0
3: {
4: public:
5: int member1;
6: int member2;
7: };
8: int main()
9: {
10: Class0 c;
11: printf("object addr=0x%lx\nmember1 addr=0x%lx\nmember2 addr=0x%lx\n",
12: &c, &c.member1, &c.member2);
13: return 0;
14: }
# ./a.out
object addr=0x7fffea480d70
member1 addr=0x7fffea480d70 //类成员1
member2 addr=0x7fffea480d74 //类成员2
成员函数
那么我们增加点复杂性,添加一个成员函数。
1: #include <cstdio>
2: class Class1
3: {
4: public:
5: int member1;
6: int member2;
7: void function1() { printf("Class1::function1"); }
8: };
9: int main()
10: {
11: Class1 c;
12: printf("object addr=0x%lx\n", &c);
13: printf("member1 addr=0x%lx\n", &c.member1);
14: printf("member1 addr=0x%lx\n", &c.member2);
15: printf("function1 addr=0x%lx\n", &Class1::function1);
16: return 0;
17: }
./a.out
object addr =0x7fff6805bf90
member1 addr=0x7fff6805bf90
member1 addr=0x7fff6805bf94
function1 addr=0x4006a0//成员函数地址在代码段。----这简直是废话,不在代码段没法玩啊。
对象: | 成员1 | 成员2 |
代码段: |成员函数|
我们看到,对象数据成员的布局并没有变化,但是函数成员的地址跑到十万八千里之外了。为什么?很简单,因为函数是代码,放在了代码段。这也是我们通过Class1::function1来取值,而不是c.function1的原因。从这里可以看出,类的函数成员本质就是一个C全局函数,那么如果函数内访问类的非静态数据成员,如何动态的获取成员地址?编译器是这样做的:
1. 编译器生成function1()的指令时,如果遇到了访问对象的数据成员,比如member1,就从一个约定的位置(比如一个寄存器)获取对象的首地址(其实就是this指针),然后加上偏移(这个是编译时期可以确定的),也就找到了member1对应的内存位置,就可以访问member1了。
2. 编译器生成c.function1()对应的指令时,把c的地址,放到了上述约定的位置。
简单来说,c.function1() 等价于function1(c), c是作为隐含参数传递给function1了。
虚函数
好了搞清楚了成员函数的工作机制,我们再进一步分析,如下例子,有了继承,并且基类成员函数是一个虚函数。派生类重载了它。
1: #include <cstdio>
2: class Base
3: {
4: public:
5: virtual void function() { printf("Base::function1\n"); }
6: };
7: class Derived : public Base
8: {
9: public:
10: void function() { printf("Derived::function1\n"); }
11: };
12: int main()
13: {
14: printf("Base::function addr = 0x%lx\n", &Base::function);
15: printf("Derived::function addr = 0x%lx\n", &Derived::function);
16: Base* pb = new Derived();
17: pb->function();
18: return 0;
19: }
./a.out
Base::function addr = 0x1
Derived::function addr = 0x1
Derived::function1
1: Base* pb = new Derived();
2: pb->function();
派生类指针赋给了基类指针,调用function,但执行还是派生类的function,这就是多态了。那么对于 pb->function(); 这个语句来说,编译器是不能够在编译时期决定调用哪个function的。因为它并不知道pb这个指针是通过派生类转化而来。大家会说,我们上面的语句不是告诉它了吗?这个肯定不行,编译器不能做这个上下文关联,你要是通过函数参数传递过来,赋值的地方离这条语句很远甚至都不在一个源文件里面怎么办?所以这个决定调用哪个function的信息,必须保存在内存里面,运行期间就可以执行正确的函数。那么具体保存在哪里?如何工作的?gcc是这样做的:
1. 申请一段内存,存放虚函数的地址。就是一些书上所说的虚表。本质就是一个数组。
2. 在对象的起始位置,存放虚表首地址,而不是像普通类对象那样存放第一个非静态数据成员。
3. pb->function(); 这条语句执行时,编译器知道function是一个虚函数(我们声明了virtual关键字),那么就会采用虚函数的调用方法,首先根据pb找到虚表的首地址,然后加上一个偏移量,因为是编译器把function这个函数的地址放到虚表内的,所以它知道偏移量。我们通过下面这段代码验证这点:
1: #include <cstdio>
2: class Base
3: {
4: public:
5: virtual void function1() { printf("Base::function1\n"); }
6: virtual void function2() { printf("Base::function2\n"); }
7: };
8: int main()
9: {
10: printf("Base::function addr = 0x%lx\n", &Base::function1);
11: printf("Base::function addr = 0x%lx\n", &Base::function2);
12: Base* pb = new Base;
13: long* vtl = *(long**)pb;
14: printf("0x%lx\n", *(vtl));
15: printf("0x%lx\n", *(vtl + 1));
16: return 0;
17: }
# ./a.out
Base::function addr = 0x1
Base::function addr = 0x9
0x40082a
0x400812
# nm a.out | grep function
000000000040082a W _ZN4Base9function1Ev
0000000000400812 W _ZN4Base9function2Ev
# c++filt _ZN4Base9function1Ev
Base::function1()
# c++filt _ZN4Base9function2Ev
Base::function2()
虚表: |虚函数1的地址|虚函数2的地址|
代码段: |虚函数1|虚函数2|
1. 通过long* vtl = *(long**)pb; 获取pb对象第一个成员的内容,我们拿到了虚表的首地址vtl。
2. printf("0x%lx\n", *(vtl)); 访问虚表的第一个元素,打印的是0x40082a,恰好对应我们通过nm查看到的Base::function1的函数地址000000000040082a 。
3. printf("0x%lx\n", *(vtl + 1)); 访问虚表的第二个元素,打印的是0x400812,恰好对应我们通过nm查看到的Base::function2的函数地址0000000000400812 。
那么&Base::function1是0x1,&Base::function2是0x9,何解?其实怎么解读,完全看编译器心情。。。从我们的实验结果来看,gcc是把它解读成了虚表偏移量+1。编译器也是可以解读为函数的真实地址的。
所谓多态,也就是这么回事儿,其逻辑并不复杂,只是C++"封装"了细节,只给我们展示了它的强大形象,让我们觉得多态好神奇啊,其实丫的,本质就是函数指针,就是地址而已,因为地址才是CPU理解的东西。懂得这点,就知道内核里到处都是多态,同样是一个read操作,read不同的文件,执行不同的函数。。。内核就是在文件对象(C结构体)里,保存了函数指针,不同的文件系统注册不同的函数指针。内核是各种编程技术、思想的集大成者,OO思想随处可见。
单继承
1: class Base
2: {
3: public:
4: int b;
5: };
6: class Derived : public Base
7: {
8: public:
9: int d;
10: };
11: int main()
12: {
13: Derived d;
14: d.b = 2012;
15: Base* b = &d;
16: b->b = 2012;
17: }
1: #include <cstdio>
2: class Base
3: {
4: public:
5: int b;
6: };
7: class Derived : public Base
8: {
9: public:
10: int d;
11: };
12: int main()
13: {
14: Derived d;
15: printf("Derived = 0x%lx\n", &d);
16: printf("Derived.b = 0x%lx\n", &d.b);
17: printf("Derived.d = 0x%lx\n", &d.d);
18: return 0;
19: }
# ./a.out
Derived = 0x7fffece35bf0
Derived.b = 0x7fffece35bf0
Derived.d = 0x7fffece35bf4
对象: | 基类的成员|派生类的成员|
多继承
单继承的内存布局,是基类成员在前,派生在后,但是多继承呢?丫的有两个基类,谁前谁后?谁前谁后不重要,关键的是根据上面单继承分析,如果基类成员在派生类对象的位置不是从头开始,派生类对像指针转化为基类指针之后,就不能正确访问基类成员了。而多继承,必然至少有一个基类不是从头开始的。那么怎么办?还能怎么办,凉拌!当你把一个派生类对象地址赋值给一个基类指针,如果这个基类在派生类中的位置,不是从头开始的,编译器偷偷的把它改变,加上基类在派生类中的位置偏移量!我们来验证下:
1: #include <cstdio>
2: class Base1
3: {
4: public:
5: int b1;
6: };
7: class Base2
8: {
9: public:
10: int b2;
11: };
12: class Derived : public Base1, public Base2
13: {
14: public:
15: int d;
16: };
17: int main()
18: {
19: Derived d;
20: printf("Derived = 0x%lx\n", &d);
21: printf("Derived.b1 = 0x%lx\n", &d.b1);
22: printf("Derived.b2 = 0x%lx\n", &d.b2);
23: printf("Derived.d = 0x%lx\n", &d.d);
24: Base2* b2p = &d;
25: printf("Base2 pointer = 0x%lx\n", b2p);
26: return 0;
27: }
# ./a.out
Derived = 0x7fffedfe10e0
Derived.b1 = 0x7fffedfe10e0
Derived.b2 = 0x7fffedfe10e4
Derived.d = 0x7fffedfe10e8
Base2 pointer = 0x7fffedfe10e4
可以看到,摆放的顺序是Base1,Base2,Derived:
对象:| 基类1的成员 | 基类2的成员 | 派生类的成员
而当我们把Derived的地址0x7fffedfe10e0赋给Base2时,变成了0x7fffedfe10e4,即Base2成员的起始位置,这样我们的b2p->b2; 可以正确的工作。是不是很神奇?=号都是不可信的!
多继承+虚函数
对象:| 虚表1的地址 | 基类1的成员 | 虚表2的地址 | 基类2的成员 | 派生类的成员
其中虚表1中存放是派生类重载的虚函数地址,无论来自于基类1还是基类2。虚表2只存放基类2的重载函数地址(实际上GCC帮你生成了一个中间函数,中间函数再去调用实际的函数)。
1: #include <cstdio>
2: class Base1
3: {
4: public:
5: int b1;
6: virtual void function1() { printf("Base1::function1\n"); }
7: };
8: class Base2
9: {
10: public:
11: int b2;
12: virtual void function2() { printf("Base2::function2\n"); }
13: };
14: class Derived : public Base1, public Base2
15: {
16: public:
17: int d;
18: void function1() { printf("Derived::function1\n"); }
19: void function2() { printf("Derived::function2\n"); }
20: };
21: int main()
22: {
23: Derived d;
24: printf("Derived = 0x%lx\n", &d);
25: printf("Derived.b1 = 0x%lx\n", &d.b1);
26: printf("Derived.b2 = 0x%lx\n", &d.b2);
27: printf("Derived.d = 0x%lx\n", &d.d);
28: Base2* b2p = &d;
29: printf("Base2 pointer = 0x%lx\n", b2p);
30: long* vtl = *(long**)b2p;
31: printf("0x%lx\n", *(vtl));
32: printf("0x%lx\n", *(vtl + 1));
33: vtl = *(long**)&d;
34: printf("0x%lx\n", *(vtl));
35: printf("0x%lx\n", *(vtl + 1));
36: return 0;
37: }
# ./a.out
Derived = 0x7fffa74ae400
Derived.b1 = 0x7fffa74ae408//b1没有放在最开始,因为第一个是虚表地址
Derived.b2 = 0x7fffa74ae418//b2没有放在b1后面,因为前边还有一个虚表地址
Derived.d = 0x7fffa74ae41c
Base2 pointer = 0x7fffa74ae410//base2在派生类中的起始位置,
0x4008aa//虚表2中存放的函数地址,gcc生成的中间函数
0x0//虚表2中存放的函数地址
0x4008c8//虚表1中存放的函数地址,function1
0x4008b0//虚表1中存放的函数地址,function2
# nm a.out |grep function
00000000004008e0 W _ZN5Base19function1Ev
00000000004008f8 W _ZN5Base29function2Ev
00000000004008c8 W _ZN7Derived9function1Ev
00000000004008b0 W _ZN7Derived9function2Ev
00000000004008aa W _ZThn16_N7Derived9function2Ev
# c++filt _ZN7Derived9function1Ev _ZN7Derived9function2Ev _ZThn16_N7Derived9function2Ev
Derived::function1()
Derived::function2()
non-virtual thunk to Derived::function2()
# objdump -d a.out | sed -n '/_ZThn16_N7Derived9function2Ev/,/00000/p'
00000000004008aa <_ZThn16_N7Derived9function2Ev>:
4008aa: 48 83 c7 f0 add $0xfffffffffffffff0,%rdi
4008ae: eb 00 jmp 4008b0 <_ZN7Derived9function2Ev>//中间函数跳转到了function2
00000000004008b0 <_ZN7Derived9function2Ev>:
了解C++内存布局的意义
浅谈C++对象内存布局相关推荐
- java对象头_浅谈java对象结构 对象头 Markword
概述 对象实例由对象头.实例数据组成,其中对象头包括markword和类型指针,如果是数组,还包括数组长度; | 类型 | 32位JVM | 64位JVM| | ------ ---- | ----- ...
- java对象头markword_浅谈java对象结构 对象头 Markword
概述 对象实例由对象头.实例数据组成,其中对象头包括markword和类型指针,如果是数组,还包括数组长度; | 类型 | 32位JVM | 64位JVM| | ------ ---- | ----- ...
- 浅谈C语言内存(栈)
浅谈C语言内存 文章目录 浅谈C语言内存 内存分配 栈 斐波纳契数列 内存分配 在C语言中内存分别分为栈区(stack).堆区(heap).未初始化全局数据区.已初始化全局数据区.静态常量区(stat ...
- 【C++】C++对象模型:对象内存布局详解(C#实例)
C++对象模型:对象内存布局详解 0.前言 C++对象的内存布局.虚表指针.虚基类指针解的探讨,参考. 1.何为C++对象模型? 引用<深度探索C++对象模型>这本书中的话: 有两个概念可 ...
- C++对象内存布局--①测试虚函数表属于类
C++对象内存布局--①测试虚函数表属于类 测试1:同一个类的多个对象共享同一张虚函数表. //虚函数表.cpp //2010年8月18日 //测试虚函数表,说明虚函数表属于类所有.同一个类的多个 ...
- C++对象内存布局--⑤GCC编译器--单个虚拟继承
C++对象内存布局--⑤GCC编译器--单个虚拟继承 测试GNU的GCC编译器在处理虚拟继承上跟VS不同的地方.派生类的虚函数表跟虚基类表合并. //GCC编译器--单个虚拟继承.cpp //2010 ...
- C++ 对象内存布局 (4)
注意:关于内存对齐(memory alignment),请看关于内存对齐问题,后面将会用到. 下面我们进行在普通继承(即非虚继承)时,派生类的指针转换到基类指针的情形研究.假定各类之间的关系如下图: ...
- C++对象内存布局--③测试多继承中派生类的虚函数在哪一张虚函数表中
C++对象内存布局--③测试多继承中派生类的虚函数在哪一张虚函数表中 测试2:证明派生类的虚函数的地址跟第一基类的虚函数地址保存在同一张虚函数表中. 派生类有多少个拥有虚函数的基类,派生类对象就有多少 ...
- C++对象内存布局--④VS编译器--单个虚拟继承
C++对象内存布局--④VS编译器--单个虚拟继承 在VS2005编译器下,证明单个虚拟继承的内存布局:无论有无虚函数,必然含有虚基类表指针.虚基类表中的内容为本类实例的偏移和基类实例的相对偏移值. ...
最新文章
- Unity Android 2021:用C#打造3D ZigZag赛车游戏
- 传递给beanshell的参数_jmeter基础之断言及参数的关联性
- 试题 历届试题 包子凑数(dp)
- Django从理论到实战(part31)--Django数据库查询操作
- 【今日CV 计算机视觉论文速览 第97期】Tue, 9 Apr 2019
- JAVA读取属性文件的几种方法
- Mininet的内部实现原理简介
- Mac改变默认python并删除旧版本python
- python中将字符变为大写_python3.4.3将汉字转换为大写拼音首字母
- Lucas-Kanade稀疏光流法
- matlab eigs 求特征值函数
- 批量导入手机通讯录_手机QQ批量导入电话号码
- nginx 服务器大文件上传时500错误
- 别总用git merge了,Git高级操作之选择合并,git将一个分支的提交合并到另一个分支,git cherry-pick
- 百度网盘不限速下载网页版
- y4m文件转换成yuv
- Node.js知识点整理之----Buffer类
- Windows 技术篇-重装电脑系统后搜不到蓝牙设备问题,电脑连接蓝牙不成功原因及解决办法
- Java与模式学习笔记 —— 桥梁(Bridge)模式
- 云数据中心与传统数据中心
热门文章
- Android下常见的内存泄露
- 七十一、Python | Leetcode字符串系列(上篇)
- 期末考试前的预习,科目:化工设备与反应器(4)
- 2021年最值得期待的数据智能赛事之一,有何解题妙招?
- 冲击SuperGLUE:基于多任务与对抗训练的Finetune方案解析
- ACL 2019开源论文 | 基于Attention的知识图谱关系预测
- 随机迷宫 c语言思路,[原创]递归随机迷宫生成算法详解
- 【Linux部署】第一次部署ZooKeeper启动两次报错问题解决 Could not find or load main class+Failed to bind to /0.0.0.0:8080
- structs2文件下载
- php软件安装及调试_PHP调试利器XDebug的安装与使用