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

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

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

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

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

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

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

关于虚继承详细的内存分布情况

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

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

我们新建一个名叫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;
};

我们可以看到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的内存布局:

                 

虚继承的内存分布情况

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

/**
    虚继承(虚基类)
*/
 
#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;
};

不同点在与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;
}

得到结果为:

补充:

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 0;
}

C++虚继承的实现原理、内存分布、作用相关推荐

  1. 为什么需要虚继承,虚继承的实现原理

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

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

    2019独角兽企业重金招聘Python工程师标准>>> 原创作品,转载请标明:http://blog.csdn.net/xiejingfa/article/details/48028 ...

  3. 菱形继承,多继承,虚继承、虚表的内存结构全面剖析(逆向分析基础)

    // 声明:以下代码均在Win32_Sp3   VC6.0_DEBUG版中调试通过.. 在逆向还原代码的时候,必须得掌握了菱形继承,多继承,虚继承虚函数的内存虚表结构.所以,这篇文章献给正在学习C++ ...

  4. 多重继承和虚继承的内存布局

    这篇文章主要讲解虚继承的C++对象内存分布问题,从中也引出了dynamic_cast和static_cast本质区别.虚函数表的格式等一些大部分C++程序员都似是而非的概念.原文见这里 (By Eds ...

  5. 继承详解(虚继承实现原理)

    继承的概念及定义 概念: ​ 继承机制是面向对象程序设计为了提高代码复用率的一种手段,它可以保持原类特性的基础上进行拓展,简单来说继承是类层次的复用. 接下来我们来看一个简单的继承 class Per ...

  6. C++继承机制(三)——多继承、菱形继承、虚继承原理

    目录: C++继承机制(一)--基本语法.三种继承方式.继承哪些数据 C++继承机制(二)--继承中的构造和析构顺序.继承同名成员的处理方式 C++继承机制(三)--多继承.菱形继承.虚继承原理 本篇 ...

  7. C++虚继承下的内存模型(二)

    对于虚继承,恰恰和普通继承相反,大部分编译器会把基类成员变量放在派生类成员变量的后面,这样随着继承层级的增加,基类成员变量的偏移就会改变,就得通过其他方案来计算偏移量. 下面我们来一步一步地分析虚继承 ...

  8. [C++基础]虚继承实现原理

    在C++中,我们会遇到virtual这个关键字,但是它有两种含义:虚函数和虚继承,它们两个是完全无相关的两个概念. 什么是虚继承 虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类, ...

  9. C++ 面向对象(一)继承:继承、对象切割、菱形继承、虚继承、继承与组合

    目录 继承 继承的概念 继承方式 基类与派生类的赋值转换 作用域与隐藏 派生类的默认成员函数 友元与静态成员 友元 静态成员 多继承 菱形继承 虚继承 继承和组合 什么是组合 如何选择组合和继承 继承 ...

最新文章

  1. 人工智能写散文之躲进你的心里记录温暖的你
  2. MacroMedia FreeHand中文版
  3. 【双100%解法】剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
  4. andorid 查看OpenCv Mat的Debug信息
  5. Python BaseHTTPServer 模块解析
  6. Android应用程序打包时,出现错误:XXX is not translated in af (Afrikaans), am (Amharic), ar (Arabic).....(...
  7. CUDA C编程权威指南 第八章 多GPU编程
  8. 统计标识符个数C语言,C语言文件-统计其中的用户自定义标识符号的个数,并列出用户自定义的标识符号...
  9. 科研生活:避免碌碌无为的感觉
  10. Microsoft .NET FrameWork 4安装失败解决方法
  11. 日本专利如何检索 昭57-192247
  12. MySQL处理生意参谋_生意参谋低点击率怎么处理?
  13. python基础:sys模块
  14. Java获取本机外网ip地址的方法
  15. 1916 Problem C 合唱队形
  16. openlayers 地图上加图标_OpenLayers学习笔记中级篇(四、地图图标操作)
  17. 优秀的人都在读的10本好书!
  18. 【小程序源码】简洁界面清爽让人非常舒服的一款短视频去印自带接口
  19. java 包别名_Java包导入别名
  20. YOLOV5在deepstream6.1.1上应用

热门文章

  1. python recv_[Python]关于socket.recv()的非阻塞用法
  2. 2.2 基本算法之递归和自调用函数 1751 分解因数 python
  3. STM32的学习记录--2.WiFi模块的使用
  4. php 定时缓存,php定时清理缓存文件的简单示例
  5. Windows破解逆向-CrackMe_1实例(使用IDA Pro修改静态区资源)
  6. Python笔记-安装python虚拟环境及配置opencv及通过opencv识别颜色
  7. Qt文档阅读笔记-QPointer的概念及实例(并发多线程实例)
  8. Qt|OpenGL学习笔记-绕X轴、Y轴、以及XY轴旋转的四边体
  9. Java高级语法笔记-文件及目录操作
  10. audio 上一首 下一首 自定义样式_被 iPhone 吹爆的最香功能,安卓也终于安排上了...