博客转载:https://blog.csdn.net/longlovefilm/article/details/80558879

一、虚继承和虚函数概念区分

虚继承和虚函数是完全无相关的两个概念。

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

虚继承可以解决多种继承前面提到的两个问题:

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

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

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

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

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

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

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

补充:

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

2、虚基类表存储的是,虚基类相对直接继承类的偏移(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 ;
}
  

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

准备工作

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

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

c1 [filename].cpp /d1reportSingleClassLayout[className]

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

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

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

我们新建一个名叫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 = ;
d->dataB = ;
d->dataC = ;
d->dataD = ; 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)) + )<< std::endl; // 偏移量20
std::cout << "dataB value : " << *((int*)d + ) << std::endl;
std::cout << "vbptr address: " << ((int*)d + ) << std::endl;
std::cout << " [0] => " << *(int*)(*((int*)d + )) << std::endl;
std::cout << " [1] => " << *((int*)(*((int*)d + )) + ) << std::endl; // 偏移量12
std::cout << "dataC value : " << *((int*)d + ) << std::endl;
std::cout << "dataD value : " << *((int*)d + ) << std::endl;
std::cout << "dataA value : " << *((int*)d + ) << std::endl;
}

得到结果为:

C++多重继承分析——《虚继承实现原理(虚继承和虚函数)》的更多相关文章

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

    虚继承和虚函数是完全无相关的两个概念. 虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝.这将存在两个问题:其一,浪费存储空间:第二,存在二义性问题,通常可 ...

  2. c++ 继承类强制转换时的虚函数表工作原理

    本文通过简单例子说明子类之间发生强制转换时虚函数如何调用,旨在对c++继承中的虚函数表的作用机制有更深入的理解. #include<iostream> using namespace st ...

  3. c++继承汇总(单继承、多继承、虚继承、菱形继承)

    多重继承中,一个基类可以在派生层次中出现多次,如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多分同名成员.C++提供虚基类的方法使得在 ...

  4. C++虚函数表原理

    C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指 向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有“多种形态”,这是一种泛型技 ...

  5. OO的片段,继承与组合,继承的优点与目的,虚机制在构造函数中不工作

    摘自C++编程思想: ------------------------------ 继承与组合:接口的重用 ------------------------------- 继承和组合都允许由已存在的类 ...

  6. C++多态中虚函数表合并与继承问题

    多态: C++的多态是通过一张虚函数表(Virtual Table)来实现的,简称为 V-Table.在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承.覆写的问题,保证其真实反应实际的函数. ...

  7. C++类有继承时,析构函数必须为虚函数

    C++类有继承时,析构函数必须为虚函数.如果不是虚函数,则使用时可能存在内在泄漏的问题. 假设我们有这样一种继承关系: 如果我们以这种方式创建对象: SubClass* pObj = new SubC ...

  8. Atitit.实现继承的原理and方法java javascript .net c# php ...

    Atitit.实现继承的原理and方法java javascript .net c# php ... 1. 实现继承的问题 1 2. 如何拷贝基类方法?采用prototype原型方式,通过冒充对象 1 ...

  9. 简单的自绘CListBox,重载虚MeasureItem和DrawItem这两个虚函数

    [cpp] view plain copy //例如CNewListBox继承自CListBox,重载虚MeasureItem和DrawItem这两个虚函数,代码如下: void CNewListBo ...

随机推荐

  1. python3基础之“函数(2)”

    1.def:定义一个函数 def f(x): return x+1 #返回函数值 a=f(2) print(a) >>3 def even_odd(x): if x%2==0: " ...

  2. python3 中的try 异常调试与 raise 异常抛出

    一.什么是异常? 异常即是一个事件,该事件会在程序执行过程中发生,影响了程序的正常执行. 一般情况下,在Python无法正常处理程序时就会发生一个异常. 异常是Python对象,表示一个错误. 当Py ...

  3. Linux下系统调用的组成与实现

    主要包括三个部分:(1)唯一的系统调用号(System Call Number):(2)系统调用表中相应的表项,即具体的函数地址:(3)对应的具体函数,即系统调用函数体. 以getpid()POSIX ...

  4. CLR、CIL、CTS、CLS、CLI、BCL和FCL,JIT,IL,GC

    如果要想深入学习.NET平台,那么标题中的这些关键字对你来说并不陌生,这些名词构成了.NET庞大的生态系统,为了宏观认识.NET平台,学些.NET架构体系,针对一些常用常用名词的理解是很有必要的,未必 ...

  5. ViewBag---MVC3中 ViewBag、ViewData和TempData的使用和差别-------与ViewBag+Hashtable应用例子

    ViewBag 在MVC3開始.视图数据能够通过ViewBag属性訪问.在MVC2中则是使用ViewData.MVC3中保留了ViewData的使用.ViewBag 是动态类型(dynamic),Vi ...

  6. Python3正则匹配re.split,re.finditer及re.findall函数用法详解

    这篇文章主要介绍了Python3正则匹配re.split,re.finditer及re.findall函数用法,结合实例形式详细分析了正则匹配re.split,re.finditer及re.finda ...

  7. Flink原理(七)——动态表(Dynamic tables)

    前言 本文是结合Flink官网,个人理解所得,若是有误欢迎留言指出,谢谢!文中图皆来自官网(链接[1]). 本文将随着下面这个问题展开,针对该问题更为生动的解释可以参见金竹老师的分享(链接[2]). ...

  8. Linux命令——rsync

    参考:Rsync (Remote Sync): 10 Practical Examples of Rsync Command in Linux How to Sync Files/Directorie ...

  9. HDU-2204-Eddy's爱好-容斥求n以内有多少个数形如M^K

    HDU-2204-Eddy's爱好-容斥求n以内有多少个数形如M^K [Problem Description] 略 [Solution] 对于一个指数\(k\),找到一个最大的\(m\)使得\(m^ ...

  10. 剑指offer:二维数组的查找

    题目描述 在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序.请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数 ...