【C++拾遗】 从内存布局看C++虚继承的实现原理

原创做品,转载请标明:http://blog.csdn.net/xiejingfa/article/details/48028491ios

准备工做工具

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

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

      c1 [filename].cpp /d1reportSingleClassLayout[className]命令行

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

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

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

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


/**
    普通继承(没有使用虚基类)
*/
 
// 基类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;
}

获得结果为:

相关文章
相关标签/搜索