C++的多重继承及virtual继承所带来的布局繁杂

前言

在我写下这篇文章的时候,是2012年的春节。此刻已经深夜了,外面鞭炮声震耳欲聋,想起往年的这个时候,现在该是和家里人聚在一起看春晚的。而今已时过境迁。虽然如此,但我从未感到孤独或什么的,甚至连春晚都懒得看了。。 怪也就怪自己这几天突然开始深度迷恋《inside c++ object model》,看到最入迷的时候,我甚至感觉自己的呼吸都停止了,真是一本不可多得的好书啊,呵呵。 题外话说多了,开始入正题。

对于C++的对象布局,早在以前看《effective c++》的时候,Scott Meyers在讨论“为多态基类声明虚析构函数”(条款7)中如是说道:“无端地将所有class臼的析构函数声明为virtual ,就像从未声明它们为virtual 一样,都是错误的。许多人的心得是:只有当class 内含至少一个virtual 函数,才为它声明virtual 析构函数”。对于这点,当时或多或少能理解个大概,因为在以前看侯捷的《深入浅出MFC》的时候,作者对对象的虚函数表也略作了讨论,虽不是很深入的探讨,但也足以让这样我一个当时对C++对象内部布局毫无理解的小白对虚函数表的外观有个大体认识。而今看的《inside c++ object model》也为侯捷所译,而且对原版书籍中的很多错误都进行了详细注解,不得不为这位台湾大师的资深水平及其多国外经典翻译所作出的贡献而深感敬佩。。不知什么时候才会达到侯捷的水平,至少现在,我在努力。 —— stay hungry,stay foolish!

 

单一继承后的对象布局

对于单个类的对象布局,如果其不含虚函数,那么其与一个普通的c-struct布局类似。对于含有虚函数的类,还得考虑虚函数表vptr所带来的开销。而对于单一继承后的对象布局呢?来看看如下代码:

 

    #include <iostream> 
     
    using namespace std; 
     
    #include <iostream>  
     
    using namespace std;  
     
    class BaseClass 
    { 
    protected: 
        int m_nValue; 
        char m_chValue; 
    };  
     
    class DerivedClass1:public BaseClass 
    { 
    protected: 
        char m_chValue1; 
    };  
     
    class DerivedClass2:public DerivedClass1 
    { 
    public: 
        //virtual void VFunction(){} 
    protected: 
        char m_chValue2;  
     
    };  
     
    int main(int *argc,char **argv) 
    { 
        DerivedClass2 DerivedObject; 
        cout<<"size of DerivedObject is: "<<sizeof(DerivedObject)<<endl; 
        return 0; 
    } 
这段代码在VS2008中的测试结果为:“size of DerivedObject is: 16”,DerivedClass的内存布局并非直接将所有基类成员先复制到自身然后再初始化,而是将基类以一种单一对象的方式初始化,然后初始化自身的nonstatic data member(期间当然要遵循内存对齐原则)。这种布局方式类似于类或结构中存在委托对象的布局,试着将DerivedClass2改为如下:

 

    class DerivedClass2 
    { 
    public: 
        //virtual void VFunction(){} 
    protected: 
        DerivedClass1 derived1; 
        char m_chValue2; 
     
    }; 

 

那么得出的结果也会一样。

 

加上虚继承及多承继承之后的对象布局

对于虚继承之后的对象布局,来看看原书中一段有趣的代码:

    #include <iostream>  
     
    using namespace std;  
     
    class BaseClass{}; 
    class DerivedClassA:virtual public BaseClass 
    { 
    public: 
        //virtual void VFunction(){} 
    }; 
    class DerivedClassB:virtual public BaseClass{}; 
    class DerivedClass:public DerivedClassA,public DerivedClassB{};  
     
    int main() 
    { 
        BaseClass baseClass; 
        DerivedClassA derivedClassA; 
        DerivedClassB derivedClassB; 
        DerivedClass derivedClass;  
     
        cout<<"size of baseClass is: "<<sizeof(baseClass)<<endl; 
        cout<<"size of direvedClassA is: "<<sizeof(derivedClassA)<<endl; 
        cout<<"size of direvedClassB is: "<<sizeof(derivedClassB)<<endl; 
        cout<<"size of direvedClass  is: "<<sizeof(derivedClass)<<endl; 
        return 0; 
    } 

 

在VS2008中得出的结果是:

size of baseClass is: 1
size of direvedClassA is: 4
size of direvedClassB is: 4
size of direvedClass is: 8

有人在G++编译器下测试的结果为1,4,4,4。看来MS在编译优化方面还没有G++走的快。或许也仅仅是这一点吧。

以前只知道空struct 或 class的大小为1,肯定不是0,而一直不知其原因,直至现在看了Lippman大牛的著作,才一解心中之惑。对于BaseClass对象的布局,如果它里面什么都不存放,那么定义两个baseClass的对象,形式上看来它们应该是完全独立的对象,而它们在内存中的地址值或许会一样,为了避免这一点,编译器在编译时候为空class或struct就增加一个char(1byte),以使得不同对象有两个相对独立的地址值。

derivedClassA和derivedClassB因为有virtual base class,其对象中会存储一个隐含的vbtptr来指向一个virtual base class table,这种表类似于virtual function table,只是virtual function table中存储的是虚函数地址,而virtual base class table中存储的是当前类的所有vitrual base的对象地址。vbtptr的初始化和析构工作如同普通nonstatc data member一样,故而对象大小为4,而derivedClass继承两个vtptr(至少vs2008编译器肯定如此,g++编译器对于相同的vtptr进行优化后,只存在一个vtptr)。大小为8也就不足为怪了。

 

如果去掉DerivedClassA中的virtual function的注释,得出的结果会是:

size of baseClass is: 1
size of direvedClassA is: 8
size of direvedClassB is: 4
size of direvedClass is: 12

在direvedClassA的对象中又会增加一个隐含的vftptr来指向一个virtual function table。对于这一点,没必要进行过多阐述了。

 

如果将DerivedClassA中的virtual function移动到BaseClass中,得出的结果又会是:

size of baseClass is: 4
size of direvedClassA is: 8
size of direvedClassB is: 8
size of direvedClass is: 12

此时的baseClass因为有了一个virtual function,对应的内存消耗又会增加一个virtual function table和一个vftptr,direvedClassA和direvedClassB对象除了存放一个vtbptr之外,还会存放一个继承而来的vtfptr,大小为8也毋庸置疑。而direvedClass的大小,给我的第一感觉应该是16才对,仔细一想。脑子又糊涂了,呵呵。大概是夜太深的缘故吧。。

 

后记

回到“前言”处对Scott Meyers的条款讨论中的困惑,无缘无故的乱将nonstatic member funtion声明为virtualfunction,那么每一次乱来,都将会由于virtul function table而无形中增加一些内存消耗。。 Lippman对于C++的object model讲解非常细,以至于我写下这篇博客后感觉自己一直是大而化之的一笔带过。不知道看的人有什么感受。。呵呵,还是想说句:writing just for remembring。

posted on 2012-01-23 08:44  酋长Clement  阅读(428)  评论(0编辑  收藏  举报

导航