c/c
内存首地址为1000h-凯发ag旗舰厅登录网址下载
前言
在叙述c 虚继承之前,我先给大家抛出一个问题。例如现在有4个类,分别是class a, class b, class c, class d。它们的关系如下图。
如上如所示,class b和class c都继承class a;class d又继承class b和c。
我们用代码展示如下。
class a {public: void fun() { std::cout << "class a fun" << std::endl; } int data; };class b : public a { };class c : public a { };class d : public b, public c {};如上面代码所示,当class b和class c都继承了class a。由单继承的原理我们可知,类b和类c中都继承了类a的成员变量data。所以类b的对象大小和类c的对象大小都为4。那么当类d
继承类b和类c,按照单继承的原理,派生类是要继承基类的public成员变量,因此类d的对象中会有来自类b的成员变量data,和类c的成员变量data。
因此,当实例化一个class d的时候,其对象的大小应该为8。其内存分布图为
但是呢,如果我们需要对data进行操作,我们可以直接写成下面这样吗?
d d; //实例化对象d.data = 100; //?可以这样吗?//我们用编译器编译后,报出了如下的错误。//error: request for member ‘data’ is ambiguous//表明我们使用的data是不明确的,模棱两可的因为对象d中有2个data,如果我们只调用data,编译器是不知道我们到底要使用哪一个data。所以我们可以使用::(域限定符号)来表明我们要使用哪一个data。比如
d d;d.b::data = 100;d.c::data = 200;貌似,我们好像已经解决了这个“二义性”的问题,但是深究这个二义性,其实更深层次的是因为公共基类a发生了两次实例化问题。
虚继承
一
虚继承的实现是在继承的时候加上关键字virtual。它的作用是只会在最后的派生类中将虚基类的构造函数调用一次,忽略虚基类的中间派生类对虚继承的构造函数的调用,从而保证虚基类的数据成员不会被初始化多次。
所以,通过虚继承,我们也能很好的解决多重继承下公共基类的多份拷贝问题.
例如
class a {public: void fun() { std::cout << "class a fun" << std::endl; } int data; };class b : virtual public a { };class c : virtual public a { };class d : public b, public c { };如上面代码所示,class b和class c对class a使用了虚继承。那么当class d继承class b和c的时候,只能实例化一份class a的对象。所以d的对象中只有一份data数据。其内存分布如下
所以可以直接对类d的对象进行如下操作
d d;d.data = 100;std::cout << "data = " << data << std::endl; //100注意
1. 虽然是虚基类,但是依然可以使用基类的指针或者引用来指向派生类的对象。
2. 虚继承只是解决了多重继承中公共基类被实例化多次的问题
二
虚继承下派生类的对象内存分布分析。
这一节是延续我之前写的《c 虚函数继承之对象内存分布》这篇文章,其实也是本篇文章的重点。
虚继承的派生类的内存布局与普通继承有很多不同,主要体现如下:
1. 虚继承的子类,如果本身定义了虚函数,则编译器会为其生成一个虚函数指针(vptr)及虚函数表。该vptr在对象内存的最前面。这里跟普通的继承就有区别了:普通的继承如果基类有虚函数,那么派生类会在基类的虚表之后继续扩展基类的虚表,与基类共用一个虚函数指针。
2. 虚继承的子类会单独保留基类的虚函数指针vptr和虚表。
总而言之,通过虚继承的子类会生成一个虚基类指针(vbptr)。虚基类表指针总在虚函数表指针之后。所以,如果一个类它是虚继承的子类并且它有自身的虚函数,那么虚基类指针便在虚函数指针之后,有一个指针大小的偏移量。如果该类没有虚函数,那么它的虚基类表指针在对象内存最前面。
虚基类表:虚基类指针指向的是虚基类表,虚基类表中也是存储有多条数据,不过与虚函数表不同的是,虚函数表中每个元素存储的是虚函数的地址,虚基类表中存储的是偏移值。第一个元素存储的是虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由上面的分析我们可知,这个值要么是0(该类本身没有虚函数),要么是-4(该类有虚函数)。虚基类表的第二个,第三个元素记录依次为该类的最左虚继承父类,次左虚继承父类...的内存地址相对于虚基类表指针的偏移值。我们可以通过下图加深理解。
接下来我们通过几个案例,来阐述一下虚继承后,派生类中的内存分布情况。
2.1 单虚拟继承
接下来我们通过几个案例,来阐述一下虚继承后,派生类中的内存分布情况。2.1 单虚拟继承如上述代码所示,child虚继承类base。base类中有2个虚函数,child类中也有自身的虚函数,并且重写了base的fun1函数。
在64位系统下,sizeof(child)=32。具体的内存分布图如下。
由图可知。
1. child类的首地址存放的是child类自身的虚表指针,指向的是存放自身虚函数地址的虚函数表(vtable)。
2. 接着存放的是虚基类指针,虚基类指针指向的是虚基类表,虚基类表中存放的是偏移值。第一个元素为虚基类指针相对于内存首地址的偏移值,第二个元素为虚继承的基类的内存与虚基类指针的偏移值。
3. 接着存放child类成员变量data
4. 接着存放基类base的虚表指针。虚表指针指向的虚表中,由于child类重写了fun1,所以虚函数表的信息也有所改变。
5. 最后存放基类base的成员变量data
2.2 多虚继承
class base1 {public: virtual void base1_fun1() { std::cout << "base1::base1_fun1" << std::endl; } virtual void base1_fun2() { std::cout << "base1::base1_fun2" << std::endl; } int base1_data;};class base2 {public: virtual void base2_fun1() { std::cout << "base2::base2_fun1" << std::endl; } virtual void base2_fun2() { std::cout << "base2::base2_fun2" << std::endl; } int base2_data; };class child : virtual public base1, virtual public base2 {public: void base1_fun1() override { std::cout << "child::base1_fun1" << std::endl; } void base2_fun1() override { std::cout << "child::base2_fun1" << std::endl; } virtual child_fun3() { std::cout << "child::child_fun3" << std::endl; } int child_data;};如上述代码所示,类child虚继承类base1和类base2。并且类child重写了类base1和类base2的部分虚函数。
具体的内存分布图如下。不考虑内存对齐,只阐述各个元素的位置。
由上图可知,
1. child类实例化一个对象后,其对象内存首地址存放的是指向child类自身虚函数表的虚表指针
2. 接着放入虚基类指针,指向的是虚基类表。虚基类表中存放的是偏移值。第一个元素是虚基类指针与对象内存首地址之间的偏移值;第二个元素存放的是对象中base1(child先继承的基类)的首地址与虚基类指针的偏移值;第三个元素存放的是对象内存中base2(child第二继承的基类)的首地址与虚基类指针的偏移值
3. 接着存放的是子类的成员变量
4. 接着放入先继承的base1的虚表指针
5. 接着放入base1的成员变量
6. 接着放入后继承的base2的虚表指针
7. 最后放入base2的成员变量
2.3 菱形继承
菱形继承其实就是本篇文章最前面提的案例。
class a {public: virtual void fun1() { std::cout << "a::fun1" << std::endl; } int a_data;};class b : virtual public a {public: virtual void fun2() { std::cout << "b::fun2" << std::endl; } int b_data;};class c : virtual public a {public: virtual void fun3() { std::cout << "c::fun3" << std::endl; } int c_data;};class d : public b, public c {public: virtual void fun4() { std::cout << "d::fun4" << std::endl; } int d_data;};如代码所示,类b和类c虚继承类a,类d公有实继承类b和类c,那么类d的对象内存分布图如下
由上图可知,因为类b和类c都虚继承了类a,由上面的案例可知,类b和类c中都有一个vbptr(虚基类指针),然后由于类d继承了类b和c,所以推断类d中有2个vbptr,分别来自类b和c。
并且由于类d是实继承,不同于虚继承,所以根据之前学习的继承原理,类d中的虚函数地址应该放在第一个继承的基类的虚函数表之后(扩展)。并且由于类a是被虚继承的,类a的数据在类d中只有一份,由类b和类c的虚基类指针,通过偏移量获取类a的数据。
总结
以上主要从菱形继承会产生的问题,到引出虚继承,以及简单从单虚继承,多虚继承,菱形虚继承这几个方面简单阐述了虚继承的实现,功能,以及虚继承后的类对象内存分布情况。
当然,本文只是简单分析了内存分布情况,并没有实际考虑内存对齐等情况,因此具体问题需要具体分析。
总之,c 是一门高深的语言,我们都不断的在学习的过程中,望共勉之。
上面如有概念错误之处,烦请指正,谢谢!
最后,喜欢的小伙伴麻烦点个赞和关注,以后定期会发一些更多的文章,谢谢!
总结
以上是凯发ag旗舰厅登录网址下载为你收集整理的内存首地址为1000h_c 虚继承,菱形继承,内存分布的全部内容,希望文章能够帮你解决所遇到的问题。
如果觉得凯发ag旗舰厅登录网址下载网站内容还不错,欢迎将凯发ag旗舰厅登录网址下载推荐给好友。
- 上一篇: telnet本机端口不通原因_【acad
- 下一篇: python使用matplotlib绘图