C++ 虚继承实现原理(虚基类表指针与虚基类表)

虚继承和虚函数是彻底无相关的两个概念。ios

虚继承是解决C++多重继承问题的一种手段,从不一样途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题,一般能够将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,可是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。函数


虚继承能够解决多种继承前面提到的两个问题:工具

虚继承底层实现原理与编译器相关,通常经过虚基类指针和虚基类表实现,每一个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(须要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并非不在子类里面了);当虚继承的子类被当作父类继承时,虚基类指针也会被继承。布局


实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;经过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份一样的拷贝,节省了存储空间。spa


在这里咱们能够对比虚函数的实现原理:他们有类似之处,都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)。.net

虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。命令行

虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。3d

 

此篇博客有关于虚继承详细的内存分布状况 指针

http://blog.csdn.net/xiejingfa/article/details/48028491code


补充:

一、D继承了B,C也就继承了两个虚基类指针

二、虚基类表存储的是,虚基类相对直接继承类的偏移(D并不是是虚基类的直接继承类,B,C才是)

#include<iostream>  
    using namespace std;  
      
    class A  //大小为4  
    {  
    public:  
        int a;  
    };  
    class B :virtual public A  //大小为12,变量a,b共8字节,虚基类表指针4  
    {  
    public:  
        int b;  
    };  
    class C :virtual public A //与B同样12  
    {  
    public:  
        int c;  
    };  
    class D :public B, public C //24,变量a,b,c,d共16,B的虚基类指针4,C的虚基类指针  
    {  
    public:  
        int d;  
    };  
      
    int main()  
    {  
        A a;  
        B b;  
        C c;  
        D d;  
        cout << sizeof(a) << endl;  
        cout << sizeof(b) << endl;  
        cout << sizeof(c) << endl;  
        cout << sizeof(d) << endl;  
        system("pause");  
        return 0;  
    }

二: 从内存布局看C++虚继承的实现原理

准备工做

一、VS2012使用命令行选项查看对象的内存布局

微软的Visual Studio提供给用户显示C++对象在内存中的布局的选项:/d1reportSingleClassLayout。使用方法很简单,直接在[工具(T)]选项下找到“Visual Studio命令提示(C)”后点击便可。切换到cpp文件所在目录下输入以下的命令便可

      c1 [filename].cpp /d1reportSingleClassLayout[className]

其中[filename].cpp就是咱们想要查看的class所在的cpp文件,[className]指咱们想要查看的class的类名。(下面举例说明...)


二、查看普通多继承子类的内存布局

既然咱们今天讲的是虚基类和虚继承,咱们就先用上面介绍的命令提示工具查看一下普通多继承子类的内存布局,能够跟后文虚继承子类的内存布局状况加以比较。

咱们新建一个名叫NormalInheritance的cpp文件,输入一下内容。

/** 
        普通继承(没有使用虚基类) 
    */  
      
    // 基类A  
    class A  
    {  
    public:  
        int dataA;  
    };  
      
    class B : public A  
    {  
    public:  
        int dataB;  
    };  
      
    class C : public A  
    {  
    public:  
        int dataC;  
    };  
      
    class D : public B, public C  
    {  
    public:  
        int dataD;  
    };
上面是一个简单的多继承例子,咱们启动Visual Studio命令提示功能,切换到NormalInheritance.cpp文件所在目录,输入一下命令:

c1  NormalInheritance.cpp /d1reportSingleClassLayoutD

咱们能够看到class D的内存布局以下:



从类D的内存布局能够看到A派生出B和C,B和C中分别包含A的成员。再由B和C派生出D,此时D包含了B和C的成员。这样D中就总共出现了2个A成员。你们注意到左边的几个数字,这几个数字代表了D中各成员在D中排列的起始地址,D中的五个成员变量(B::dataA、dataB、C::dataA、dataC、dataD)各占用4个字节,sizeof(D) = 20。

为了跟后文加以比较,咱们再来看看B和C的内存布局:


                                  


