虚继承

虚继承解决了菱形继承中最派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同,主要体现在:

  • 虚继承的派生类,如果定义了新的虚函数,则编译器为其生成一个虚函数指针(vptr)以及一张虚函数表。该vptr位于对象内存最前面。(非虚继承时,派生类新的虚函数直接扩展在基类虚函数表的下面。)
  • 虚继承的派生类有单独的虚函数表,基类也有单独的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界。
  • 虚继承的派生类对象中,含有四字节的虚基表指针。

在C++对象模型中,虚继承而来的派生类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后,因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由上面的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。虚基类表的第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值。我们通过一张图来更好地理解。

代码

class B{public:int ib;public:B(int i = 1) :ib(i) {}virtual void f() { cout << "B::f()" ; }virtual void Bf() { cout << "B::Bf()" ; }};class B1 : virtual public B{public:int ib1;public:B1(int i = 100) :ib1(i) {}virtual void f() { cout << "B1::f()" ; }virtual void f1() { cout << "B1::f1()" ; }virtual void Bf1() { cout << "B1::Bf1()" ; }};

对象模型

代码演示

typedef void(*Fun)(void);
int main()
{B1 a;cout << "B1对象内存大小为:" << sizeof(a) << endl;//取得B1的虚函数表cout << "[0]B1::vptr";cout << "\t地址:" << (int*)(&a) << endl;//输出虚表B1::vptr中的函数for (int i = 0; i < 2; ++i){cout << "  [" << i << "]";Fun fun1 = (Fun) * ((int*)*(int*)(&a) + i);fun1();cout << "\t地址:\t" << *((int*)*(int*)(&a) + i) << endl;}//[1]cout << "[1]vbptr ";cout << "\t地址:" << (int*)(&a) + 1 << endl;  //虚表指针的地址//输出虚基类指针条目所指的内容for (int i = 0; i < 2; i++){cout << "  [" << i << "]";cout << *(int*)((int*)*((int*)(&a) + 1) + i);cout << endl;}//[2]cout << "[2]B1::ib1=" << *(int*)((int*)(&a) + 2);cout << "\t地址:" << (int*)(&a) + 2;cout << endl;//[3]cout << "[3]值=" << *(int*)((int*)(&a) + 3);cout << "\t\t地址:" << (int*)(&a) + 3;cout << endl;//[4]cout << "[4]B::vptr";cout << "\t地址:" << (int*)(&a) + 4 << endl;//输出B::vptr中的虚函数for (int i = 0; i < 2; ++i){cout << "  [" << i << "]";Fun fun1 = (Fun) * ((int*)*((int*)(&a) + 4) + i);fun1();cout << "\t地址:\t" << *((int*)*((int*)(&a) + 4) + i) << endl;}//[5]cout << "[5]B::ib=" << *(int*)((int*)(&a) + 5);cout << "\t地址: " << (int*)(&a) + 5;cout << endl;
}

结果

这个结果与我们的C++对象模型图完全符合。这时我们可以来分析一下虚表指针的第二个条目值12的具体来源了,回忆上文讲到的:

第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值。

在我们的例子中,也就是B类实例内存地址相对于vbptr的偏移值,也即是:[4]-[1]的偏移值,结果即为12,从地址上也可以计算出来:00F8FDE4-00F8FDD8结果的十进制数是12。现在,我们对虚基类表的构成应该有了一个更好的理解。

菱形继承

class B{public:int ib;public:B(int i = 1) :ib(i) {}virtual void f() { cout << "B::f()" << endl; }virtual void Bf() { cout << "B::Bf()" << endl; }};class B1 : virtual public B{public:int ib1;public:B1(int i = 100) :ib1(i) {}virtual void f() { cout << "B1::f()" << endl; }virtual void f1() { cout << "B1::f1()" << endl; }virtual void Bf1() { cout << "B1::Bf1()" << endl; }};class B2 : virtual public B{public:int ib2;public:B2(int i = 1000) :ib2(i) {}virtual void f() { cout << "B2::f()" << endl; }virtual void f2() { cout << "B2::f2()" << endl; }virtual void Bf2() { cout << "B2::Bf2()" << endl; }};class D : public B1, public B2{public:int id;public:D(int i = 10000) :id(i) {}virtual void f() { cout << "D::f()" << endl; }virtual void f1() { cout << "D::f1()" << endl; }virtual void f2() { cout << "D::f2()" << endl; }virtual void Df() { cout << "D::Df()" << endl; }};

菱形虚拟继承下,最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点:

  • 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)。
  • D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。
  • 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。
  • 共同虚基类B的内容放到了派生类对象D内存布局的最后。

代码演示

typedef void(*Fun)(void);
int main()
{D d;cout << "D对象内存大小为:" << sizeof(d) << endl;//取得B1的虚函数表cout << "[0]B1::vptr";cout << "\t地址:" << (int*)(&d) << endl;//输出虚表B1::vptr中的函数for (int i = 0; i < 3; ++i){cout << "  [" << i << "]";Fun fun1 = (Fun) * ((int*)*(int*)(&d) + i);fun1();cout << "\t地址:\t" << *((int*)*(int*)(&d) + i) << endl;}//[1]cout << "[1]B1::vbptr ";cout << "\t地址:" << (int*)(&d) + 1 << endl;  //虚表指针的地址//输出虚基类指针条目所指的内容for (int i = 0; i < 2; i++){cout << "  [" << i << "]";cout << *(int*)((int*)*((int*)(&d) + 1) + i);cout << endl;}//[2]cout << "[2]B1::ib1=" << *(int*)((int*)(&d) + 2);cout << "\t地址:" << (int*)(&d) + 2;cout << endl;//[3]cout << "[3]B2::vptr";cout << "\t地址:" << (int*)(&d) + 3 << endl;//输出B2::vptr中的虚函数for (int i = 0; i < 2; ++i){cout << "  [" << i << "]";Fun fun1 = (Fun) * ((int*)*((int*)(&d) + 3) + i);fun1();cout << "\t地址:\t" << *((int*)*((int*)(&d) + 3) + i) << endl;}//[4]cout << "[4]B2::vbptr ";cout << "\t地址:" << (int*)(&d) + 4 << endl;  //虚表指针的地址//输出虚基类指针条目所指的内容for (int i = 0; i < 2; i++){cout << "  [" << i << "]";cout << *(int*)((int*)*((int*)(&d) + 4) + i);cout << endl;}//[5]cout << "[5]B2::ib2=" << *(int*)((int*)(&d) + 5);cout << "\t地址: " << (int*)(&d) + 5;cout << endl;//[6]cout << "[6]D::id=" << *(int*)((int*)(&d) + 6);cout << "\t地址: " << (int*)(&d) + 6;cout << endl;//[7]cout << "[7]值=" << *(int*)((int*)(&d) + 7);cout << "\t\t地址:" << (int*)(&d) + 7;cout << endl;//间接父类//[8]cout << "[8]B::vptr";cout << "\t地址:" << (int*)(&d) + 8 << endl;//输出B::vptr中的虚函数for (int i = 0; i < 2; ++i){cout << "  [" << i << "]";Fun fun1 = (Fun) * ((int*)*((int*)(&d) + 8) + i);fun1();cout << "\t地址:\t" << *((int*)*((int*)(&d) + 8) + i) << endl;}//[9]cout << "[9]B::id=" << *(int*)((int*)(&d) + 9);cout << "\t地址: " << (int*)(&d) + 9;cout << endl;getchar();
}

数据成员如何访问(直接取址)

跟实际对象模型相关联,根据对象起始地址+偏移量取得。

函数成员如何访问(间接取址)

跟实际对象模型相关联,普通函数(nonstatic、static)根据编译、链接的结果直接获取函数地址;如果是虚函数根据对象模型,取出对于虚函数地址,然后在虚函数表中查找函数地址。

多态如何实现?

多态(Polymorphisn)在C++中是通过虚函数实现的。如果类中有虚函数,编译器就会自动生成一个虚函数表,对象中包含一个指向虚函数表的指针。能够实现多态的关键在于:虚函数是允许被派生类重写的,在虚函数表中,派生类函数对覆盖(override)基类函数。除此之外,还必须通过指针或引用调用方法才行,将派生类对象赋给基类对象。

为什么析构函数设为虚函数是必要的

析构函数应当都是虚函数,除非明确该类不做基类(不被其他类继承)。基类的析构函数声明为虚函数,这样做是为了确保释放派生对象时,按照正确的顺序调用析构函数。

如果析构函数不定义为虚函数,那么派生类就不会重写基类的析构函数,在有多态行为的时候,派生类的析构函数不会被调用到(有内存泄漏的风险!)。例如,通过new一个派生类对象,赋给基类指针,然后delete基类指针,缺少了派生类的析构函数调用。把析构函数声明为虚函数,调用就正常了。

c++对象模型05:虚继承内存布局相关推荐

  1. C++虚继承内存布局===写得很牛!推荐

    http://www.cnblogs.com/ltang/archive/2010/10/25/1861137.html

  2. Cpp 对象模型探索 / 虚继承带虚函数的基类的子类的内存布局

    源码 class Base { public:Base() {}virtual void func() {}int bi_; };class Son:virtual public Base { pub ...

  3. [C++对象模型][9]虚继承与虚函数表

    一 虚继承 1) 代码: Code #include <iostream> using namespace std; class B { public:     int i;     vi ...

  4. C++对象模型探索 / 子类的内存布局

    一.栗子 #include <iostream>class Father { public:Father(){std::cout << "I am father,th ...

  5. Cpp 对象模型探索 / 单一继承的类的内存布局

    目录 1.父类和子类都没有虚函数 2.父类有虚函数.子类没有虚函数 3.父类没有虚函数,子类有虚函数 4.父类和子类都有虚函数 5.总结 #include <iostream> class ...

  6. C++虚继承(二) --- C++ 对象的内存布局(上)(陈皓)

    C++ 对象的内存布局(上) 陈皓 http://blog.csdn.net/haoel 点击这里查看下篇>>> 前言 07年12月,我写了一篇<C++虚函数表解析>的文 ...

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

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

  8. C++ 多继承和虚继承的内存布局

    原文链接:https://www.oschina.net/translate/cpp-virtual-inheritance 警告. 本文有点技术难度,需要读者了解C++和一些汇编语言知识. 在本文中 ...

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

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

最新文章

  1. 谈一谈算法工程师的落地能力
  2. 原理竟然是这!2021年字节跳动74道高级程序员面试
  3. 服务器里这么修改404页面,网站404页面怎么做
  4. poj 2151 Check the difficulty of problems
  5. yota3墨水屏设置_使用ESP32驱动电子墨水屏
  6. linux c 进程编程,linux c/c++ 编程之-----进程操作
  7. linux系统编程:IO读写过程的原子性操作实验
  8. python中调用万年历_python 打印万年历
  9. 【洛谷 - P1507 】NASA的食物计划(二维费用背包,dp)
  10. 苏宁易购回复深交所关注函:深国际和鲲鹏资本非一致行动人
  11. 彻底理解Cisco NAT内部的一些事
  12. python画兔子代码_Python基础练习实例11(兔子问题)
  13. 计算机桌面窗口背景原始设置,如何设置和更改桌面背景? -电脑资料
  14. NT1000无线测温系统 方维监测
  15. 采集网易云上面的MV保存方法
  16. php msvcr110,安装PHP时计算机错误丢失了msvcr110.dll
  17. mysql怎么限制输入男女_excel表格中如何限制只输入男女
  18. 群体智能中的联邦学习算法综述
  19. 金山快盘 linux,WPS移动版5.5发布 支持金山快盘双向读写
  20. unity3d创建简单地形遇到的那些坑

热门文章

  1. 市场上有什么vr软件制作哪个好?酷雷曼VR软件制作如何
  2. 给大家分享一篇 Python:渗透测试开源项目「源码值得精读」
  3. 计算机主机和cpu的区别,双CPU电脑跟普通的有什么区别
  4. Vue3 学习笔记 —— 破坏式更新、自定义指令 directive
  5. (转)左手坐标系和右手坐标系
  6. 我这一年的技术学习概况
  7. Robertamodel
  8. 完美程序员的十种特质(转)
  9. java如何开发生产派工报工_工序报工
  10. mac 系统使用chromeheadless报错,无法打开“chromedriver”