最近做题的时候经常遇到虚函数和虚继承的内存分布情况问题,自己也有点生疏了,所以,赶紧在这里回忆补充一下!
先从一个类的内存分布情况开始看起:
环境: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的偏移量;这样就达到了共享的目的;
如果对于虚基类只被构造了一次抱有怀疑,可以在前面的测试中的构造函数中加入测试语句查看;
先复习到这吧,,,好累,后面继续补充!