虚继承的内存分布状况

上面咱们看到了普通多继承子类的内存分布状况,下面咱们进入主题,来看看典型的菱形虚继承子类的内存分布状况。

咱们新建一个名叫VirtualInheritance的cpp文件,输入一下内容:


/** 
        虚继承(虚基类) 
    */  
      
    #include <iostream>  
      
    // 基类A  
    class A  
    {  
    public:  
        int dataA;  
    };  
      
    class B : virtual public A  
    {  
    public:  
        int dataB;  
    };  
      
    class C : virtual public A  
    {  
    public:  
        int dataC;  
    };  
      
    class D : public B, public C  
    {  
    public:  
        int dataD;  
    };
VirtualInheritance.cpp和NormalInheritance.cpp的不一样点在与C和C继承A时使用了virtual关键字,也就是虚继承。一样,咱们看看B、C、D类的内存布局状况:


                                                                                                                                                                    

                                   


咱们能够看到,菱形继承体系中的子类在内存布局上和普通多继承体系中的子类类有很大的不同。对于类B和C,sizeof的值变成了12,除了包含类A的成员变量dataA外还多了一个指针vbptr,类D除了继承B、C各自的成员变量dataB、dataA和本身的成员变量外,还有两个分别属于B、C的指针。

那么类D对象的内存布局就变成以下的样子:


vbptr:继承自父类B中的指针

int dataB:继承自父类B的成员变量

vbptr:继承自父类C的指针

int dataC:继承自父类C的成员变量

int dataD:D本身的成员变量

int A:继承自父类A的成员变量


显然,虚继承之因此可以实如今多重派生子类中只保存一份共有基类的拷贝,关键在于vbptr指针。那vbptr到底指的是什么?又是如何实现虚继承的呢?其实上面的类D内存布局图中已经给出答案:

                                                             

实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚表(virtual table),虚表中记录了vbptr与本类的偏移地址;第二项是vbptr到共有基类元素之间的偏移量。在这个例子中,类B中的vbptr指向了虚表D::$vbtable@B@,虚表代表公共基类A的成员变量dataA距离类B开始处的位移为20,这样就找到了成员变量dataA,而虚继承也不用像普通多继承那样维持着公共基类的两份一样的拷贝,节省了存储空间。


为了进一步肯定上面的想法是否正确,咱们能够写一个简单的程序加以验证:

int main()  
    {  
        D* d = new D;  
        d->dataA = 10;  
        d->dataB = 100;  
        d->dataC = 1000;  
        d->dataD = 10000;  
      
        B* b = d; // 转化为基类B  
        C* c = d; // 转化为基类C  
        A* fromB = (B*) d;  
        A* fromC = (C*) d;  
      
        std::cout << "d address    : " << d << std::endl;  
        std::cout << "b address    : " << b << std::endl;  
        std::cout << "c address    : " << c << std::endl;  
        std::cout << "fromB address: " << fromB << std::endl;  
        std::cout << "fromC address: " << fromC << std::endl;  
        std::cout << std::endl;  
      
        std::cout << "vbptr address: " << (int*)d << std::endl;  
        std::cout << "    [0] => " << *(int*)(*(int*)d) << std::endl;  
        std::cout << "    [1] => " << *(((int*)(*(int*)d)) + 1)<< std::endl; // 偏移量20  
        std::cout << "dataB value  : " << *((int*)d + 1) << std::endl;  
        std::cout << "vbptr address: " << ((int*)d + 2) << std::endl;  
        std::cout << "    [0] => " << *(int*)(*((int*)d + 2)) << std::endl;  
        std::cout << "    [1] => " << *((int*)(*((int*)d + 2)) + 1) << std::endl; // 偏移量12  
        std::cout << "dataC value  : " << *((int*)d + 3) << std::endl;  
        std::cout << "dataD value  : " << *((int*)d + 4) << std::endl;  
        std::cout << "dataA value  : " << *((int*)d + 5) << std::endl;  
    }
获得结果为: