虚拟继承

虚拟继承

虚拟继承

c++ primer 3thp813

在缺省情况下,C++中的继承是按值组合的一种特殊情况。当我们写:

class Bear : public ZooAnimal { ... };

每个Bear 类对象都含有其ZooAnimal 基类子对象的所有非静态数据成员,以及在Bear中声明的非静态数据成员。类似地,当派生类自己也作为一个基类对象时,如:

class PolarBear : public Bear { ... };

PolarBear 类对象含有在PolarBear 中声明的所有非静态数据成员,以及其Bear 子对象的所有非静态数据成员和ZooAnimal 子对象的所有非静态数据成员。

在单继承下,这种由继承支持的、特殊形式的按值组合提供了最有效的、最紧凑的对象表示。在多继承下,当一个基类在派生层次中出现多次时就会有问题。最主要的实际例子是iostream 类层次结构,ostream istream 类都从抽象ios 基类派生而来,而iostream 类又是从ostream istream 派生:

 

class iostream :public istream, public ostream { ... };//非虚拟继承

缺省情况下,每个iostream 类对象含有两个ios 子对象:在istream 子对象中的实例以及在ostream 子对象中的实例。这为什么不好?从效率上而言,存储ios 子对象的两个复本,浪费了存储区,因为iostream 只需要一个实例。而且ios 构造函数被调用了两次,每个子对象一次。更严重的问题是由于两个实例引起的二义性。例如,任何未限定修饰地访问ios 的成员都将导致编译时刻错误:到底访问哪个实例?如果ostream istream 对其ios 子对象的初始化稍稍不同,会怎样呢?怎样通过iostream 类保证这一对ios 值的一致性?在缺省的按值组合机制下,真的没有好办法可以保证这一点。

 

C++语言的解决方案是,提供另一种可替代“按引用组合”的继承机制:虚拟继承(virtual Inheritance)在虚拟继承下,只有一个共享的基类子对象被继承,而无论该基类在派生层次中出现多少次。共享的基类子对象被称为虚拟基类(virtual base class)。在虚拟继承下,基类子对象的复制及由此而引起的二义性都被消除了。

