谈谈对象大小——从字节对齐到对象模型
来源:互联网 发布:pip安装tensorflow 编辑:程序博客网 时间:2024/05/23 11:33
一. 前言
这篇文章主要介绍以下从c的结构体变量到c++的类对象中编译器对内存分配做的事情。总而言之,言而总之,这篇文章就是讲述对于一个变量(对象)它的内存布局是怎么样子的。为了方便描述,我们按照以下几个层次来讲述:
1.c中struct的字节对齐
2.从struct到class的过渡
3.单继承对对象内存模型的影响
4.虚函数对对象内存布局的影响
5.多继承对对象内存模型的影响
6.虚基类对对象内存布局的影响
二. c中struct的字节对齐
在谈字节对齐之前,我们先思考一下下面这个结构体大小?struct node{};
没错,就是一个空结构体,按道理来说,如果没有数据应该内存大小为0,然而事实上并不是这样。它有一个隐藏的1B大小,那是呗编译器安插进去的一个char,这使得这个struct对应的对象在内存中有独一无二的地址。下面就来了解字节对齐:
不同的系统中这些策略都是有区别的,例如:
Linux系统: 对于2字节数据类型变量地址必须是2的倍数,其余数据类型变量地址必须是4的倍数。
Windows系统: 对于大小为k字节的数据类型变量必须是k的倍数。
而上述中提到的地址必须是x的倍数,换句话说也就是按照k对齐。我们可以更改对齐规则么?可以!不过一般我们不这么做,除非有特殊需求,比如thunk技术。
先看下面这段代码:
struct node{ char mem1; //sizeof(char) = 1 int mem2; //sizeof(int) = 4 char mem3;};struct node a;
sizeof(a)的值应该是多少呢? 5? 12!如果从未了解过字节对齐的朋友可能对这个答案感到很不可思议,因为这比本应该占的内存(5字节)的两倍还多。
但是事实上,按照上述所说的字节对齐规则来说,得到12这个答案其实并不意外。我们先来看来对齐规则:
1.对于基本数据类型变量按照其字节数大小k对齐。
2.对于结构体类型变量按照其成员中最大对齐量对齐。 ———保证结构体第一个变量满足对齐规则
3.对于结构体类型变量其大小要为其成员最大对齐量的整数倍 ———保证结构体数组中每个元素满足对齐规则。
所以按照上述规则,a的内存布局应该是这样的:
假如我们最后不进行3个字节的间隙插入,那么很显然,即使arr[0]元素的每个成员满足对齐规则,然而数组要保证元素地址连续,那么必定导致arr[1]的第一个成员一定不满足对齐规则。
如何修改字节对齐的规则呢?请看下面这段代码:
#pragma pack(push,1) //使结构体按1字节方式对齐,将原来的对齐规则压栈struct node{ char mem1; int mem2;};#pragma pack(pop) //恢复原来的对齐规则//sizeof(node) = 5
三. 从struct到class的过渡
首先,需要注意的一点是,在c++中,我们可以使用struct替换class,因为本质上这两个关键字除了default access section不一样以外,其他都是都无差别。所以这里说,从struct到class的过渡的实质,只是在这里,我把struct认为一个数据集合体,没有private data,也没有member function等等,虽然struct可以代替class实现private data、member function、继承等等。
后面所谈到的继承派生等等都只是指class。
四.单继承对对象内存模型的影响
首先先来看下这段代码://带透明通道颜色信息的顶点结构class ColorAVertex{public: int _x,_y,_z; unsigned char _r,_g,_b,_a; //颜色rgba三通道值};
嗯,经过第二段和第三段的介绍,我们应该很容易推出 sizeof(ColorAVertex) = 16。不过,经过分析之后,决定将其分裂成三层结构:
class Vertex{ int _x,_y,_z;};class ColorVertex:public Vertex{public: unsigned char _r,_g,_b;};class ColorAVertex:public ColorVertex{public: unsigned char _a;};
从设计的角度来看,可能这个结构相对更加合理,但是从实现的角度来说,我们发现一个事情:sizeof(ColorAVertex) = 20嗯,要对这个问题追根溯源,我们就需要了解单继承对对象模型产生的影响。我们可以观察上述两种情况对应产生的对象模型:
VC++2015 : sizeof(ColorAVertex) = 20
GUN GCC : sizeof(ColorAVertex) = 16
除此之外,还有一些member类型需要注意:static member 和 member function( 不包括 virtual function ) 。这些类型的成员是不会引起对象内存布局的变化的。这些数据成员并不是存在每个对象的内存模型之中。
struct A{};struct B:A{};
在上面我们了解到如果member data为空,那么编译器会安插一个char,那么在这段代码中B的大小是2还是1呢?事实上,当B继承A之后,因为A已经安插了一个char了,B就不为空了,所以说sizeof(B) = 1。
五.虚函数对对象内存布局的影响
在上一段末尾中提到,member function不影响对象内存布局,但是把virtual function排除开外了。因为virtual function作为c++运行时多态的核心,而为了支持这一特性,在对象内部也添加了相应的一些信息。例如:下面这段代码:
class A{public: int a; virtual void say(){ printf("A::say()\n"); }};class B:public A{public: int b; void say(){ printf("B::say()\n"); }};int main(){ A* p = ....; p->say(); return 0;}
在以上继承体系中,编译阶段并不能知道A* p真正的类型,而编译阶段就需要进行函数绑定,而真正的函数只有在运行阶段才知道。所以,对象内部必须要存储相应的动态类型信息,这样才能实现动态绑定。而这样的信息就是vptr,也就是虚表指针,指向一个虚函数表。而一个类的虚函数表只有一个,也就是一个类的不同对象vptr值是相同的。所以说,拥有虚函数的对象,会多4个字节用于存放vptr虚表指针,而存放位置由编译器决定,一般来说不是对象首部,就是尾部。不过大多数编译器都是放在对象首部,也就是说&vptr=&obj。
在单继承体系中,如果一个类的父类中已经有vptr了,那么在子类的虚函数只有可能是新添或者重写。而这些操作都是先复制父类的虚表,然后在其上修改。注意:在单继承体系中,对象内存模型中vptr只有一个,也就是后面的操作都是在vptr对应的虚表上覆盖或者新添。
六.多继承对对象内存模型的影响
在多继承体系中,对象的很多东西就会变得十分复杂,这里我们撇开这些复杂的特殊性不谈。只是简单谈谈在对象内存模型中的影响。首先看下面这段代码:
class Top{public: int _top;};class Left: public Top{public: int _left;};class Right: public Top{public: int _right;};class Bottom: public Left,public Right{public: int _bottom;};
七.虚基类对对象内存布局的影响
嗯,如果要花篇幅去深究虚基类里面的种种细节,可能再写几千字也不为过。不过在这里我只是简单的介绍,如果类的继承体系中出现了虚基类会对内存布局产生的影响。
虚基类的内存布局难点是在于,既要保证多个继承源的情况下,虚基类只能有一个。并且子类的内存布局中不能改变父类的内存布局模式。这里就不探讨解决方案的相关的。直接给出常规编译器的解决方案之一吧(VC++2015)。如果一个类B虚拟继承自类A。相当于类B组合了一个A对象,并且含有一个指针指向A对象。而基于B对象读取A类的相关data或者function都是通过这个指针。所以说,在子类中,A对象只有一个,其他父类中如果虚拟继承了类A,只不过是把这个指针指向了这个A对象,而这些操作都是子类的构造函数完成的。
观察如下代码:
class Top {public: int _top;};class Left : public virtual Top {public: int _left;};class Right : public virtual Top {public: int _right;};class Bottom : public Left, public Right {public: int _bottom;};
对于该继承体系,Bottom的对象内存布局应该如下: class Top {public: int _top; virtual int top(){ return _top; }};class Left : public virtual Top {public: int _left; virtual int left(){ return _left; }};class Right : public virtual Top {public: int _right; virtual int right(){ return _right; }};class Bottom : public Left, public Right {public: int _bottom; virtual int bottom(){ return _bottom; }};
对于该继承体系,Bottom的对象内存布局应该如下: 对于不同的编译器,对于虚拟继承的内存布局处理方式可能有区别,这里只是按照VC++2015编译器的解决方式来进行说明。
当然,本文章对内存模型的布局原因等没有过多着墨介绍。一是本人能力有限,怕言辞有误。二是的确本身c++标准对内存布局并没有很严格的规定,在各个编译器中有的细节设计也有不同之处。所以这里只是作为简单了解。如果想更进一步了解可以查看<Inside the C++ Object Model>一书。
2 0
- 谈谈对象大小——从字节对齐到对象模型
- 内存字节对齐,bitset对象
- Java对象的字节大小
- 面向对象化——从table到对象
- 轻松加精确完成水晶报表对象的对齐、移动及大小调整—
- 求解字节大小,字节对齐
- 字节长度,字节对齐以及类,对象的长度
- 字节长度,字节对齐以及类,对象的长度
- 字节长度,字节对齐以及类,对象的长度
- 字节长度,字节对齐以及类,对象的长度
- 从项目始末谈面向对象——领域模型
- JAVA对象所占字节大小计算方法
- c++对象内存模型【内存对齐】
- 嵌入式—字节对齐
- 从对象到方面
- 从过程到对象
- JavaScript面向对象(1)——谈谈对象
- JavaScript创建对象—从es5到es6
- 1001.A+B Format
- 文章标题 coderforces 761B : Dasha and friends(KMP)
- javascript笔记--(第九章)日期与时间
- 配置JDBC连接池
- SPOJ DWARFLOG 线段树
- 谈谈对象大小——从字节对齐到对象模型
- SRM 688 ParenthesesDiv2Hard
- Linux文件权限及用户管理
- backtracking: 22. Generate Parentheses
- linux命令中的特殊字符
- Java——包装类
- PAT甲级1013
- 【C++】引用调用
- 剑指offer之字符串字典序全排列