第三章:Data语意学
这一章主要讲了类中的数据在内存中是如何分配的,包括(多重)继承和多态。
让我们首先从一段代码开始:
class X{}; class Y :virtual public X{}; class Z :virtual public X{}; class A :public Y, public Z{}; std::cout << sizeof(X) << std::endl; std::cout << sizeof(Y) << std::endl; std::cout << sizeof(Z) << std::endl; std::cout << sizeof(A) << std::endl;
在VS2013上输出的结果为1,4,4,8。为什么会这样呢?这就涉及到编译器针对C++语法而采取的对象模型。X虽然是一个空类,但为了使它的对象具有地址,编译器实际上是会为它的对象分配1个字节大小的空间,因此它的大小为1。Y和Z是对X的虚拟继承,编译器会为它们各分配一个指针,指向X的对象,因此它们的大小为4.A同理,它是Y和Z的多重继承,因此编译器会为它分配两个指针,所以它的大小为8.内存分布如图所示:
书中还提到了一种模型,其输出的结果为1,8,8,12。这种模型的思路是给每一个空类分配1 byte的空间,再考虑到4 byte的alignment,因此可以推导出书中的结果,在这里不再赘述。
关于类中Member的布局,C++ Standard是这样规定的:static data members不会被放到对象的布局之中;在同一个access section(public、private、protected等区段),member的排列是按照声明顺序分布的,但不一定是连续排列(因为存在alignment);同时,对于不同的access section,它们的data members自由排列,不必在乎声明顺序(也就是说access section之内是按照声明顺序排列,而access section之间自由排列)。
之后,书中讨论了单一继承、多态、多重继承、虚拟继承下类中内存分布的表现,这也是本章的重中之重。
这是一个很简单的程序:
Point3d origin; origin.x=0.0;
执行这段程序所需要的时间和空间代价,随x的性质而不同。让我们分情况讨论:
1.x是static data members
类的每个static成员都只有一个实例,存放在程序的data segment之中,和对象无关;因此对于这种情况,对x的存取并不会招致任何空间和时间上的额外负担。
2.x是 nonstatic data members
访问类的nonstatic data members时,实际上编译器做了如下工作:
//源代码 origin.x=0.0; //编译后的代码 *(&origin+(&Point3d::x))=0.0;
也就是说,为了得到x的地址,编译器需要将origin的起始地址加上x的地址偏移量。(实际上,起码在VS里用&Point3d::x来表示偏移量是有问题的,但思想可以先这样理解)
3.x是基类的变量,没有多态与多重继承
在有继承的情况下,可能会导致空间上的浪费。我们来看这样一个例子:
这个类中存有一个int和三个char,如果我们把这些变量都放到一个类中声明,那么算上alignment,它的对象大小为8字节。
假设我们要继承:
那么Concrete3的对象的大小将达到16字节,比原先的设计多了100%!
这是因为alignment导致的,因为C++的对象模型中,在一个继承而来的类的内存分布里,各个基类需要分别遵循alignment,从而导致了空间的浪费。具体地对象布局可见下图:
4.加上多态
在这种情况下,无论是时间还是空间上,访问类的成员都会带来一定额外的负担,主要体现在以下几个方面:
1.virtual table,用来存放它所声明的每一个virtual functions的地址。
2.每一个对象中会有一个vptr,提供执行期的链接。
3.编译器会重写constructor和destructor,使其能够创建和删除vptr。
5.多重继承
在多重继承的条件下,对于指针之间的赋值需要运行时计算。举个例子,以下的继承结构:
我们声明几个对象和指针并赋值:
Vertex3d v3d; Vertex *pv; Point2d *p2d; Point3d *p3d; pv=&v3d; p2d=&v3d;' p3d-&v3d;
对于p3d和p2d的赋值,只需要直接将v3d的地址赋过去就好。但对于pv的赋值,编译器需要计算一个Vertex在Vertex3d中的偏移量,从这个偏移量起始来得到pv的地址。因为类之间的内存分布如下所示:
Vertex3d中Vertex部分的起始地址并不是Vertex3d对象的起始地址,因此对pv赋值需要一个运行时计算的开销。
6.虚拟继承
在虚拟继承中,C++对象模型将Class分为两个区域,一个是不变区域,直接存储在对象中;一个是共享区域,存储的是virtual base class subobjects,它在内存中单独存储在某处,derived class object持有指向它的指针。在cfront编译器中,每一个derived class object中安插一些指针,每个指针指向一个virtual base class,为此需要付出相应的时间和空间成本。如下所示:
//具体的类同上一节多重继承,不同的是Vertex和Point3d虚拟继承了Point2d void Point3d::operator+=(const Point3d&rhs) { x+=rhs.x; y+=rhs.y; z+=rhs.z; } //编译器翻译后的版本 _vbcPoint2d->x+=rhs._vbcPoint2d->x; _vbcPoint2d->y+=rhs._vbcPoint2d->y; z+=rhs.z;
这只是最基本的解决方案,书中还提出了一些编译器优化时间和空间的方法,感兴趣可以深入阅读一下。
最后,书中探讨了如何获得类中某个成员的地址偏移。我在这里就总结两种方法:
1.((int)&((structure*)0)->member)
2.先通过 int Test::* pOffset = &Test::x;获取偏移变量,再利用reinterpret_cast<int>(*(void**)(&pOffset))将其转化为整形量。