深度探索c++对象模型之三 — Data语意学
空类的大小也不会为0,它有一个隐晦的1byte,是被编译器安插进去的一个char,这使得class的两个objects得以在内存中配置独一无二的地址。
class X{};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y, public Z {};
上面的类的层次结构就是我们之前提到过的菱形继承。
刚才我们说过因为编译器做的优化处理,都会在空类中插入1bye,因此这里的class X
的大小是1byte,那么class Y
和class Z
的大小是多少呢?我们可以得到影响它们两个的大小的因素 1. 语言本身带来的负担,如为了支持virtual base class,我们需要增加指针指向virtual base class subobject或者是存放其偏移量的表格 2. 编译器做的优化处理,正如之前说的空类插入1byte 3. Alignment限制,为了存取效率进行的对齐
那么我们就可以得到class Y
和class Z
的大小和布局结构,如下图所示:
从上图中可以看到class Y
和class Z
的大小一样。都是8bytes。另外还有一些新近的编译器直接就是把指向virtual base class的指针作为最开头的一部分,那么这时候就不需要因为empty class而插入1byte,那么每个class的大小就会缩减至4byte(比如 visual C++)。如下图所示:
那么class A
的大小是多少呢?
因为Y和Z都是“虚拟派生”自class X
,而一个virtual base class subobject只会在derived class中存在一份实体。因此class A的对象布局如下图所示:
这里可以参考下文虚拟继承中Vertex3D的对象布局图
Data Member的布局
Nonstatic data member在class object中的排列顺序和其被声明的顺序一样。
c++标准要求,在同一个access section中,memeber的排序只需符合“较晚出现的members在class object中有较高的地址”这一条件,各个members并不一定得连续排列。
下面这个template function,接受两个data members,然后判断谁先出现在class obejct中,
template <class class_type,
class data_type1,
class data_type2>
char *access_order(data_type1 class_type::*mem1,
data_type2 class_type::*mem2)
{
assert(mem1 != mem2);
return mem1<mem2 ? "member 1 occurs first":"member 2 occurs first";
}
Data Member的存取
我们考虑存取成员变量的两种方式,如下:
Point3d origin, *pt = &origin;
origin.x = 0.0;
pt->x = 0.0
第一种是直接通过对象存取,第二种是通过指针存取,那么这两种方式效率上有什么区别吗?
对于Static data member来说,因为data member是针对class而存在的,独立于各个class object,static data member存放在程序的data segment之中,那么上面两种方式在内部都会被转化为对该唯一的extern实体的直接参考操作。
//origin.chunkSize = 250;
//pt->chunkSize = 250;
Point3d::chunkSize = 250;
存取static data member并不需要通过class object,上面的两种方式是等效的。
另外还要说明的是 如果取一个static data member的地址的话,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针,例如:
&Point3d::chunkSize
上面的这行代码会得到如下的内存地址: const int *
那如果有两个类中都声明了相同的名字的静态成员变量,如果都把这个static data member放在程序的data segment中是不是会出现名字冲突呢?这里编译器会在暗中为每一个static data member进行编码(name-mangling),所以不会出现所谓的名称冲突。
而对于NonStatic data members来说,这一类的成员变量就依附于具体的class object存在的,都是在class object之中。在class member function中处理nonstatic data member的时候,都会有一个隐式的class object传入(就是传说中的this指针),例如下面的代码:
Point3d Point3d::translate(const Point3d &pt) {
x += pt.x;
...
}
上面的代码会转变成如下形式
Point3d Point3d::translate(Point3d::const this, const Point3d &pt) {
this->x += pt.x;
...
}
如果要对一个nonstatic data member进行存取操作的话,编译器需要把class object的起始地址加上data member的偏移量,如:
origin._y = 0.0;
===> &origin + (&Point3d::_y-1);
注意上的一个减1的操作,编译系统为了区分“一个指向data member的指针,用以指出class的第一个member” 和 “一个指向data member的指针,没有指出任何member”两种情况,于是对于指向data member的指针,其offset的值都会加1
每一个data member的偏移量在编译时期就可以知道,因此存取nonstatic data member的效率也没什么降低。
但是对于虚拟继承。虚拟继承经由base class subobject存取class member会增加一层间接性。所以,如果存取的data member是一个virtual base class的member,存取速率会比较慢一点。
对于上面说的两种存取方式,当Point3d是一个derived class,并且其继承结构中有一个virtual base class,存取的又是从virtual base class中继承而来的member,那么通过指针的方式会使得效率下降一点。因为对于指针来说我们不能够断定指针指向的是哪一种class type,这个只有等到执行期才能够决定,需要经过一个间接导引才能解决,而直接通过class object的方式的因为member的offset在编译器就决定了,因此不存在这个问题。于是相对来说指针存取的方式就会慢一点。
“继承”与Data Member
首先C++语言保证“出现在derived class中的base class subobject有其完整原样性”,也就是说对于每一个base class subobject都会有自己的padding,而derived的data member不会占用base object的padding的部分,否则的话在进行memberwise复制的时候derived class memeber的内容会被覆盖。
只要继承不要多态
这种情况不会增加空间和时间上的额外负担,base class 和derived class的object都是从相同地址开始,其差异只在于derived object比较大,把一个derived object指定给base class指针或者饮用的时候,不需要编译器去修改地址,很自然的发生,提供了最佳的效率.
还有一点,如果base class没有virtual function而derived class有virtual function的时候,那么把deried class object指定给base class的时候也是需要修改地址的,因为编译器产生的vptr是放在第一个slot里面的。此时base class object 和 derived class object的地址就不是相同的。
加上多态
为了支持多态那么在空间和时间上就需要额外的负担:
1. 在编译时期需要产生一个virtual table,用来指向virtual functions的地址
2. 为每一个class object插入一个vptr,用来指向virtual table
3. 对于constructor和deconstrutor,编译器需要插入必要的代码以便在恰当的时机修改vptr的值
多重继承
对于多重继承,如果把derived class object指定给“最左端的base class的指针”,那么情况和单一继承一样,但是如果“指定给第二个或者后面的base class指针”,那么就需要将地址修改:加上(减去)介于中间的base class objects的大小。
虚拟继承
class如果含有一个或多个virtual base class subobjects那么就会被分割成一个不变局部和一个共享局部,不变局部的内容在编译时期offset就确定所以可以直接存取。至于共享局部,所表现出现的就是base class subobject,其位置会因为每次的派生操作而有变化。
下面我们就介绍一种解决方法,即在virtual function table中放置virtual base class的offset(而不是地址)。那么virtual function table的索引就可以分为正索引和负索引,正索引代表是virtual function,而负索引即为virtual base class subobject的offset。
Point3d pv3d;
Point2d *pd = pv3d;
那么就会被转化为
Point2d *p2d = pv3d?pv3d+pv3d->_vptr_Point3d[-1]:0;
上面-1索引中保存的就是virtual base class object的offset,如下图所示:
指向Data member的指针
上面我们有提到对于指向class data member的指针的offset都需要加1的,这是历史原因,我们考虑如下case:
float Point3d::*p1 = 0;
flota Point3d::*p2 = &Point3d::x;
上面第二行代码&Point3d::x
正常是返回x的offset也就是为0,但是p1没有指向任何data member,他甚至没有指向任何class object,而p2却是要指向x的,为了要区分“一个没有指向任何data member的指针” 和 “一个指向第一个data member的指针”,我们就把真正的member offset都加上1。因此我们和编译器都要记住,在真正使用该值的时候一定要先减掉1.