C++ 虚继承

转自:http://www.cppblog.com/chemz/archive/2007/06/12/26135.html

虚继承和虚基类的定义是非常的简单的,同时也是非常容易判断一个继承是否是虚继承的,虽然这两个概念的定义是非常的简单明确的,但是在C++语言中虚继承作为一个比较生僻的但是又是绝对必要的组成部份而存在着,并且其行为和模型均表现出和一般的继承体系之间的巨大的差异(包括访问性能上的差异),现在我们就来彻底的从语言、模型、性能和应用等多个方面对虚继承和虚基类进行研究。
    首先还是先给出虚继承和虚基类的定义。

    虚继承:在继承定义中包含了virtual关键字的继承关系;
    虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是:
            class CSubClass : public virtual CBase {}; 其中CBase称之为CSubClass的虚基类,而不是说CBase就是个虚基类,因为CBase还可以不不是虚继承体系中的基类。

有了上面的定义后,就可以开始虚继承和虚基类的本质研究了,下面按照语法、语义、模型、性能和应用五个方面进行全面的描述。

1. 语法
       语法有语言的本身的定义所决定,总体上来说非常的简单,如下:
           class CSubClass : public virtual CBaseClass {};

       其中可以采用public、protected、private三种不同的继承关键字进行修饰,只要确保包含virtual就可以了,这样一来就形成了虚继承体系,同时CBaseClass就成为了CSubClass的虚基类了。

        其实并没有那么的简单,如果出现虚继承体系的进一步继承会出现什么样的状况呢?

        如下所示:

/*
  * 带有数据成员的基类
 */
class CBaseClass1
{
public:
        CBaseClass1( size_t i ) : m_val( i ) {}
private:
         size_t m_val;
};

/*
 * 虚拟继承体系
 */
class CSubClassV1 : public virtual CBaseClass1
{
public:
          CSubClassV1( size_t i ) : CBaseClass1( i ) {}
};           

class CSubClassV2 : public virtual CBaseClass1
{
public:
          CSubClassV2( size_t i ) : CBaseClass1( i ) {}
};  
        
class CDiamondClass1 : public CSubClassV1, public CSubClassV2
{
public:
           CDiamondClass1( size_t i ) : CBaseClass1( i ), CSubClassV1( i ), CSubClassV2( i ) {}
};   
        
class CDiamondSubClass1 : public CDiamondClass1
{
public:
           CDiamondSubClass1( size_t i ) : CBaseClass1( i ), CDiamondClass1( i ) {}
};


注意上面代码中的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体系就是这样的)。由于每一个继承类都必须包含初始化语句而又仅仅只在最底层子类中调用,这样可能就会使得某些上层子类得到的虚基类子对象的状态不是自己所期望的(因为自己的初始化语句被压制了),所以一般建议不要在虚基类中包含任何数据成员(不要有状态),只可以作为接口类来提供。

 代码示例:

#include <iostream>
#include <ostream>
#include <cstring>

using namespace std;

class CBase
{
public:
    virtual void fun ()
    {
        cout << "CBase:fun" << endl;
    }
private:
    char a[3];
};

class Derived1:public virtual CBase
{
public:
    void fun()
    {
        cout << "Derived1:fun" << endl;
    }
};

class Derived2:public virtual CBase
{
public:
    void fun()
    {
        cout << "Derived2:fun" << endl;
    }
};

class Derived:public virtual Derived1, public virtual Derived2 //有无virtual同一效果 与下比较
{
public:
    void fun()
    {
        cout << "Derived:fun" << endl;
    }
};

class Derived_:public  Derived1, public  Derived2
{
public:
    void fun()
    {
        cout << "Derived_:fun" << endl;
    }
};


int main()
{
    CBase b;
    Derived1 d1;
    Derived2 d2;
    Derived d;
    Derived_ d_;

    cout << "CBase " << sizeof(b) << endl;
    cout << "Derived1 " << sizeof(d1) << endl;
    cout << "Derived2 " << sizeof(d2) << endl;
    cout << "Derived " << sizeof(d) << endl;

    cout << "Derived_ " << sizeof(d_) << endl;

    return 0;
}


执行效果:

 

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

 

posted on 2012-11-12 14:38  as_  阅读(2544)  评论(0编辑  收藏  举报

导航