C++虚继承对内存空间影响分析

1、  虚继承概念

虚继承 (也称为共享继承)是面向对象编程中的一种技术,是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类。

虚继承是多重继承中特有的概念。虚基类是为解决多重继承而出现的。

2、  虚继承和虚基类

虚继承:在继承定义中包含了virtual关键字的继承关系;

虚基类:在虚继承体系中的通过virtual继承而来的基类

举例说明:

class CSubClass: public virtual CBase {};

其中CBase称之为CSubClass的虚基类,而不是说CBase就是个虚基类,因为CBase还可以作为不是虚继承体系中的基类。

3、  为什么使用虚继承

虚继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要(因为这样只会降低效率和占用更多的空间,关于这一点,我自己还没有太多深刻的理解,有兴趣的可以看网络上白杨的作品《RTTI、虚函数和虚基类的开销分析及使用指导》)。

虚继承是解决C++多重继承(比如菱形继承)问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题,通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。

可参阅连接:http://blog.csdn.net/monamokia/article/details/51537312

4、  虚继承实现

虚继承底层实现原理与编译器相关,一般通过虚基类指针虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

 

实际上,vbptr指的是虚基类表指针(virtualbase table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

在这里我们可以对比虚函数的实现原理:他们有相似之处,都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)。

虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。

虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。

5、  源代码示例

5.1、简单虚继承

///******模式1-只有虚继承

//解析:这里需要理解虚继承基类对派生类的空间大小的影响,理解虚指针在虚继承中为子类带来了哪些空间的改变。

classA

{

public:

    inta;

    virtualfunc();//如果虚基类中存在虚函数,则虚继承的子类中出现虚函数也不再增加存储空间。也可以理解为:虚继承关系的所有类中只有一个选函数指针。

};//sizeof(A)=8

 

classB:  virtualpublicA

{

public:

    intb;

};//sizeof(B)=8(A副本或A的拷贝)+4(虚基类指针)+4(自己变量)=16

 

classC:virtualpublicB

{

    charc;

};//sizeof(c)=16(B副本或B的拷贝)+4(虚基类指针)+4(自己变量)=24

//******/

5.2、多重继承

///******模式2-虚继承+多重继承

//解析:这里需要关注classD的数据空间大小,理解多重虚继承对派生类虚指针以及派生类空间的影响。

classA

{

public:

    inta;

    virtualfunc();

};//sizeof(A)=8

 

classB:virtualpublicA

{

    intb;

};//sizeof(B)=8(A副本或A的拷贝)+4(虚基类指针)+4(自己变量)=16

 

classC:virtualpublicA

{

    intc;

};//sizeof(C)=8(A副本或A的拷贝)+4(虚基类指针)+4(自己变量)=16

 

classD:publicB,publicC

{

    intd;

};

//sizeof(D)=16(B副本或B的拷贝)+16(C副本或C的拷贝)+4(自己变量)-8(A副本或A的拷贝)=28

//这里需要注意要减去8(A副本或A的拷贝),因为B和C同时继承A,都含有A的副本或拷贝,而D只需要保存一个A的副本就好了

//即sizeof(D)=8(A的副本)+8(B的虚表+B的变量)+8(C的虚表+C的变量)+4(自己变量)=28

//******/

5.3、普通继承

///******模式3-普通继承(含有:空类、虚函数)

//解析:这里需要关注classD的数据空间大小,理解多重虚继承对派生类虚指针以及派生类空间的影响。

//如果类是空的,普通继承和虚继承大小忽略。

//这里需要区分一下:

//①没有继承的时候,存在虚函数则需要加上虚指针,如果有多个也只需要加上一个,因为只有一个虚指针;

//②对于普通继承,类D和类E中自己的虚函数,大小为0,因为,它没有虚表;

//③对于虚继承中,派生类中存在一个或多个虚函数的时候,它本身就有一个虚表,指向自己的虚表,所以要加4

 

classA

{

};//sizeof(A)=1

classB

{

    charch;

    virtualvoidfunc0()  {  }

};//sizeof(B)=8

 

 

classC

{

    charch1;

    charch2;

    virtualvoidfunc()  {  }

    virtualvoidfunc1()  {  }

};//sizeof(C)=8

