C++继承详解之三——菱形继承+虚继承内存对象模型详解vbptr(1)
2016-04-12 14:47
1046 查看
在我个人学习继承的过程中,在网上查阅了许多资料,这些资料中有关菱形继承的知识都是加了虚函数的,也就是涉及了多态的问题,而我在那个时候并没有学习到多态这一块,所以看很多资料都是云里雾里的,那么这篇文章我想以我自己学习过程中的经验,由简到较难的先分析以下菱形继承,让初学者先对这个问题有一点概念,在后面会由浅入深的继续剖析。
本篇文章不会涉及到多态也就是虚函数的菱形继承,在后面的文章更新中,我会慢慢把这些内容都加进去。
菱形继承(也叫钻石继承)是下面的这种情况:
对应代码如下:
运行结果为:
我们希望上面的代码中D类所对应的对象模型如下:
而实际上上面代码中D类所对应的模型为
菱形继承会造成派生类的数据冗余,比如上例就有D类中包含两个int b这种情况发生。
为了解决菱形继承数据冗余的问题,下面我要引入虚继承的概念。
1.虚继承
虚继承 是面向对象编程中的一种技术,是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类。
虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。
下图可以看出虚基类和非虚基类在多重继承中的区别
那么为什么要引入虚拟继承呢?
我们已经剖析了一般非虚基类的多重继承得到的派生类的对象模型,那么看看下面的代码会输出什么
编译出错,输出
编译器报错为:不明确的b,即编译器无法分清到底b是继承自C1的还是继承自C2的。
解决上面由于菱形继承而产生二义性与数据冗余的问题,需要用到虚继承。
虚继承的提出就是为了解决多重继承时,可能会保存两份副本的问题,也就是说用了虚继承就只保留了一份副本,但是这个副本是被多重继承的基类所共享的,该怎么实现这个机制呢?
下面我来一步一步的分析这个问题:
1.类中不加数据成员
看下面的代码:
输出为:
我们分析一下结果:
首先,基类中除了构造函数和析构函数没有其他成员了,所以
有的初学者可能会问为什么为1,首先类在内存中的存储是这样的:
如果有一个类B
那么在内存中模型如下图
所以成员函数是单独存储,并且所有类对象公用的。
那么有人可能要说那sizeof(B)为什么不为0,那是因为编译器要给对象一个地址,就需要区分开所有的类对象,1只是一个占位符,表示这个对象存在,并且让编译器给这个对象分配地址。
现在sizeof(B)的问题解决,下面看C1与C2
由于C1与C2都是虚拟继承,故会在C1,C2内存起始处存放一个vbptr,为指向虚基类表的指针。
那么这个指针vbptr指向什么呢?
我们在main函数中生成一个C1类对象c1
在内存中查看c1究竟存了什么
由上图我们可以看出,c1占了四个字节,存了一个指针变量,指针变量的内容就是c1的vbptr指向的虚基类表的地址。
那我们去c1.vbptr指向的虚基类表中查看下究竟存了什么。
可以看到,这个虚基类表有八个字节,分别存的为0和4。
那么0和4代表的都是什么呢?
虚基类表存放的为两个偏移地址,分别为0和4。
其中0表示c1对象地址相对与存放vptr指针的地址的偏移量
可以用&c1->vbptr_c1表示。
其中vptr指的是指向虚表的指针,而虚表是定义了虚函数后才有的,由于我们这里没有定义虚函数,当然也就没有vptr指针,所以偏移量为0.
8表示c1对象中基类对象部分相对于存放vbptr指针的地址的偏移量
可以用&c1(B)-&vbpt表示,其中&c1(B)表示对象c1中基类B部分的地址。
c2的内存布局与c1一样,因为C1,C2都是虚继承自B基类,且C1,C2都没有加数据成员。
现在大家都对
没有什么疑虑了吧。
总结一下,因为C1,C2是虚继承自基类B,所以编译器会给C1,C2中生成一个指针vbptr指向一个虚基类表,即指针vbptr的值是虚基类表的地址。
而这个虚基类表中存储的是偏移量。
这个表中分两部分,第一部分存储的是对象相对于存放vptr指针的偏移量,可以用&(对象名)->vbptr_(对象名)来表示。对c1对象来说,可以用&c1->vbprt_c1来表示。
vptr指针是指向虚表的指针,而只有在类中定义了虚函数才会有虚表,因为我们这个例子中没有定义虚函数,所以没有vptr指针,所以第一部分偏移量均为0。
表的第二部分存储的是对象中基类对象部分相对于存放vbptr指针的地址的偏移量,我们知道在本例中基类对象与指针偏移量就是指针的大小。
在内存中看d究竟存了什么
如上图所示,d的内存中存了两个指针,我们进入指针存放的地址看里面究竟是什么:
如上图所示,d中存放了两个虚基类指针,每个虚基类表中存储了偏移量。
说了这么多,还是太抽象了。
现在看一下内存布局:
2.类中加数据成员
上面我们剖析了不加数据成员的菱形继承,下面剖析一下加数据成员的,这样可以更清晰的看出内存布局
这次的输出为:
这次我们再剖析下各个类的输出大小
首先C1,C2都是虚继承自基类B的,所以我就一起剖析了。
首先B占四个字节没有问题,因为B类中有int b数据成员,所以B类占四个字节。
那么C1,C2是虚继承自B类的,所以C1,C2的内存布局是相似的,在这里我只剖析一下C1。
我们在C1类中加一个Fun成员函数,为了更清楚的看到内存布局
在main函数中生成对象c1,那么在内存中的c1是什么样呢?
我们通过vbptr指针进入c1的虚基类表中
由上面两图我们可以画出c1的内存布局
C2跟C1一样。
所以
现在来看看D类的内存布局
我们进入内存中看d
可以看出,前四个字节是vbptr指针,然后是c1类,+另一个vbptr指针+c2类+D类数据成员d+基类B这样的结构
我们进入第一个vbptr指针中看
可得出偏移量分别为0(因为没有虚函数),14
再进入第二个vbptr指针中
可以看出偏移量分别为0(因为没有虚函数),12
好了,到这里我们可以画出D类的内存布局了
所以,
这就是不带虚函数的菱形继承,关于带虚函数的菱形继承因为涉及到多态的知识,我放在后面会专门再写一篇文章的。
如果有问题,欢迎评论私信指正。
有更加深入的想法也欢迎讨论。
本篇文章不会涉及到多态也就是虚函数的菱形继承,在后面的文章更新中,我会慢慢把这些内容都加进去。
菱形继承(也叫钻石继承)是下面的这种情况:
对应代码如下:
#include <iostream> using namespace std; class B { public: B() { cout << "B" << endl; } ~B() { cout << "~B()" << endl; } private: int b; }; class C1 :public B { public: C1() { cout << "C1()" << endl; } ~C1() { cout << "~C1()" << endl; } private: int c1; }; class C2 :public B { public: C2() { cout << "C2()" << endl; } ~C2() { cout << "~C2()" << endl; } private: int c2; }; class D :public C1, public C2 { public: D() { cout << "D()" << endl; } ~D() { cout << "~D()" << endl; } private: int d; }; int main() { cout << sizeof(B) << endl; cout << sizeof(C1) << endl; cout << sizeof(C2) << endl; cout << sizeof(D) << endl; return 0; }
运行结果为:
我们希望上面的代码中D类所对应的对象模型如下:
而实际上上面代码中D类所对应的模型为
菱形继承会造成派生类的数据冗余,比如上例就有D类中包含两个int b这种情况发生。
为了解决菱形继承数据冗余的问题,下面我要引入虚继承的概念。
1.虚继承
虚继承 是面向对象编程中的一种技术,是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类。
虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。
下图可以看出虚基类和非虚基类在多重继承中的区别
那么为什么要引入虚拟继承呢?
我们已经剖析了一般非虚基类的多重继承得到的派生类的对象模型,那么看看下面的代码会输出什么
#include <iostream> using namespace std; class B { public: B() { cout << "B" << endl; } ~B() { cout << "~B()" << endl; } int b; }; class C1 :public B { public: C1() { cout << "C1()" << endl; } ~C1() { cout << "~C1()" << endl; } private: int c1; }; class C2 :public B { public: C2() { cout << "C2()" << endl; } ~C2() { cout << "~C2()" << endl; } private: int c2; }; class D :public C1, public C2 { public: D() { cout << "D()" << endl; } ~D() { cout << "~D()" << endl; } void FunTest() { b = 10; } private: int d; }; int main() { D d; d.FunTest(); return 0; }
编译出错,输出
Error 1 error C2385: ambiguous access of 'b' e:\demo\继承\blog\project1\project1\source.cpp 58 1 Project1 2 IntelliSense: "D::b" is ambiguous e:\DEMO\继承\blog\Project1\Project1\Source.cpp 58 3 Project1
编译器报错为:不明确的b,即编译器无法分清到底b是继承自C1的还是继承自C2的。
解决上面由于菱形继承而产生二义性与数据冗余的问题,需要用到虚继承。
虚继承的提出就是为了解决多重继承时,可能会保存两份副本的问题,也就是说用了虚继承就只保留了一份副本,但是这个副本是被多重继承的基类所共享的,该怎么实现这个机制呢?
下面我来一步一步的分析这个问题:
1.类中不加数据成员
看下面的代码:
#include <iostream> using namespace std; class B //基类 { public: B() { cout << "B" << endl; } ~B() { cout << "~B()" << endl; } }; class C1 :virtual public B { public: C1() { cout << "C1()" << endl; } ~C1() { cout << "~C1()" << endl; } }; class C2 :virtual public B { public: C2() { cout << "C2()" << endl; } ~C2() { cout << "~C2()" << endl; } }; class D :public C1, public C2 { public: D() { cout << "D()" << endl; } ~D() { cout << "~D()" << endl; } }; int main() { cout << sizeof(B) << endl; cout << sizeof(C1) << endl; cout << sizeof(C2) << endl; cout << sizeof(D) << endl; return 0; }
输出为:
我们分析一下结果:
class B //基类 { public: B() { cout << "B" << endl; } ~B() { cout << "~B()" << endl; } };
首先,基类中除了构造函数和析构函数没有其他成员了,所以
sizeof(B) = 1;
有的初学者可能会问为什么为1,首先类在内存中的存储是这样的:
如果有一个类B
class B { public: int b; void fun(); }; int Test() { B b1,b2,b3; }
那么在内存中模型如下图
所以成员函数是单独存储,并且所有类对象公用的。
那么有人可能要说那sizeof(B)为什么不为0,那是因为编译器要给对象一个地址,就需要区分开所有的类对象,1只是一个占位符,表示这个对象存在,并且让编译器给这个对象分配地址。
现在sizeof(B)的问题解决,下面看C1与C2
class C1 :virtual public B { public: C1() { cout << "C1()" << endl; } ~C1() { cout << "~C1()" << endl; } }; class C2 :virtual public B { public: C2() { cout << "C2()" << endl; } ~C2() { cout << "~C2()" << endl; } };
由于C1与C2都是虚拟继承,故会在C1,C2内存起始处存放一个vbptr,为指向虚基类表的指针。
那么这个指针vbptr指向什么呢?
我们在main函数中生成一个C1类对象c1
int main() { C1 c1; return 0; }
在内存中查看c1究竟存了什么
由上图我们可以看出,c1占了四个字节,存了一个指针变量,指针变量的内容就是c1的vbptr指向的虚基类表的地址。
那我们去c1.vbptr指向的虚基类表中查看下究竟存了什么。
可以看到,这个虚基类表有八个字节,分别存的为0和4。
那么0和4代表的都是什么呢?
虚基类表存放的为两个偏移地址,分别为0和4。
其中0表示c1对象地址相对与存放vptr指针的地址的偏移量
可以用&c1->vbptr_c1表示。
其中vptr指的是指向虚表的指针,而虚表是定义了虚函数后才有的,由于我们这里没有定义虚函数,当然也就没有vptr指针,所以偏移量为0.
8表示c1对象中基类对象部分相对于存放vbptr指针的地址的偏移量
可以用&c1(B)-&vbpt表示,其中&c1(B)表示对象c1中基类B部分的地址。
c2的内存布局与c1一样,因为C1,C2都是虚继承自B基类,且C1,C2都没有加数据成员。
现在大家都对
sizeof(C1) = 4; sizeof(C2) = 4;
没有什么疑虑了吧。
总结一下,因为C1,C2是虚继承自基类B,所以编译器会给C1,C2中生成一个指针vbptr指向一个虚基类表,即指针vbptr的值是虚基类表的地址。
而这个虚基类表中存储的是偏移量。
这个表中分两部分,第一部分存储的是对象相对于存放vptr指针的偏移量,可以用&(对象名)->vbptr_(对象名)来表示。对c1对象来说,可以用&c1->vbprt_c1来表示。
vptr指针是指向虚表的指针,而只有在类中定义了虚函数才会有虚表,因为我们这个例子中没有定义虚函数,所以没有vptr指针,所以第一部分偏移量均为0。
表的第二部分存储的是对象中基类对象部分相对于存放vbptr指针的地址的偏移量,我们知道在本例中基类对象与指针偏移量就是指针的大小。
在内存中看d究竟存了什么
如上图所示,d的内存中存了两个指针,我们进入指针存放的地址看里面究竟是什么:
如上图所示,d中存放了两个虚基类指针,每个虚基类表中存储了偏移量。
说了这么多,还是太抽象了。
现在看一下内存布局:
2.类中加数据成员
上面我们剖析了不加数据成员的菱形继承,下面剖析一下加数据成员的,这样可以更清晰的看出内存布局
#include <iostream> using namespace std; class B { public: B() { cout << "B" << endl; } ~B() { cout << "~B()" << endl; } int b; }; class C1 :virtual public B { public: C1() { cout << "C1()" << endl; } ~C1() { cout << "~C1()" << endl; } int c1; }; class C2 :virtual public B { public: C2() { cout << "C2()" << endl; } ~C2() { cout << "~C2()" << endl; } int c2; }; class D :public C1, public C2 { public: D() { cout << "D()" << endl; } ~D() { cout << "~D()" << endl; } void fun() { b = 0; c1 = 1; c2 = 2; d = 3; } int d; }; int main() { cout << sizeof(B) << endl; cout << sizeof(C1) << endl; cout << sizeof(C2) << endl; cout << sizeof(D) << endl; D d; d.fun(); return 0; }
这次的输出为:
这次我们再剖析下各个类的输出大小
class C1 :virtual public B { public: C1() { cout << "C1()" << endl; } ~C1() { cout << "~C1()" << endl; } int c1; };
首先C1,C2都是虚继承自基类B的,所以我就一起剖析了。
首先B占四个字节没有问题,因为B类中有int b数据成员,所以B类占四个字节。
那么C1,C2是虚继承自B类的,所以C1,C2的内存布局是相似的,在这里我只剖析一下C1。
我们在C1类中加一个Fun成员函数,为了更清楚的看到内存布局
class C1 :virtual public B { public: C1() { cout << "C1()" << endl; } ~C1() { cout << "~C1()" << endl; } void Fun() { b = 5; c1 = 6; } int c1; }; int main() { C1 c1; c1.Fun(); return 0; }
在main函数中生成对象c1,那么在内存中的c1是什么样呢?
我们通过vbptr指针进入c1的虚基类表中
由上面两图我们可以画出c1的内存布局
C2跟C1一样。
所以
sizeof(C1) == 12; sizeof(C2) == 12;
现在来看看D类的内存布局
class D :public C1, public C2 { public: D() { cout << "D()" << endl; } ~D() { cout << "~D()" << endl; } void fun()//fun()函数主要帮助我们看D类的内存布局 { b = 0;//基类数据成员 c1 = 1;//C1类数据成员 c2 = 2;//C2类数据成员 d = 3;//D类自己的数据成员 } int d; };
我们进入内存中看d
可以看出,前四个字节是vbptr指针,然后是c1类,+另一个vbptr指针+c2类+D类数据成员d+基类B这样的结构
我们进入第一个vbptr指针中看
可得出偏移量分别为0(因为没有虚函数),14
再进入第二个vbptr指针中
可以看出偏移量分别为0(因为没有虚函数),12
好了,到这里我们可以画出D类的内存布局了
所以,
sizeof(D) == 24;
这就是不带虚函数的菱形继承,关于带虚函数的菱形继承因为涉及到多态的知识,我放在后面会专门再写一篇文章的。
如果有问题,欢迎评论私信指正。
有更加深入的想法也欢迎讨论。
相关文章推荐
- 如何正确的通过 C++ Primer 学习 C++?
- C/C++编程规范
- 组合模式(composite)C++实现
- c++上机实验---3
- 与概率相关的算法题C++解法(附证明过程)
- c++ typedefy用法
- 在C++中调用DLL中的函数
- C++第三次实验—友元类
- C++ Builder中导入Excel表格数据
- php C++扩展的开发
- C++访问控制
- 腾讯2016春季校园实习招聘技术岗初试(一面)问题汇总(CC++后台)
- 腾讯2016春季校园实习招聘技术岗初试(一面)问题汇总(CC++后台)
- C++中链表的插入和删除
- C++中经常使用到宏
- C++第3次作业
- c++作业3
- c++作业3
- 大型分布式C++框架《三:序列化与反序列化》
- C++作业3