欢迎访问 生活随笔!

ag凯发k8国际

当前位置: ag凯发k8国际 > 编程语言 > c/c >内容正文

c/c

内存首地址为1000h-ag凯发k8国际

发布时间:2024/10/14 c/c 33 豆豆
ag凯发k8国际 收集整理的这篇文章主要介绍了 内存首地址为1000h_c 虚继承,菱形继承,内存分布 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

前言

在叙述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凯发k8国际为你收集整理的内存首地址为1000h_c 虚继承,菱形继承,内存分布的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得ag凯发k8国际网站内容还不错,欢迎将ag凯发k8国际推荐给好友。

网站地图