C++虚继承与虚基类的本质

 

我的新浪微博:http://weibo.com/freshairbrucewoo

 

欢迎大家相互交流,共同提高技术。

 

 声明:此篇博客网上转载,转载原因是对于虚拟继承分析得比较彻底:让我很好的理解了虚拟继承的作用于带来的坏处(性能的损失)。 

  虚继承和虚基类的定义是非常的简单的,同时也是非常容易判断一个继承是否是虚继承的,虽然这两个概念的定义是非常的简单明确的,但是在C++语言中虚继承作为一个比较生僻的但是又是绝对必要的组成部份而存在着,并且其行为和模型均表现出和一般的继承体系之间的巨大的差异(包括访问性能上的差异),现在我们就来彻底的从语言、模型、性能和应用等多个方面对虚继承和虚基类进行研究。 首先还是先给出虚继承和虚基类的定义。 虚继承:在继承定义中包含了virtual关键字的继承关系; 虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是: struct CSubClass : public virtual CBase {}; 其中CBase称之为CSubClass 的虚基类,而不是说CBase就是个虚基类,因为CBase还可以不不是虚继承体系 中的基类。 有了上面的定义后,就可以开始虚继承和虚基类的本质研究了,下面按照语法、语义、 模型、性能和应用五个方面进行全面的描述。
    1. 语法 语法有语言的本身的定义所决定,总体上来说非常的简单,如下: struct CSubClass : public virtual CBaseClass {}; 其中可以采用public、protected、private三种不同的继承关键字进行修饰,只要 确保包含virtual就可以了,这样一来就形成了虚继承体系,同时CBaseClass就成为 了CSubClass的虚基类了。 其实并没有那么的简单,如果出现虚继承体系的进一步继承会出现什么样的状况呢? 如下所示:

 1  /* * 带有数据成员的基类 */
2 struct CBaseClass1
3 {
4
5 CBaseClass1( size_t i ) : m_val( i ) {}
6 size_t m_val;
7 };
8 /* * 虚拟继承体系 */
9 struct CSubClassV1 : public virtual CBaseClass1
10 {
11 CSubClassV1( size_t i ) : CBaseClass1( i ) {}
12 };
13 struct CSubClassV2 : public virtual CBaseClass1
14 {
15 CSubClassV2( size_t i ) : CBaseClass1( i ) {}
16 };
17 struct CDiamondClass1 : public CSubClassV1, public CSubClassV2
18 {
19 CDiamondClass1( size_t i ) : CBaseClass1( i ), CSubClassV1( i ), CSubClassV2( i ) {}
20 };
21 struct CDiamondSubClass1 : public CDiamondClass1
22 {
23 CDiamondSubClass1( size_t i ) : CBaseClass1( i ), CDiamondClass1( i ) {}
24 };

  注意上面代码中的CDiamondClass1和CDiamondSubClass1两个类的构造函数初始化列 表中的内容。可以发现其中均包含了虚基类CBaseClass1的初始化工作,如果没有这 个初始化语句就会导致编译时错误,为什么会这样呢?一般情况下不是只要在 CSubClassV1和CSubClassV2中包含初始化就可以了么?要解释该问题必须要明白虚 继承的语义特征,所以参看下面语义部分的解释。
  2. 语义 从语义上来讲什么是虚继承和虚基类呢?上面仅仅是从如何在C++语言中书写合法的 虚继承类定义而已。首先来了解一下virtual这个关键字在C++中的公共含义,在C++ 语言中仅仅有两个地方可以使用virtual这个关键字,一个就是类成员虚函数和这里 所讨论的虚继承。不要看这两种应用场合好像没什么关系,其实他们在背景语义上 具有virtual这个词所代表的共同的含义,所以才会在这两种场合使用相同的关键字。 那么virtual这个词的含义是什么呢?

