虚继承---虚基类


转自出处


虚继承就是虚基类的使用;
引入虚基类的目的是为了解决类继承过程中产生的二义性问题;这种二义性问题常见于具有菱形继承关系的类中;
比如:有四个类:A、B、C、D;它们之间的继承关系是:B继承A,C继承A,D继承B和C;这就形成了一个菱形的继承关系;具有这种继承关系的图叫做有向无环图;
那么类D就有两条继承路径:D-->B-->A和D-->C-->A;而类A是派生类D的两条继承路径上的公共基类,那么这个公共基类就会在派生类D的对象中产生多个基类子对象;这个时候在引用派生类D的对象时,就会产生明显的二义性;要解决这个二义性,就必须将这个基类设定为虚基类;

虚基类的定义格式如下:
class <派生类>: virtual <继承方式> <基类名>{};

例如:
class A:
{
  public:
    void fun();
  protected:
    int a;
};

class B: virtual public A
{
  protected:
    int b;
};

class C: virtual pulic A
{
  protected:
    int c;
};

class D: public B, public C
{
  public:
    int g();
  protected:
    int d;
};

这样的话,不同继承路径上的虚基类子对象在派生类中被合并成一个子对象了,这便是虚基类的作用,这样就可以消除合并之前出现的二义性问题;这时在派生类D的对象中只存在一个类A的子对象;

C++中的二义性检查是在访问权限或类型检查之前进行的;

注意:
引进虚基类之后,派生类(子类)的对象中只存在一个虚基类的子对象;当一个类拥有虚基类的时候,编译系统会为这个类的对象定义一个指针成员,并让它指向虚基类的子对象;该指针被称为虚基类指针;这个概念与虚函数表指针不同;在内存中,一般情况下,虚基类子对象在派生类对象中是放置在派生类对象所占内存块的尾部,这一点由编译器来决定的;

虚基类的构造函数:
为了初始化虚基类的子对象,派生类的构造函数要调用基类的构造函数.由于派生类的对象中只有一个虚基类子对象,那么就必须要保证虚基类的子对象只能被初始化一次,也就是说,虚基类的构造函数只能被调用一次;由于继承的层次可能会很深,C++规定:把真正创建对象时所指定的类称为是最派生类,虚基类子对象是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的;如果一个派生类有一个直接或间接的虚基类,那么派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用,如果没有列出,则表示使用该虚基类的缺省构造函数来初始化派生类对象中的虚基类子对象;
从虚基类直接或间接继承的派生类的成员初始化列表中必须列出对该虚基类构造函数的调用,但是只有真正用于创建对象的那个最派生类的构造函数才会真正调用虚基类的构造函数,而该派生类的基类中所列出的对这个虚基类的构造函数的调用在实际执行中被忽略,这样就保证对虚基类的子对象只初始化一次;
C++又规定:在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,则虚基类的构造函数先于非虚基类的构造函数被执行;也就是说,执行虚基类构造函数的优先级要高于执行非虚基类构造函数的优先级;
在需要使用虚基类的场合,由于每一个继承类都必须包含初始化语句,而这些初始化语句仅仅只在最底层子类(最派生类)中才实际调用;这样就会使得某些上层子类得到的虚基类子对象的状态不是自己所期望的(因为自己的初始化语句被压制了/跳过了),所以,一般建议不要在虚基类中包含任何数据成员(不要有状态),只可以作为接口类来提供;
虚基类实例地址 = 派生类的vbptr + 派生类的vbptr到虚基类实例地址的偏移量;

菱形继承关系的类图:
虚继承---虚基类_第1张图片

虚继承---虚基类_第2张图片

虚继承---虚基类 - 哥哥 - 哥哥

虚继承---虚基类_第3张图片虚继承---虚基类_第4张图片虚继承---虚基类_第5张图片虚继承---虚基类_第6张图片虚继承---虚基类_第7张图片


你可能感兴趣的:(C++/C,class,编译器,c,fun)