为了讨论虚拟继承的语法和语义,我们选择用Panda 类作为教学示例。在动物学领域中,人们对Panda属于浣熊科(Raccoon 还是熊(Bear科,已经激烈争论了100 多年。由于软件设计主要是一种服务性工业,所以,我们最实际的解决方案是同时从两者派生:

class Panda : public Bear,public Raccoon, public Endangered { ... };

虚拟继承Panda 层次结构如下图所示,其中两个虚箭头分别表示Bear Raccoon ZooAnimal 的虚拟派生,而三个实箭头分别表示Panda BearRaccoon Endangered的非虚拟派生:

 

如果仔细查看上图,我们会注意到虚拟继承的不直观部分:虚拟派生(本例中的BearRaccoon在先,实际上应该在后。只有伴随着Panda 的声明,虚拟继承才是必要的。但是,如果Bear Raccoon 还没有实现虚拟派生,则Panda 类的设计者就不走运了。

这是否意味着,我们应该尽可能地以虚拟方式派生我们的基类,以便层次结构中后续的派生类可能会需要虚拟继承,是这样吗?不!我们强烈反对,那样做对性能的影响会很严重(而且增加了后续类派生的复杂性)

那么,我们从不应该使用虚拟继承吗?不是,在实践中几乎所有成功使用虚拟继承的例子中,凡是需要虚拟继承的整个层次结构子树,如iostream 库或Panda 子树,都是由同一个人或项目设计组一次设计完成的。

一般地,除非虚拟继承为一个眼前的设计问题提供了解决方案,否则建议不要使用它。当然,尽管如此,现在我们仍然要看看怎样使用它J

 

虚拟基类声明

通过用关键字virtual 修正一个基类的声明可以将它指定为被虚拟派生。例如,下列声明使得ZooAnimal 成为Bear Raccoon 的虚拟基类:

// 关键字 public virtual 的顺序不重要

class Bear : public virtual ZooAnimal { ... };

class Raccoon : virtual public ZooAnimal { ... };

虚拟派生不是基类本身的一个显式特性,而是它与派生类的关系。如前面所说明的,虚拟继承提供了“按引用组合”。也就是说,对于子对象及其非静态成员的访问是间接进行的。这使得在多继承情况下,把多个虚拟基类子对象组合成派生类中的一个共享实例,从而提供了必要的灵活性。同时,即使一个基类是虚拟的,我们仍然可以通过该基类类型的指针或引用,来操纵派生类的对象。例如,尽管Panda 被设计为虚拟继承层次结构,下面的Panda 基类转换也可以正确执行:

extern void dance( const Bear* );

extern void rummage( const Raccoon* );

extern ostream& operator<<( ostream&, const ZooAnimal& );

int main()

{

Panda yin_yang;

dance( &yin_yang ); // ok

rummage( &yin_yang ); // ok

cout << yin_yang; // ok

// ...

}

如果一个类可以被指定为基类,那么我们就可以将它指定为虚拟基类,而且它可以包含非虚拟基类支持的所有元素。例如,下面是ZooAnimal 类声明:

#include <iostream>

#include <string>

class ZooAnimal;

extern ostream& operator<<( ostream&, const ZooAnimal& );

class ZooAnimal {

public:

ZooAnimal( string name, bool onExhibit, string fam_name )

: _name( name ), _onExhibit( onExhibit), _fam_name( fam_name )

{}

virtual ~ZooAnimal();

virtual ostream& print( ostream& ) const;

string name() const { return _name; };

string family_name() const { return _fam_name; }

// ...

protected:

bool _onExhibit;

string _name;

string _fam_name;

// ...

};

直接派生类实例的声明和实现与非虚拟派生的情形相同,只是要用到关键字virtual例如,下面是Bear 类声明:

class Bear : public virtual ZooAnimal {

public:

enum DanceType {two_left_feet, macarena, fandango, waltz };

Bear( string name, bool onExhibit=true )

: ZooAnimal( name, onExhibit, "Bear" ),_dance( two_left_feet )

{}

virtual ostream& print( ostream& ) const;

void dance( DanceType );

// ...

protected:

DanceType _dance;

// ...

};

类似地,下面是Raccoon 类的声明:

class Raccoon : public virtual ZooAnimal {

public:

Raccoon( string name, bool onExhibit=true )

: ZooAnimal( name, onExhibit, "Raccoon" ),_pettable( false )

{}

virtual ostream& print( ostream&) const;

bool pettable() const { return _pettable; }

void pettable( bool petval ) { _pettable = petval; }

// ...

protected:

bool _pettable;

// ...

};

 

特殊的初始化语义

如果在一个派生类中有一个或多个虚拟基类间接出现,那么它就需要有特殊的初始化语义。稍后我们将看一看前面的Bear Raccoon 类的实现。你能看出由Panda 类派生引起的问题吗?

class Panda : public Bear,public Raccoon, public Endangered {

public:

Panda( string name, bool onExhibit=true );

virtual ostream& print( ostream& ) const;

bool sleeping() const { return _sleeping; }

void sleeping( bool newval ) { _sleeping = newval; }

// ...

protected:

bool _sleeping;

// ...

};

对,问题在于Bear Raccoon 的基类构造函数都提供了一个带有显式实参集合的ZooAnimal 构造函数。更加糟糕的是,在我们的例子中,这个被用作科目名(name的实参不但不相同,而且对Panda 类无效。

在非虚拟派生中,派生类只能显式初始化其直接基类。例如,在ZooAnimal 的非虚拟派生中,Panda 类不能在Panda 成员初始化表中直接调用ZooAnimal 的构造函数。然而,在虚拟派生中,只有Panda 可以直接调用其ZooAnimal 虚拟基类的构造函数。

虚拟基类的初始化变成了最终派生类的责任,这个最终派生类是由每个特定类对象的声明来决定的。例如,我们在声明Bear 类对象时:

Bear winnie( "pooh" );

Bear winnie 对象的最终派生类,它所调用的ZooAnimal 构造函数被执行。当我们写如下语句时:

cout << winnie.family_name();

输出的是:

The family name for pooh is Bear.

类似地,如下声明:

Raccoon meeko( "meeko" );

声明meekoRaccoon类对象的最终派生类时,初始化ZooAnimal 成为Raccoon类的责任。当我们写如下语句时:

cout << meeko.family_name();

输出的是:

The family name for meeko is Raccoon.

现在,当我们声明Panda 类对象时,比如:

Panda yolo( "yolo" );

Panda yolo 类对象的最终派生类,所以初始化ZooAnimal 成为Panda类的责任。

当一个Panda 对象被初始化时,在Raccoon Bear 的构造函数执行过程中,它们对于ZooAnimal构造函数的调用不再被执行;ZooAnimal构造函数被调用时,其实参是在Panda的初始化表中被指定的。下面是具体实现:

Panda::Panda( string name, bool onExhibit=true )

: ZooAnimal( name, onExhibit, "Panda" ),

Bear( name, onExhibit ),

Raccoon( name, onExhibit ),

Endangered(Endangered::environment,Endangered::critical)_sleeping(false)

{}

如果Panda 的构造函数没有显式地为ZooAnimal 构造函数指定实参,则发生下面两个动作之一:调用ZooAnimal 的缺省构造函数,或者,如果没有缺省构造函数,则编译器在编译Panda构造函数的定义时会给出一个错误消息。

当我们写如下语句时:

cout << yolo.family_name();

输出的是:

The family name for yolo is Panda.

PandaBear Raccoon 类都被用作中间派生类而不是最终派生类。作为中间派生类,所有对虚拟基类构造函数的调用都被自动抑制了。如果Panda 又被其他类派生,则Panda也将成为中间派生类,它对ZooAnimal 构造函数的调用也将被自动抑制住。

或许你已经注意到,当Bear Raccoon 类被用作中间派生类时,向Bear Raccoon 构造函数传递的两个实参是不必要的。避免这种不必要的参数传递的解决方案是,提供一个显式的构造函数,用于“当它被作为中间派生类时”的情形。例如,中间类Bear 的构造函数可以修改如下:

class Bear : public virtual ZooAnimal {

public:

// 当作为最终派生类时

Bear( string name, bool onExhibit=true )

: ZooAnimal( name, onExhibit, "Bear" ),_dance( two_left_feet )

{}

// ... rest the same

protected:

// 当作为一个中间派生类时

Bear() : _dance( two_left_feet ) {}

// ... rest the same

};

我们将这个实例指定为protected因为它只希望在后续的派生类中被调用。假设我们已经为Raccoon 提供了类似的缺省构造函数,则可以如下修改Panda 构造函数:

Panda::Panda( string name, bool onExhibit = true )

: ZooAnimal( name, onExhibit, "Panda" ),

Endangered( Endangered::environment,Endangered::critical )

_sleeping( false )

{}

 

构造函数与析构函数顺序

无论虚拟基类出现在继承层次中的哪个位置上,它们都是在非虚拟基类之前被构造。例如,在下面这个有点古怪的TeddyBear 派生类中,有两个虚拟基类:直接的ToyAnimal 实例,以及来自Bear ZooAnimal 实例:

class Character { ... };

class BookCharacter : public Character { ... };

class ToyAnimal { ... };

class TeddyBear : public BookCharacter,public Bear, public virtual ToyAnimal

{ ... };

层次结构如下图所示,这里的虚拟派生用虚箭头表示,而非虚拟派生用实箭头表示:

 

编译器按照直接基类在声明中的顺序,来检查虚拟基类的出现情况。在我们的例子中,BookCharacter 的继承子树首先被检查,然后是Bear最后是ToyAnimal每个子树按深度优先的顺序被检查。即,查找从树根类开始,然后向下移动。对于BookCharacter子树,先检查Character然后是BookCharacter对于Bear 子树而言,则先检查ZooAnimal然后是Bear

在这个查找算法下,TeddyBear 的虚拟基类构造函数的调用顺序是,先ZooAnimal后跟ToyAnimal

一旦调用了虚拟基类的构造函数,则非虚拟基类构造函数就按照声明的顺序被调用:先是BookCharater然后是BearBookCharacter 构造函数执行之前,它的基类Character构造函数先被调用。已知声明:

TeddyBear Paddington;

基类构造函数的调用顺序如下:

ZooAnimal();      // Bear 的虚拟基类

ToyAnimal();      // 直接虚拟基类

Character();      // BookCharacter 的非虚拟基类

BookCharacter(); // 直接非虚拟基类

Bear();            // 直接非虚拟基类

TeddyBear();      // 最终派生类

这里初始化ZooAnimal ToyAnimal TeddyBear 的责任,因为它是Paddington 类对象的最终派生类。

 

虚拟基类成员的可视性

让我们重新定义Bear 类,以提供它自己的onExhibit()成员函数的实例(原来的onExhibit()成员实例从ZooAnimal 继承而来):

bool Bear::onExhibit() { ... }

通过Bear 类对象引用的onExhibit()现在被解析为Bear 的实例:

Bear winnie( "a lover of honey" );

winnie.onExhibit(); // Bear::onExhibit()

通过Raccoon 类对象引用的Raccoon meeko( "a lover of all foods" );

Raccoon meeko( "a lover of all foods" );

meeko.onExhibit(); // ZooAnimal::onExhibit()

派生类Panda 从它的两个基类所继承而来的成员可被分为以下三类:

1 ZooAnimal 虚拟基类实例,如name()family_name()它们没有被Bear Raccoon改写。

2 继承自Raccoon属于ZooAnimal 虚拟基类的onExhibit()实例,以及Bear 定义的、被改写了的onExhibit()实例。

3 继承自ZooAnimal分别被Bear Raccoon 特化了的print()实例。

对于这些继承得到的成员,哪些可以在Panda 类域中被直接地、无二义地访问?在非虚拟派生下,答案是没有,所有非限定修饰的引用都是二义的。在虚拟派生下,第1 项和第2项的所有成员都可以被直接地、无二义地访问。例如,已知Panda 类对象:

Panda spot( "Spottie" );

下面的调用

spot.name();

调用了共享的ZooAnimal 虚拟基类成员函数name()而下面的调用:

spot.onExhibit();

调用了派生的Bear 成员函数onExhibit()

当两个以上的成员实例分别通过不同的派生路径被继承(不但适用于成员函数,也适用于数据成员和联套类型),并且它们都代表了相同的虚拟基类成员时,则不存在二义性,因为它们共亭了该成员的单个实例(第1 项)。如果一个代表虚拟基类的成员,而另一个是后续派生类的成员,则也不会有二义性(特化的派生类实例的优先级高于共享的虚拟基类实例[2 ])。但是,如果它们都代表后续派生类的实例,则直接访问该成员就是二义的。最好的解决办法是在派生类中给出一个改写的实例(第3 项)。

例如,在非虚拟派生下,通过Panda 类对象对onExhibit()的非限定修饰引用就是二义的:

// 错误: 在非虚拟派生下二义

Panda yolo( "a lover of bamboo" );

yolo.onExhibit();

在非虚拟派生下的解析引用过程中,每个继承得到的实例都具有同样的权值,所以未限定修饰的引用将导致编译时刻二义性错误。

在虚拟派生下,对于虚拟基类成员的继承比“该成员后来重新定义的实例”的权值小。继承得到的Bear onExhibit()实例,比通过Raccoon 继承得到的ZooAnimal 实例优先:

// ok: 在虚拟继承下没有二义

// 调用 Bear::onExhibit()

yolo.onExhibit();

如果在同一派生级别上有两个或多个基类重新定义了一个虚拟基类成员,则在派生类中,它们有相同的优先级。例如,如果Raccoon 也定义了一个onExhibit()成员,则Panda 需要用适当的类域操作符来限定修饰每个访问:

bool Panda::onExhibit(){

return Bear::onExhibit() && Raccoon::onExhibit() && ! _sleeping;

}


虚拟继承

  C++使用虚拟继承(Virtual Inheritance),使得派生类如果继承 基类多次,但只有一份基类的拷贝在派生类对象中。
  虚拟继承的语法:  class 派生类: virtual 基类1,virtual 基类2,...,virtual 基类n{
  ...//派生类成员声明  
};
多重继承构造执行顺序
  首先执行 虚基类构造函数,多个虚基类的构造函数按照被继承的顺序构造;
  执行基类的构造函数,多个基类的构造函数按照被继承的顺序构造;
  执行成员对象的构造函数,多个成员对象的构造函数按照申明的顺序构造;
  执行派生类自己的构造函数;
  析构以与构造相反的顺序执行;

虚继承与虚基类的本质


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

    1. 语法
       语法有语言的本身的定义所决定,总体上来说非常的简单,如下:
           struct CSubClass : public virtual CBaseClass {};
       其中可以采用public、protected、private三种不同的继承关键字进行修饰,只要
       确保包含virtual就可以了,这样一来就形成了虚继承体系,同时CBaseClass就成为
       了CSubClass的虚基类了。
       其实并没有那么的简单,如果出现虚继承体系的进一步继承会出现什么样的状况呢?
       如下所示:
            /*
             * 带有数据成员的基类
             */
            struct CBaseClass1
            {
                CBaseClass1( size_t i ) : m_val( i ) {}
            
                size_t m_val;
            };
            /*
             * 虚拟继承体系
             */
            struct CSubClassV1 : public virtual CBaseClass1
            {
                CSubClassV1( size_t i ) : CBaseClass1( i ) {}
            };           
            struct CSubClassV2 : public virtual CBaseClass1
            {
                CSubClassV2( size_t i ) : CBaseClass1( i ) {}
            };           
            struct CDiamondClass1 : public CSubClassV1, public CSubClassV2
            {
                CDiamondClass1( size_t i ) : CBaseClass1( i ), CSubClassV1( i ), CSubClassV2( i ) {}
            };           
            struct CDiamondSubClass1 : public CDiamondClass1
            {
                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 <ctime>
#include <iostream>

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

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

struct CSubClassV2 : public virtual CBaseClass1
{
    CSubClassV2( size_t i ) : CBaseClass1( i ) {}
};

struct CDiamondClass1 : public CSubClassV1, public CSubClassV2
{
    CDiamondClass1( size_t i ) : CBaseClass1( i ), CSubClassV1( i ), CSubClassV2( i ) {}
};

struct CDiamondSubClass1 : public CDiamondClass1
{
    CDiamondSubClass1( size_t i ) : CBaseClass1( i ), CDiamondClass1( i ) {}
};
/*
 * 正常继承体系
 */
struct CSubClassN1 : public CBaseClass1
{
    CSubClassN1( size_t i ) : CBaseClass1( i ) {}
};
struct CSubClassN2 : public CBaseClass1
{
    CSubClassN2( size_t i ) : CBaseClass1( i ) {}
};
struct CMultiClass1 : public CSubClassN1, public CSubClassN2
{
    CMultiClass1( size_t i ) : CSubClassN1( i ), CSubClassN2( i ) {}
};
struct CMultiSubClass1 : public CMultiClass1
{
    CMultiSubClass1( size_t i ) : CMultiClass1( i ) {}
};
/*
 * 不带有数据成员的接口基类
 */
struct CBaseClass2
{
    virtual void func() {};
    virtual ~CBaseClass2() {}
};
/*
 * 虚拟继承体系
 */
// struct CBaseClassX { CBaseClassX() {i1 = i2 = 0xFFFFFFFF;} size_t i1, i2;};
struct CSubClassV3 : public virtual CBaseClass2
{
};
struct CSubClassV4 : public virtual CBaseClass2
{
};
struct CDiamondClass2 : public CSubClassV3, public CSubClassV4
{
};
struct CDiamondSubClass2 : public CDiamondClass2
{
};

/*
 * 正常继承体系
 */
struct CSubClassN3 : public CBaseClass2
{
};
struct CSubClassN4 : public CBaseClass2
{
};
struct CMultiClass2 : public CSubClassN3, public CSubClassN4
{
};
struct CMultiSubClass2 : public CMultiClass2
{
};

/*
 * 内存布局用类声明.
 */
struct CLayoutBase1
{
    CLayoutBase1() : m_val1( 0 ), m_val2( 1 ) {}

    size_t m_val1, m_val2;
};
struct CLayoutBase2
{
    CLayoutBase2() : m_val1( 3 ) {}

    size_t m_val1;
};
struct CLayoutSubClass1 : public virtual CBaseClass1, public CLayoutBase1, public CLayoutBase2
{
    CLayoutSubClass1() : CBaseClass1( 2 ) {}
};


#define MAX_TEST_COUNT 1000 * 1000 * 16
#define TIME_ELAPSE() ( std::clock() - start * 1.0 ) / CLOCKS_PER_SEC

int main( int argc, char *argv[] )
{
    /*
     * 类体系中的尺寸.
     */
    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;

    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;

    int *ptr = (int*)lsc;
    std::cout << "vbc in CLayoutSubClass1 is " << *(int*)ptr[3] << std::endl;

    delete lsc;

    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;

    ptr = (int*)scv1;
    std::cout << "vbc in CSubClassV1 is " << *(int*)ptr[0] << std::endl;

    delete scv1;

    /*
     * 性能测试
     */
    std::cout << "================================ Performance ================================" << std::endl;
    double times[4];
    size_t idx = 0;

    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<CDiamondClass1*>( ptr1 );

    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<CMultiClass1*>( ptr2 );

    std::cout << "CSubClassV1::ptr1->m_val " << times[0] << " s" << std::endl;
    std::cout << "CSubClassN1::ptr2->m_val " << times[1] << " s" << std::endl;

    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倍左右,符合汇编语言输出文件中的汇编语句的安排。
       

Feedback

# re: 虚继承与虚基类的本质  回复  

2007-06-12 12:25 by chemz
http://www.cppblog.com/chemz/archive/2007/05/31/25189.html文章中jazz提到的那段代码为何编译出错的原因在这篇文章中可以找到。
根据文章中的语义小节,虚基类的子对象必须要在每一个子类中都包含初始化语句所以,仅仅有Usable是虚基类的友元是不行的,还必须将D作为虚基类的友元。如下:
class Usable;
class D;

class Usable_lock {
friend class Usable;
friend class D;
private:
Usable_lock() {}
Usable_lock(const Usable_lock&) {}
};

class Usable : public virtual Usable_lock {
// ...
public:
Usable();
Usable(char*);
// ...
};

Usable a;

class DD : public Usable { };

DD dd;

# re: 虚继承与虚基类的本质  回复  更多评论  

2008-10-16 18:16 by frank.sunny
语法语义上的解释很连贯,我能看懂
但是模型和性能方面,可能我功力还不够吧

总得来说,让我收获不小

# re: 虚继承与虚基类的本质  回复  更多评论  

2009-02-24 17:29 by 哈子猪
求vbc in CLayoutSubClass1 is和vbc in CSubClassV1时 代码:
int *ptr = (int*)lsc;
std::cout << "vbc in CLayoutSubClass1 is " << *(int*)ptr[3] << std::endl;


ptr = (int*)scv1;
std::cout << "vbc in CSubClassV1 is " << *(int*)ptr[0] << std::endl;
没能不懂,特别是为啥一个是1 一个是3 ,请大师指点

# re: 虚继承与虚基类的本质  回复  更多评论  



另外的两篇文章,都比这篇强很多,大家可以看一下

http://www.cnblogs.com/itech/archive/2009/03/01/1399996.html

http://archive.cnblogs.com/a/2043794/

# re: 虚继承与虚基类的本质  回复  更多评论  

2012-09-24 17:01 by 81300687
1. 由于虚继承引入的间接性指针所以导致了虚继承类的尺寸会增加4个字节;
2. 由Layout输出可以看出,虚基类子对象被放在了对象的尾部(偏移为16),并且vbc
指针必须紧紧的接在虚基类子对象的前面,所以vbc指针所指向的内容为“偏移 - 4”;
************************************************************
文章没有考虑字节对齐的问题, 而敲死了尺寸就是增加4个字节, 可以试试把成员变量改成double类型试试,然后可以不同的pack(n)指令, 看看布局是如何变化的~~
一两句话讲不清楚~~~ 但是要精确到字节的话,就必须考虑字节对齐的原因。不能随意敲死它就是增加几个字节。

# re: 虚继承与虚基类的本质[未登录]  回复  更多评论  

2012-10-12 15:53 by Benson
virtual 的语义我认为是“允许重叠”,因此是“虚”的(就像在实物上盖了一块有实物影像的玻璃,最终看来只有一个实物),从而避免二义性的问题。例如基类成员函数 f(),如果不加virtual修饰,子类如果有相同名字的成员函数 f()就会产生二义性,所以编译是不能通过的。同样,对与基类,在多重继承时可能会出现两个基类,因此需要加上virtual来使它们“重叠”在一起,避免二义性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值