virtual在《美国传统词典[双解]》中是这样定义的:

  adj.(形容词) 1. Existing or resulting in essence or effect though not in actual fact, form, or name: 实质上的,实际上的:虽然没有实际的事实、形式或名义,但在实际上或效 果上存在或产生的;

           2. Existing in the mind, especially as a product of the imagination. Used in literary criticism of text. 虚的,内心的:在头脑中存在的,尤指意想的产物。用于文学批评中。

   我们采用第一个定义,也就是说被virtual所修饰的事物或现象在本质上是存在的, 但是没有直观的形式表现,无法直接描述或定义,需要通过其他的间接方式或手段 才能够体现出其实际上的效果。 那么在C++中就是采用了这个词意,不可以在语言模型中直接调用或体现的,但是确 实是存在可以被间接的方式进行调用或体现的。比如:虚函数必须要通过一种间接的 运行时(而不是编译时)机制才能够激活(调用)的函数,而虚继承也是必须在运行 时才能够进行定位访问的一种体制。存在,但间接。其中关键就在于存在、间接和共 享这三种特征。 对于虚函数而言,这三个特征是很好理解的,间接性表明了他必须在运行时根据实际 的对象来完成函数寻址,共享性表象在基类会共享被子类重载后的虚函数,其实指向 相同的函数入口。 对于虚继承而言,这三个特征如何理解呢?存在即表示虚继承体系和虚基类确实存在, 间接性表明了在访问虚基类的成员时同样也必须通过某种间接机制来完成(下面模型 中会讲到),共享性表象在虚基类会在虚继承体系中被共享,而不会出现多份拷贝。 那现在可以解释语法小节中留下来的那个问题了,“为什么一旦出现了虚基类,就必须在没有一个继承类中都必须包含虚基类的初始化语句”。由上面的分析可以知道, 虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会 出现一个虚基类的子对象(这和多继承是完全不同的),这样一来既然是共享的那么每一个子类都不会独占,但是总还是必须要有一个类来完成基类的初始化过程(因为 所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到 底谁应该负责完成初始化呢?C++标准中(也是很自然的)选择在每一次继承子类中 都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),而在最下层 继承子类中实际执行初始化过程。所以上面在每一个继承类中都要书写初始化语句, 但是在创建对象时,而仅仅会在创建对象用的类构造函数中实际的执行初始化语句, 其他的初始化语句都会被压制不调用。
  3. 模型 为了实现上面所说的三种语义含义,在考虑对象的实现模型(也就是内存模型)时就 很自然了。在C++中对象实际上就是一个连续的地址空间的语义代表,我们来分析虚 继承下的内存模型。

  3.1. 存在 也就是说在对象内存中必须要包含虚基类的完整子对象,以便能够完成通过地址 完成对象的标识。那么至于虚基类的子对象会存放在对象的那个位置(头、中间、 尾部)则由各个编译器选择,没有差别。(在VC8中无论虚基类被声明在什么位置, 虚基类的子对象都会被放置在对象内存的尾部)

  3.2. 间接 间接性表明了在直接虚基承子类中一定包含了某种指针(偏移或表格)来完成通 过子类访问虚基类子对象(或成员)的间接手段(因为虚基类子对象是共享的, 没有确定关系),至于采用何种手段由编译器选择。(在VC8中在子类中放置了 一个虚基类指针vbc,该指针指向虚函数表中的一个slot,该slot中存放则虚基 类子对象的偏移量的负值,实际上就是个以补码表示的int类型的值,在计算虚 基类子对象首地址时,需要将该偏移量取绝对值相加,这个主要是为了和虚表 中只能存放虚函数地址这一要求相区别,因为地址是原码表示的无符号int类型 的值)

  3.3. 共享 共享表明了在对象的内存空间中仅仅能够包含一份虚基类的子对象,并且通过 某种间接的机制来完成共享的引用关系。在介绍完整个内容后会附上测试代码, 体现这些内容。

  4. 性能 由于有了间接性和共享性两个特征,所以决定了虚继承体系下的对象在访问时必然会在时间和空间上与一般情况有较大不同。

  4.1. 时间 在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都 必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样), 其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。 (在VC8中通过打开汇编输出,可以查看*.cod文件中的内容,在访问虚基类对象 成员时会形成三条mov间接寻址语句,而在访问一般继承类对象时仅仅只有一条mov 常量直接寻址语句)

  4.2. 空间 由于共享所以不同在对象内存中保存多份虚基类子对象的拷贝,这样较之多继承节省空间。

  5. 应用 谈了那么多语言特性和内容,那么在什么情况下需要使用虚继承,而一般应该如何使 用呢? 这个问题其实很难有答案,一般情况下如果你确性出现多继承没有必要,必须要共享 基类子对象的时候可以考虑采用虚继承关系(C++标准ios体系就是这样的)。由于每 一个继承类都必须包含初始化语句而又仅仅只在最底层子类中调用,这样可能就会使 得某些上层子类得到的虚基类子对象的状态不是自己所期望的(因为自己的初始化语 句被压制了),所以一般建议不要在虚基类中包含任何数据成员(不要有状态),只 可以作为接口类来提供。
附录:测试代码

  1 #include #include 
2 /* * 带有数据成员的基类 */
3 struct CBaseClass1
4 {
5 CBaseClass1( size_t i ) : m_val( i ) {}
6 size_t m_val;
7 };
8 /* * 虚拟继承体系 */
9 struct CSubClassV1 : public virtual CBaseClass1
10 {
11 CSubClassV1( size_t i ) : CBaseClass1( i ) {}
12 };
13 struct CSubClassV2 : public virtual CBaseClass1
14 {
15 CSubClassV2( size_t i ) : CBaseClass1( i ) {}
16 };
17 struct CDiamondClass1 : public CSubClassV1, public CSubClassV2
18 {
19 CDiamondClass1( size_t i ) : CBaseClass1( i ), CSubClassV1( i ), CSubClassV2( i ) {}
20 };
21 struct CDiamondSubClass1 : public CDiamondClass1
22 {
23 CDiamondSubClass1( size_t i ) : CBaseClass1( i ), CDiamondClass1( i ) {}
24 };
25 /* * 正常继承体系 */
26 struct CSubClassN1 : public CBaseClass1
27 {
28 CSubClassN1( size_t i ) : CBaseClass1( i ) {}
29 };
30 struct CSubClassN2 : public CBaseClass1
31 {
32 CSubClassN2( size_t i ) : CBaseClass1( i ) {}
33 };
34 struct CMultiClass1 : public CSubClassN1, public CSubClassN2
35 {
36 CMultiClass1( size_t i ) : CSubClassN1( i ), CSubClassN2( i ) {}
37 };
38 struct CMultiSubClass1 : public CMultiClass1
39 {
40 CMultiSubClass1( size_t i ) : CMultiClass1( i ) {}
41 };
42 /* * 不带有数据成员的接口基类 */
43 struct CBaseClass2
44 {
45 virtual void func() {};
46 virtual ~CBaseClass2() {}
47 };
48 /* * 虚拟继承体系 */ //
49 struct CBaseClassX
50 {
51 CBaseClassX()
52 {
53 i1 = i2 = 0xFFFFFFFF;
54 }
55 size_t i1, i2;
56 };
57 struct CSubClassV3 : public virtual CBaseClass2
58 { };
59 struct CSubClassV4 : public virtual CBaseClass2
60 { };
61 struct CDiamondClass2 : public CSubClassV3, public CSubClassV4
62 { };
63 struct CDiamondSubClass2 : public CDiamondClass2
64 { };
65 /* * 正常继承体系 */
66 struct CSubClassN3 : public CBaseClass2
67 { };
68 struct CSubClassN4 : public CBaseClass2
69 { };
70 struct CMultiClass2 : public CSubClassN3, public CSubClassN4
71 { };
72 struct CMultiSubClass2 : public CMultiClass2
73 { };
74 /* * 内存布局用类声明. */
75 struct CLayoutBase1
76 {
77 CLayoutBase1() : m_val1( 0 ), m_val2( 1 ) {}
78 size_t m_val1, m_val2;
79 };
80 struct CLayoutBase2
81 {
82 CLayoutBase2() : m_val1( 3 ) {}
83 size_t m_val1;
84 };
85 struct CLayoutSubClass1 : public virtual CBaseClass1, public CLayoutBase1, public CLayoutBase2
86 {
87 CLayoutSubClass1() : CBaseClass1( 2 ) {}
88 };
89 #define MAX_TEST_COUNT 1000 * 1000 * 16
90 #define TIME_ELAPSE() ( std::clock() - start * 1.0 ) / CLOCKS_PER_SEC
91 int main( int argc, char *argv[] )
92 {
93 /* * 类体系中的尺寸. */ std::cout << "================================ sizeof ================================" << std::endl; std::cout << " ----------------------------------------------------------------" << std::endl; std::cout << "sizeof( CBaseClass1 ) = " << sizeof( CBaseClass1 ) << std::endl; std::cout << std::endl; std::cout << "sizeof( CSubClassV1 ) = " << sizeof( CSubClassV1 ) << std::endl; std::cout << "sizeof( CSubClassV2 ) = " << sizeof( CSubClassV2 ) << std::endl; std::cout << "sizeof( CDiamondClass1 ) = " << sizeof( CDiamondClass1 ) << std::endl; std::cout << "sizeof( CDiamondSubClass1 ) = " << sizeof( CDiamondSubClass1 ) << std::endl; std::cout << std::endl; std::cout << "sizeof( CSubClassN1 ) = " << sizeof( CSubClassN1 ) << std::endl; std::cout << "sizeof( CSubClassN2 ) = " << sizeof( CSubClassN2 ) << std::endl; std::cout << "sizeof( CMultiClass1 ) = " << sizeof( CMultiClass1 ) << std::endl; std::cout << "sizeof( CMultiSubClass1 ) = " << sizeof( CMultiSubClass1 ) << std::endl;
94 std::cout << " ----------------------------------------------------------------" << std::endl; std::cout << "sizeof( CBaseClass2 ) = " << sizeof( CBaseClass2 ) << std::endl; std::cout << std::endl; std::cout << "sizeof( CSubClassV3 ) = " << sizeof( CSubClassV3 ) << std::endl; std::cout << "sizeof( CSubClassV4 ) = " << sizeof( CSubClassV4 ) << std::endl; std::cout << "sizeof( CDiamondClass2 ) = " << sizeof( CDiamondClass2 ) << std::endl; std::cout << "sizeof( CDiamondSubClass2 ) = " << sizeof( CDiamondSubClass2 ) << std::endl; std::cout << std::endl; std::cout << "sizeof( CSubClassN3 ) = " << sizeof( CSubClassN3 ) << std::endl; std::cout << "sizeof( CSubClassN4 ) = " << sizeof( CSubClassN4 ) << std::endl; std::cout << "sizeof( CMultiClass2 ) = " << sizeof( CMultiClass2 ) << std::endl; std::cout << "sizeof( CMultiSubClass2 ) = " << sizeof( CMultiSubClass2 ) << std::endl; /* * 对象内存布局 */ std::cout << "================================ layout ================================" << std::endl; std::cout << " --------------------------------MI------------------------------" << std::endl; CLayoutSubClass1 *lsc = new CLayoutSubClass1; std::cout << "sizeof( CLayoutSubClass1 ) = " << sizeof( CLayoutSubClass1 ) << std::endl; std::cout << "CLayoutBase1 offset of CLayoutSubClass1 is " << (char*)(CLayoutBase1 *)lsc - (char*)lsc << std::endl; std::cout << "CBaseClass1 offset of CLayoutSubClass1 is " << (char*)(CBaseClass1 *)lsc - (char*)lsc << std::endl; std::cout << "CLayoutBase2 offset of CLayoutSubClass1 is " << (char*)(CLayoutBase2 *)lsc - (char*)lsc << std::endl;
95 int *ptr = (int*)lsc; std::cout << "vbc in CLayoutSubClass1 is " << *(int*)ptr[3] << std::endl;
96 delete lsc;
97 std::cout << " --------------------------------SI------------------------------" << std::endl; CSubClassV1 *scv1 = new CSubClassV1( 1 ); std::cout << "sizeof( CSubClassV1 ) = " << sizeof( CSubClassV1 ) << std::endl; std::cout << "CBaseClass1 offset of CSubClassV1 is " << (char*)(CBaseClass1 *)scv1 - (char*)scv1 << std::endl;
98 ptr = (int*)scv1; std::cout << "vbc in CSubClassV1 is " << *(int*)ptr[0] << std::endl;
99 delete scv1;
100 /* * 性能测试 */ std::cout << "================================ Performance ================================" << std::endl; double times[4]; size_t idx = 0;
101 CSubClassV1 *ptr1 = new CDiamondClass1( 1 ); std::clock_t start = std::clock(); { for ( size_t i = 0; i < MAX_TEST_COUNT; ++i ) ptr1->m_val = i; } times[idx++] = TIME_ELAPSE(); delete static_cast( ptr1 );
102 CSubClassN1 *ptr2 = new CMultiClass1( 0 ); start = std::clock(); { for ( size_t i = 0; i < MAX_TEST_COUNT; ++i ) ptr2->m_val = i; } times[idx++] = TIME_ELAPSE(); delete static_cast( ptr2 );
103 std::cout << "CSubClassV1::ptr1->m_val " << times[0] << " s" << std::endl; std::cout << "CSubClassN1::ptr2->m_val " << times[1] << " s" << std::endl;
104 return 0; }

 

