浅谈C++非多态单继承数据布局

最近在看《深度探索C++对象模型》,打算先总结下C++中的数据布局,这篇暂时先谈谈非多态(non-polymorphic)单继承的情况:

一般而言,当我们谈及C++中的继承和多态就默认进入到其面向对象的语境中了。封装是基础,公有继承(public)是手段,然后带来运行时多态(run-time polymorphism)的弹性,通过“基类指针或引用可以透明的指向任何派生类对象”这句话来支撑着C++的面向对象体系。目的无他,无非是简化用户(这里的用户指的是类的使用者)的使用逻辑,即归一化。归一化说白了也就是用简单、统一的外部接口获取多样的内在执行逻辑,而在暴露在外的统一的接口背后其实隐藏着千差万别的内在执行逻辑,处理这些则是类的生产者的任务,它们对用户而言则是透明的,根本无需关注。

其实这一点在现实生活中也有很多活生生的例子可以类比:比如插头与插座,不管是哪个厂家生产的插座与插头,不管背后的电路多么复杂,对于用户而言,想要接通,只需要将插头对准插孔插进去这么简单。什么是归一化,这就是归一化。

归一化、弹性、多态这些词语背后其实蕴藏着共同的内涵,简化使用逻辑。还有遥控器也是,无论空调遥控器、冰箱遥控器还是电视遥控器,对用户而言,无非按一下相应功能的按钮即可使用。想想如果不是这样那就麻烦大了,我每往家里添置一件新的电器都要经过培训一段时间才能使用那不得麻烦死。正因为这些背后的内在逻辑被封装了,暴露给用户的只有简单、统一的接口,所以大大降低了用户使用的门槛。

要说面向对象能带来的好处也无非这些:用户能够通过使用简单、统一的接口获取不同的内在逻辑。当然天下没有免费的午餐,任何一件事情要想获得其好处的同时必然会付出相应的代价,无论在现实世界还是代码的世界,所以权衡是永远的主题。面向对象也是一样,要获得面向对象带来的弹性,就必然需要付出时间和空间上的代价,这个以后会谈到。

借题发挥了这么多,让我们回到正题,先看一个例子:

 1 class Concrete1{
 2 private:
 3     int value;
 4     char c1;
 5 };
 6 
 7 class Concrete2:public Concrete1{
 8 private:
 9     char c2;
10 };
11 
12 class Concrete3:public Concrete2{
13 private:
14     char c3;
15 };
16 
17 int main(){
18 
19     Concrete1 con1;
20     Concrete2 con2;
21     Concrete3 con3;
22 
23     cout<<sizeof(con1)<<endl;
24     cout<<sizeof(con2)<<endl;
25     cout<<sizeof(con3)<<endl;
26 
27     getchar();
28     return 0;
29 }

上面我们定义了三个类,这三个类是单继承的关系,但并不具备多态性,因为没有使用到虚(virtual)机制,这里仅仅是为了说明问题,实际中当然不会有这么trivial的类。好了,上述输出结果是什么呢?在本人32位机器,使用Visual Studio 2010编译器输出的结果如下:

一般而言,对于结果1应该很好理解,因为Concrete1对象包含4bytes的int加上1Bytes的char一共是5bytes,但是为了提高bus的运输效率,cpu会偷偷将数据大小调整到某数的整数倍,在32位机器上,alignment为4bytes,那么现在5bytes对齐之后就是8bytes,其中3bytes为cpu暗自进行的填充所致,目的是使该object能够符合该机器字长的边界。

但是纵然如此,按照常理,Concrete2对象的大小不应该是4+1+1+填充的2 bytes=8bytes 以及 Concrete3对象的大小不应该是4+1+1+1填充的1 bytes=8bytes么?我们可以看到,实际情况并非如此,原因在于C++保证“出现在派生类对象中的基类子对象有其完整原样性”

那么现在好理解了,在Concrete2的对象中,Concrete1子对象原封不动的被保留了,即8 bytes,尽管其中有3 bytes是完全浪费的,那么Concrete2对象的大小就应该是 8+1+3(填充的)=12bytes了,符合实验结果;同理,Concrete3对象的大小就应该是12+1+3(填充)=16bytes了,跟实验结果是相符合的。

看到这里人们肯定会困惑C++这么做不是浪费空间么,Concrete3对象的大小可硬生生比预期大了一倍!

C++这么做当然是有他的道理的,让我们反过来想想,如果不这么做,会怎样?如果不在派生类对象中保留基类子对象的完整原样性,那么上述三个类的对象在内存中的布局应该是这样的:

那么当我们执行*p2=*p1时(其中p1,p2均为Concrete1类型的指针,但p1指向Concrete1类型对象,p2指向Concrete2类型对象),Concrete1子对象的复制操作就会破坏Concrete2的c2成员,而这种bug将会很难发现。

以上便是非多态(non-polymorphic)单继承情形下的数据布局。


posted @ 2015-01-23 17:09  _江湖夜雨十年灯  阅读(267)  评论(0编辑  收藏  举报