classD:publicA,publicC//如果类是空的,普通继承和虚继承大小忽略。

{

    intd;

    virtualvoidfunc()  {  }

    virtualvoidfunc1()  {  }

};//sizeof(D)=12

classE:publicB,publicC

{

    inte;

    virtualvoidfunc0()  {  }

    virtualvoidfunc1()  {  }

};//sizeof(E)=20

//******/

5.4、虚继承(多重继承和虚函数)

///******模式4-虚继承(多重继承和虚函数)

//如果是普通继承,(1)基类有虚函数,子类即便有虚函数也不会因此增加存储空间;(2)基类没有虚函数,子类有虚函数,只加一个指针空间4;

//如果是虚继承,不论虚基类和子类有没有虚函数,子类都仅加一个虚指针空间4.

classCommonBase

{

    intco;

};//size=4

 

classBase1:virtualpublicCommonBase

{

public:

    virtualvoidprint1(){  }

    virtualvoidprint2(){  }

private:

    intb1;

};//4副本+4自身+4(虚继承+虚函数构成指针多一个)=12

 

 

classBase2:virtualpublicCommonBase

{

public:

    virtualvoiddump1(){  }

    virtualvoiddump2(){  }

private:

    intb2;

};//4副本+4自身+4(虚继承+虚函数构成指针多一个)=12

 

classDerived:publicBase1,publicBase2

{

public:

    voidprint2(){  }

    voiddump2(){  }

private:

    intd;

};//12+12+4-4(虚继承+虚函数构成指针多一个,Base1和Base2重复)=24

//******/

5.5、虚继承(多重继承和虚函数)

///******模式5-虚继承与虚函数

 

classA

{

public:

    virtualvoidaa(){  }

    virtualvoidaa2(){  }

private:

    charch[3];

};//1+4=补齐=8

 

classB:virtualpublicA

{

public:

    virtualvoidbb(){  }

    virtualvoidbb2(){  }

};//8(副本)+4(虚基类指针)=12

//******/

6、  结论

待验证:重要的事情讲三遍!!!如果不是虚继承的类,即便有虚函数也不会因此增加存储空间,如果是虚继承的类,没有虚函数就添加一个虚指针空间,有虚函数不论多少个,就添加两个虚指针空间!!!

我的结论://如果是普通继承,(1)基类有虚函数,子类即便有虚函数也不会因此增加存储空间;(2)基类没有虚函数,子类有虚函数,只加一个指针空间4;

//如果是虚继承,不论虚基类和子类有没有虚函数,子类都仅加一个虚指针空间4.

 

 

附加:虚继承的构造顺序

链接:http://blog.csdn.net/rongxiaojun1989/article/details/42745213

 

若D1继承自B,D2继承自B,D继承自D1和D2,默认情况下派生类将含有继承链上每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中将包含该类的多个子对象。

虚继承解决了这一问题,虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象成为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。

我们指定虚基类的方式是在派生列表中添加关键字virtual,virtual说明符表明了一种愿望,即在后续的派生类当中共享虚基类的同一份实例。

如图

Panda通过Raccoon和Bear继承了ZooAnimal,因为Raccoon和Bear继承ZooAnimal的方式都是虚继承,所以在Panda中只有一个ZooAnimal基类部分。

 

如何初始化

在虚派生中,虚基类是由最低成的派生类初始化的。以上图为例,当创建Panda对象时,由Panda的构造函数独自控制ZooAnimal的初始化过程。

 

为了理解这一规则,我们不妨假设当以普通规则处理初始化任务时会发生什么情况。在此例中,虚基类将会在多条继承路径上被重复初始化。以ZooAnimal为例,如果应用普通规则,则Raccoon和Bear都会试图初始化Panda对象的ZooAnimal部分。

 

当然继承体系中的每个类都可能在某个时刻成为“最低层的派生类”。故只要我们能创建虚基类的派生类对象,则该派生类的构造函数就必须初始化它的虚基类。

 

构造顺序问题

虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。

一个类可以有多个虚基类。此时,这些虚基类的子对象按照它们在派生列表中出现的顺序从左到右依次构造。例如:

class Character {/*...*/};