测试环境: 软件环境:Visual Studio2005 Pro + SP1, boost1.34.0 硬件环境:PentiumD 3.0GHz, 4G RAM 测试数据: ================================ sizeof ================================ ---------------------------------------------------------------- sizeof( CBaseClass1 )       = 4
sizeof( CSubClassV1 )       = 8 sizeof( CSubClassV2 )       = 8 sizeof( CDiamondClass1 )    = 12 sizeof( CDiamondSubClass1 ) = 12
sizeof( CSubClassN1 )       = 4 sizeof( CSubClassN2 )       = 4 sizeof( CMultiClass1 )      = 8 sizeof( CMultiSubClass1 )   = 8 ---------------------------------------------------------------- sizeof( CBaseClass2 )       = 4
sizeof( CSubClassV3 )       = 8 sizeof( CSubClassV4 )       = 8 sizeof( CDiamondClass2 )    = 12 sizeof( CDiamondSubClass2 ) = 12
sizeof( CSubClassN3 )       = 4 sizeof( CSubClassN4 )       = 4 sizeof( CMultiClass2 )      = 8 sizeof( CMultiSubClass2 )   = 8 ================================ layout ================================ --------------------------------MI------------------------------ sizeof( CLayoutSubClass1 )   = 20 CLayoutBase1 offset of CLayoutSubClass1 is 0 CBaseClass1  offset of CLayoutSubClass1 is 16 CLayoutBase2 offset of CLayoutSubClass1 is 8 vbc in CLayoutSubClass1 is -12 --------------------------------SI------------------------------ sizeof( CSubClassV1 )   = 8 CBaseClass1 offset of CSubClassV1 is 4 vbc in CSubClassV1 is 0 ================================ Performance ================================ CSubClassV1::ptr1->m_val 0.062 s CSubClassN1::ptr2->m_val 0.016 s
结果分析:

1. 由于虚继承引入的间接性指针所以导致了虚继承类的尺寸会增加4个字节;

2. 由Layout输出可以看出,虚基类子对象被放在了对象的尾部(偏移为16),并且vbc 指针必须紧紧的接在虚基类子对象的前面,所以vbc指针所指向的内容为“偏移 - 4”;

3. 由于VC8将偏移放在了虚函数表中,所以为了区分函数地址和偏移,所以偏移是用补 码int表示的负值;

4. 间接性可以通过性能来看出,在虚继承体系同通过指针访问成员时的时间一般是一般 类访问情况下的4倍左右,符合汇编语言输出文件中的汇编语句的安排。

posted @ 2012-02-03 01:20  蔷薇理想人生  阅读(3831)  评论(1编辑  收藏  举报