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个字节;

 

### C++虚继承的概念及用法 #### 虚继承的定义 在 C++ 中,虚继承是一种特殊的继承方式,它通过 `virtual` 关键字来实现。其主要目的是解决多重继承中可能产生的重复基类子对象问题[^2]。 #### 多重继承中的问题 在普通的多重继承场景下,可能会出现同一个基类被多次继承的情况。这会导致该基类的对象实例化两次或更多次,从而引发数据冗余以及访问冲突等问题[^5]。 #### 虚继承的作用 通过使用虚继承,可以在多重继承结构中确保只有一个共同的基类副本存在于最终派生类中。这样仅减少了内存占用,还避免了因多个相同基类副本而导致的操作混乱[^3]。 #### 实现方法 要声明一个虚继承的关系,在继承列表中加入 `virtual` 关键字即可。例如: ```cpp class Base { int data; }; // 定义两个中间层类,并都以虚方式从Base继承 class Derived1 : virtual public Base {}; class Derived2 : virtual public Base {}; // 最终派生类同时继承Derived1和Derived2 class FinalClass : public Derived1, public Derived2 {}; ``` 上述代码片段展示了如何设置虚继承关系,使得即使 `FinalClass` 同时继承自 `Derived1` 和 `Derived2`,也只会有一个 `Base` 类型的数据成员存在其中[^4]。 #### 访问控制的变化 由于引入了虚继承机制,编译器会调整内部存储布局策略,通常采用间接寻址的方法(比如增加一层指针)去定位唯一的共享基类部分。这意味着相比普通单或多级线性继承模式而言,性能上会有轻微下降,但在复杂层次体系里却是必要的折衷措施之一。 以下是具体应用的一个例子: ```cpp #include <iostream> using namespace std; class Grandparent{ protected: string name="Grand"; public: void showName(){ cout <<name<< endl;} }; // Virtual base class inheritance. class Parent1: virtual public Grandparent{}; class Parent2: virtual public Grandparent{}; class Child: public Parent1, public Parent2{}; int main(){ Child c; c.showName(); // Only one copy of 'showName' exists due to virtual inheritance. } ``` 此案例说明即便 `Child` 继承了两条通往 `Grandparent` 的路径,但由于采用了虚继承技术,所以实际运行期间只保留了一份祖先级别的属性与行为定义[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值