class BookCharacter : public Character {/*...*/};

class ToyAnimal {/*...*/};

class TeddyBear : public BookCharacter, public Bear, virtual publicToyAnimal

{/*...*/};

编译器按照直接基类的声明顺序对其依次进行检查,以确定其中是否含有虚基类。如果有,则先构造虚基类,然后按照声明的顺序逐一构造其它非虚基类。因此,要想创建一个TeddyBear对象,需要按照如下次序调用这些构造函数:

ZooAnimal();           //Bear的虚基类

ToyAnimal();           //直接虚基类

Character();            //第一个非虚基类的间接基类

BookCharacter();    //第一个直接非虚基类

Bear();                     //第二个直接非虚基类

TeddyBear();           //最低层派生类

 

再如:

[cpp] view plain copy

1. #include "iostream.h"  

2. class base1  

3. {    

4. public:      

5.     base1(){cout<<"class base1"<<endl;}  

6. };  

7. class base2  

8. {    

9. public:      

10.    base2(){cout<<"class base2"<<endl;}  

11.};  

12.class level1:public base2,virtual public base1  

13.{    

14.public:      

15.    level1(){cout<<"class level1"<<endl;}  

16.};  

17.class level2:virtual public base1,virtual public base2  

18.{    

19.public:      

20.    level2(){cout<<"class level2"<<endl;}  

21.};  

22.class toplevel:public level1,virtual public level2  

23.{    

24.public:      

25.    toplevel(){cout<<"class toplevel"<<endl;}  

26.};  

27.void main()  

28.{  

29.    toplevel obj;  

30.}  

结果

class base1          
class base2          
class level2          
class base2          
class level1         
class toplevel

 

标准格式

[cpp] view plain copy

1. typedef struct TcpRow  

2. {  

3.     std::string sIp;  

4.     DWORD dwPort;  

5.   

6.     TcpRow(  

7.         const std::string &sIp_t = "",  

8.         const DWORD dwPort_t = 0  

9.         )  

10.        :  

11.        sIp(sIp_t),  

12.        dwPort(dwPort_t)  

13.    {}  

14.}TcpRow;  

15.  

16.typedef struct TcpRowWithState : virtual public TcpRow  

17.{  

18.    DWORD dwState;  

19.  

20.    TcpRowWithState(  

21.        const DWORD dwState_t = 0,   

22.        const std::string &sIp_t = "",   

23.        const DWORD dwPort_t = 0  

24.        )  

25.        :  

26.        TcpRow(  

27.            sIp_t,   

28.            dwPort_t  

29.            ),   

30.        dwState(dwState_t)  

31.    {}  

32.}TcpRowWithState;  

33.  

34.typedef struct TcpRowWithProc : virtual public TcpRow  

35.{  

36.    DWORD dwPid;  

37.    std::string sProcName;  

38.  

39.    TcpRowWithProc(  

40.        const DWORD dwPid_t = 0,  

41.        const std::string &sProcName_t = "",  

42.        const std::string &sIp_t = "",   

43.        const DWORD dwPort_t = 0  

44.        )  

45.        :  

46.        TcpRow(  

47.            sIp_t,   

48.            dwPort_t  

49.            ),   

50.        dwPid(dwPid_t),  

51.        sProcName(sProcName_t)  

52.    {}  

53.}TcpRowWithProc;  

54.  

55.typedef struct TcpRowWithAll : public TcpRowWithState, public TcpRowWithProc  

56.{  

57.    TcpRowWithAll(  

58.        const DWORD dwState_t = 0,  

59.        const DWORD dwPid_t = 0,  

60.        const std::string &sProcName_t = "",  

61.        const std::string &sIp_t = "",   

62.        const DWORD dwPort_t = 0  

63.        )  

64.        :  

65.        TcpRow(  

66.            sIp_t,   

67.            dwPort_t  

68.            ),   

69.        TcpRowWithState(dwState),  

70.        TcpRowWithProc(  

71.            dwPid_t,   

72.            sProcName_t  

73.            )  

74.    {}  

75.}TcpRowWithAll;  

 

阅读更多
个人分类: 分享
下一篇static_cast和dynamic_cast等类型转换
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