这里看看,c++的数据是怎样存储的。
开始前先看看
1 | class X {}; |
上述X,Y,Z,A中没有任何一个class内含明显的数据,其间只表示了继承关系。
按照书上的例子
1 | sizeof X = 1 |
译注是
1 | sizeof X = 1 |
先看X,事实上并不是空的,编译器会安插进去一个char。使得这个class的对象在内存中配置独一无二的地址。
至于Y和Z受到三个因素的影响:
1.语言本身所造成的额外负担。其实就是之前一直说的virtual问题。
2.编译器对于特殊情况所提供的优化处理。有些编译器会对这个1bytes作出不同的处理(例如省略)。
3.Alignment的限制,我的理解是字节对齐,在大部分机器上,群聚的结构体大小会受到alignment的限制,使它们能够更有效率地在内存中被存取。
ps:一个虚基类对象只会在派生类中存在一份实体,不管它在class继承体系中出现了多少次。
C++对象模型尽量以空间优化和存取速度优化的考虑来表现非static成员数据,并且保持和C语言struct数据配置的兼容性。它把数据直接存放在每一个类对象之中。对于继承而来的非static成员数据(不管是virtual还是非virtual基类)也是如此。而类的static成员数据是存放在全局中,只有一份实例(甚至即使该class没有任何对象实体,其static成员数据也已存在),但是一个template类的static成员数据的行为稍有不同。
每一个类对象必须有足够的大小以容纳它所有的非static成员数据,它可能比你想象的还大,原因是:
1.由编译器自动加上的额外成员数据,用以支持某些语言特性(主要是各种virtual特性)。
2.因为alignment的需要。
成员数据的绑定
先说说extern:
extern 表明该变量在别的地方已经定义过了,在这里要使用那个变量.。例如变量在xxx.cpp里面定义过了,现在在本头文件中,以extern int a; 的形式声明,那么include”本头文件”的cpp,都可以使用该变量。extern与(const&static)不同,(const和static只在本模块中起作用) extern可以在其他模块中起作用。
如果成员函数没参数,或者不闲的蛋疼把两个类型定义成同个关键字typedef。那么typedef或者成员数据放前放后都一样;如果出现了以上状况,那么必须把内联typedef 放在成员函数的参数之前。这个主要注意的是内联typedef声明需要放在类的起始处,而其他成员函数里面的数据,可以放在类里面的任何地方。
成员数据的布局
非static成员数据在类对象中的排列顺序将和其被声明的顺序一样,任何中间介入的static成员数据都不会被放进对象布局之中。
c++ standard要求,同一个access section(访问级别)中,成员的排列只需要符合“较晚出现的成员在对象中有较高的地址”即可,并不一定要连续排列。原因之一是边界调整,原因之二是插入的关于virtual的东西,如vptr。
c++ standard也允许将多个access sections中的成员数据自由排列,不必在乎声明次序(是指access sections之间的自由排列),但当前各家编译器都是把一个以上的access section连起来,依照声明次序,称为一个连续区块。另外access sections的多寡不会招来额外的负担。
成员数据的存取
static成员数据:在内存中只有一份实体,所以用什么方式,无论是指针也好,对象也罢;本子类的也好,从祖祖父继承来的也罢,都一样其存取路径还是一样的直接。
非static成员数据:直接存放在每个对象中,所以只能由对象来对他们进行存取操作。对象分为explicit class object(自己定义的)和implicit class object(由this指针表达这个implicit class object)(成员函数中访问成员数据时用的)。
从object origin存取”和“从pointer pt存取”的区别:
如果是关于virtual,这里就涉及多态的概念。如果用pt,那么我们不能说pt必然指向哪个class type(因此我们也就不知道编译时期这个成员真正的offset)所以这个存取操作必须延迟至执行期,经由一个额外的间接导引,才能解决。但如果用“.”那么class type就确定无疑了,也就没那么多的事儿了。
继承与成员数据
单一继承不含virtual函数
其数据布局是这样的,子类对象总是把从父类对象弄成一个子对象,然后把这个子对象放在自己的成员数据之前。因此,子类通过对象或者通过对象指针访问父类成员不会存在间接性,父类成员在编译期就可以确定其offset值(父类成员在父类中的offset值和在子类中的offset值是一样的,因为子类对象把整个父类对象给扒拉下来直接按头上了)。因为父类对象在子类对象的首部,这样当父类指针被子类赋值时,父类指针仍然指向子类对象的父类部分(子对象)起始地址。
当然这样的存放方式也是有缺点的(指的是没有虚函数,没有多态的情况)。
加上多态
加上多态即虚函数后,首先是virtual table 和vptr 会创建出来,当然这个创建过程会影响到constructor、copy constructor(为vptr设初值)、destructor(结束后删除vptr)。
至于vptr放在哪里要看具体的编译器,vptr放在尾端,可以兼容c的struct object;vptr放在前端可以避免“从class object起始点开始量起的offset在执行期必须备妥,甚至于class vptr之间的offset也必须备妥”。
多重继承
单一继承提供了一种“自然多态”形式,换句话说单一继承可以把子对象一个一个的叠加在derived object上面,based object和derived object 都是从相同地址开始的。所以derived object转based object比较方便(只要改变对象size就行了)。
虚拟继承
如果一个class 内含一个或多个virtual based class subobjects,那它将会被分成两个部分:不变的部分和共享的部分。不变的部分不论后继如何衍化,总是拥有固定的offset(从object头算起),所以这部分数据可以被直接存取。共享部分表现出来的就是virtual base class(虚基类) subobject(因为虚基类被多个class继承,也会被多个子object 更改,为了保证虚基类的 object 的统一性,就需要单独把虚基类的 object 给拎出来了),这一部分其位置会因为派生对象操作而发生变化,所以只能被间接存取(引入一个新的指针,指向共享的内容)。
当然只是单纯的引入指针指向共享的virtual base class subobject的话会存在两个缺点:1是随着虚基类的增加,指针的个数也会增加;2是虚基类之间如果也存在虚继承的话,间接存取的层数也会增加(子对象->父虚基类对象->祖父虚基类对象···)。
对于第一个问题,有两种解决方案:1是设一个指针指向一张虚基类表,虚基类表中存放虚基类对象访问地址;2是在虚基类表中存放每一个虚基类的offset,而不是地址。
对象成员的访问效率
指向数据成员的指针,是一个有点神秘又颇有用处的语言特性,特别是如果你需要详细调查类成员的底层布局的话。这样的调查可以用于决定vptr是放在class的起始处或者尾端。另外一个用途是可以用来决定类中的access sections的次序。
指向成员数据的指针的效率问题
具体要看编译器怎样优化了
reference
https://github.com/losophy/losophy.github.io/issues/91