【C++】虚函数和虚继承的内存分布情况

    xiaoxiao2021-03-25  232

    最近做题的时候经常遇到虚函数和虚继承的内存分布情况问题,自己也有点生疏了,所以,赶紧在这里回忆补充一下!


    先从一个类的内存分布情况开始看起:

    环境:VS2012

    class A { int a; public: A() :a(1) {} void fun() { cout<<"A"<<endl; } };

    按照我的理解,内存中只有一个int a; 这个类的大小为 4;下面写个测试:

    A a; cout<<sizof(a)<<endl;

    先通过调试看内存情况:

    sizeof(a) 的结果:

    结果和预想的一样,当然这只是复习的一个热身;

    将fun 变为虚函数:

    virtual void fun() { cout<<"A"<<endl; }

    先通过调试看内存情况:

    查看 sizeof(a)的大小:

    我们可以看到,在类中加入虚函数之后,实例之后的对象会有一个虚函数指针,指向一个虚函数表,而虚函数表中存放的是虚函数地址!


    下面我们来一个B类继承A类,看看内存的分布:

    class B: public A { int b; public: B() :b(2) {} void fun() { cout<<"B"<<endl; } };

    B继承A,并且有一个成员 b,重写了A的fun;

    测试:

    B b; cout<<"sizeof(b) = "<<sizeof(b)<<endl;

    调试查看b的内存布局:

    内存布局分析: 我们可以看到先存放的是A 的成员 a, 然后存放的才是 B的成员 b;

    sizeof(b) 的结果:

    此时我们再让A的fun为虚函数,看看 B继承 A 之后b的内存分布:

    sizeof(b) 的 大小:

    分析: 此时b的内存布局是:先是一个虚表指针,然后是 a , b;


    我们再看看一个类分别继承 A 和 B的情况:此时是这样的情况

    class C: public A, public B { int c; public: C() :c(3) {} void fun() { cout<<"C"<<endl; } };

    C类先继承 A 再继承 B 类, 注意:此时我们B类没有继承 A 类,并且没有虚函数

    测试

    C c; cout<<"sizeof(c) = "<<sizeof(c)<<endl;

    调试查看内存:

    查看 sizeof(c) 的结果:

    此时将 A 和 B 的fun设为虚函数,看看 c 的内存布局:

    sizeof(c)的结果

    分析: 内存布局,首先是一个虚表指针,和A的一致,然后是a的成员,接着是B的虚表指针,B的数据成员, 最后是C的数据成员;


    感觉情况好多,后面的sizeof的结果就不截图了;

    到这里,我们可以对含有虚函数的类的对象的内存布局做一个总结:

    首先,内存的开始一定是一个虚表指针,指向一个虚表,如果含有继承,则和第一个继承的基类的虚表指针一致,然后先存放基类的数据成员,再存放自己的数据成员;还有一点,那就是如果子类中对基类的虚函数进行了重写,那么会在虚表中覆盖掉基类虚函数的地址!


    下面我们要看的是多重继承的内存分布情况,这里就只看一种:菱形继承;

    什么是菱形继承?

    上面我们举过一个例子, c 分别继承 A 和 B; 如果我让 A 和 B 分别再继承一个基类 Base 的话,这就叫菱形继承:

    大概逻辑关系如下: 没有虚函数

    class Base { int base; public: Base() :base(0) {} }; class A:public Base { int a; public: A() :a(1) {} void fun() { cout<<"A"<<endl; } }; class B : public Base { int b; public: B() :b(2) {} void fun() { cout<<"B"<<endl; } }; class C: public A, public B { int c; public: C() :c(3) {} void fun() { cout<<"C"<<endl; } };

    那么此时如果有一个 C 的对象 c 的话,c的内存布局应该是怎么样的呢?

    我们看看 c 的内存布局:

    意料之中的布局,但是有一个问题,c 从A继承了一个 Base 的 base,又从B继承了一个Base 的 base,那么我们在调用的时候到底用的是哪一个? 而且如果base是一个非常大的结构体的话,每一个继承都需要创建一个base,那么岂不是很浪费空间,这就是多重继承的二义性和冗余的问题;

    而虚继承存在的目的便是为了解决这个问题,那么它是如何解决的呢?

    即使得A和B都虚继承Base,那么Base就是一个虚基类,只存储一份,而A和B中关于base的存储,都是存储一个指针,指向一个base, 因为把Base的成员存放在了内存的最后,这样根据相对偏移量就找到了这个共有的base;

    说了这些都是纸上谈兵,终觉浅,我们还是直接来查看内存的布局吧;

    class A:virtual public Base { int a; public: A() :a(1) {} void fun() { cout<<"A"<<endl; } }; class B : virtual public Base { int b; public: B() :b(2) {} void fun() { cout<<"B"<<endl; } };

    实例一个C 的对象 c ,看内存布局:

    我们可以 看到原本应该是00 00 00 00的位置被指针代替了,而base只在最后存了一份;

    实际上这两个指针指向一个虚基表,记录当前位置到最底下base的偏移量;这样就达到了共享的目的;

    如果对于虚基类只被构造了一次抱有怀疑,可以在前面的测试中的构造函数中加入测试语句查看;

    先复习到这吧,,,好累,后面继续补充!

    转载请注明原文地址: https://ju.6miu.com/read-198.html

    最新回复(0)