C++ 虚继承实现原理(虚基类表指针与虚基类表)
虚继承和虚函数是完全无相关的两个概念。
虚继承是解决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 0;
- }
二: 从内存布局看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 = 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;
- }
得到结果为:
C++ 虚继承实现原理(虚基类表指针与虚基类表)的更多相关文章
- C++多重继承分析——《虚继承实现原理(虚继承和虚函数)》
博客转载:https://blog.csdn.net/longlovefilm/article/details/80558879 一.虚继承和虚函数概念区分 虚继承和虚函数是完全无相关的两个概念. 虚 ...
- 虚函数列表: 取出方法 // 虚函数工作原理和(虚)继承类的内存占用大小计算 32位机器上 sizeof(void *) // 4byte
#include <iostream> using namespace std; class A { public: A(){} virtual void geta(){ cout < ...
- C++虚函数工作原理
一.虚函数的工作原理 虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数.典型情况下,这一信息具有一种被称为 vptr(virtual table poi ...
- 3.10 C++虚基类 虚继承
参考:http://www.weixueyuan.net/view/6367.html 总结: 本例即为典型的菱形继承结构,类A中的成员变量及成员函数继承到类D中均会产生两份,这样的命名冲突非常的棘手 ...
- 【整理】C++虚函数及其继承、虚继承类大小
参考文章: http://blog.chinaunix.net/uid-25132162-id-1564955.html http://blog.csdn.net/haoel/article/deta ...
- C++ 虚基类表指针字节对齐
下面博客转载自别人的,我也是被这个问题坑了快两天了,关于各种虚基类,虚继承,虚函数以及数据成员等引发的一系列内存对齐的问题再次详细描述 先看下面这片代码.在这里我使用了一个空类K,不要被这个东西所迷惑 ...
- C++虚函数、虚继承
http://blog.csdn.net/hackbuteer1/article/details/7883531 转载请标明出处,原文地址:http://blog.csdn.net/hackbutee ...
- C++中的虚继承 & 重载隐藏覆盖的讨论
虚继承这个东西用的真不多.估计也就是面试的时候会用到吧.. 可以看这篇文章:<关于C++中的虚拟继承的一些总结> 虚拟基类是为解决多重继承而出现的. 如:类D继承自类B1.B2,而类B1. ...
- C++ Primer 有感(多重继承与虚继承)
1.多重继承的构造次序:基类构造函数按照基类构造函数在类派生列表中的出现次序调用,构造函数调用次序既不受构造函数初始化列表中出现的基类的影响,也不受基类在构造函数初始化列表中的出现次序的影响.2.在单 ...
随机推荐
- H5点击不同按钮跳转显示不同分页
预期效果(页面1): 点击后显示对应的内容(页面2): HTML(页面1): 添加 onclick 跟 data-index <!-- 3我的订单 --> <div ...
- 小程序swiper高度自适应解决方案
scroll-view 里面继续套一个 scroll-view ,设置纵向允许滚动 <swiper class="swiper"> <swiper-item> ...
- IIS部署vue项目页面刷新404,url重写问题解决办法
这里需要用到URL重写工具 --URL Rewrite(默认没有,需要自己下载安装) 如果IIS上默认有安装Web平台安装程序,我们可以使用平台自动安装URL Rewrite重写工具,打开IIS在管理 ...
- 基于消息队列(RabbitMQ)实现延迟任务
一.序言 延迟任务应用广泛,延迟任务典型应用场景有订单超时自动取消:支付回调重试.其中订单超时取消具有幂等性属性,无需考虑重复消费问题:支付回调重试需要考虑重复消费问题. 延迟任务具有如下特点:在未来 ...
- JVM上篇:JVM与Java体系结构
JVM笔记 JVM传言 Java不是最强大的语言,但是JVM是最强大的虚拟机 虚拟机分类 系统虚拟机 类似VMware,就属于系统虚拟机,它提供了一个可运行完整操作系统的平台 程序虚拟机 Java虚拟 ...
- C++雾中风景18:C++20, 从concept开始
转眼间,C++20的标准已经发布快两年了.不少C++的开源项目也已经将标准升级到最新的C++20了,笔者也开启了新标准的学习历程了.所以借这系列的博文,记录下笔者学习新标准的一些心得与吐槽~~ 作为C ...
- TCC分布式事框架务详解
之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下.很多朋友看了还是不知道分布式事务到底怎么回事,在项目里到底如何使用. 所以这篇文章,就用大白话+手工绘图,并结合一 ...
- CSAPP-Lab04 Architecture Lab 深入解析
穷且益坚,不坠青云之志. 实验概览 Arch Lab 实验分为三部分.在 A 部分中,需要我们写一些简单的Y86-64程序,从而熟悉Y86-64工具的使用:在 B 部分中,我们要用一个新的指令来扩展S ...
- tp5 全选,全不选 ,ajax批量删除
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 关于malloc源码中的bin_at宏定义的个人见解
0x01:简介 在堆中的内存申请和释放中,为了减少使用系统调用函数对内存操作,malloc_state(分配区)结构中使用了fastbinsY数组和bins数组.当chunk被free后,bins链会 ...