上一篇讨论过了关于虚表的一些基础知识,除了介绍了虚函数在内存中的存储结构,还剖析了单继承与多继承的底层实现方式。
首先重提多态,多态是指C++的多种状态。
分为静态多态和动态多态。静态多态,简言之就是重载,是在编译期决议确定函数地址。动态多态主要指的是通过继承和重写基类的虚函数实现的多态,运行时决议确定,在虚表中确定调用函数的地址。C++ 实现多态,其实是通过虚表来实现。这里就菱形继承做出内存剖析。
菱形继承:
菱形继承,大致关系如下
了解过C++的人都知道,菱形继承必然会带来数据冗余和二义性问题,通过虚继承可以来解决这个问题。首先给出一组测试代码,进入内存底部,去查看没有虚拟继承下的菱形继承,会带来什么样的问题。代码如下:
class B { public: B() :_b_val(1){} virtual void test1() { cout << "B::test1()" <<endl; } protected: int _b_val; }; class B1:public B { public: B1() :_b1_val(2){} virtual void test1() { cout << "B1::test1()" << endl; } virtual void test2() { cout << "B1::test2()" << endl; } virtual void test3() { cout << "B1::test3()" << endl; } protected: int _b1_val; }; class B2 :public B { public: B2() :_b2_val(3){} virtual void test1() { cout << "B2::test1()" << endl; } virtual void test4() { cout << "B2::test4()" << endl; } virtual void test5() { cout << "B2::test5()" << endl; } protected: int _b2_val; }; class D:public B1,public B2 { public: D() :_d_val(4){} virtual void tets1() { cout << "D::test1()" << endl; } virtual void test2() { cout << "D::test2()" << endl; } virtual void tets4() { cout << "D::test4()" << endl; } virtual void test6() { cout << "D::test6()" << endl; } protected: int _d_val; }; void test() { D d; }
代码说明:定义父类B,包含一个虚函数test1(),同时包含一个成员变量,构造为1;定义类B1,共有继承类 B ,包含三个虚函数,test1()重写 B 的虚函数,test2()和test3()为自己的虚函数,同时包含成员变量_b1_val,构造为2;定义类B2,共有继承类 B ,包含三个虚函数,test1()重写 B 的虚函数,test4()和test5()为自己的虚函数,同时包含成员变量_b2_val,构造为3;定义类 D,共有继承类 B1和B2,包含四个虚成员函数,test1()、test2()、test4()为重写继承父类的虚函数,test6()为类 D 自己的虚函数,同时包含成员变量_d_val,构造为4。
接下来切换到调试窗口<如下图>,可以看到类 D 实例化对象 d 这里维护了七个成员,由于直接继承了两个类,因此这里有两个虚表指针"__vfptr"、同时包含两次继承自基类的成员变量"_b_val"、"_b1_val",和"_b_val"、"_b2_val",和自己本身的成员变量"_d_val"。在内存窗口中“&d”,得到的是0x00cbdd10,对应到监视窗口,即继承的第一个类的虚表指针 __vfptr ,接下来是0x00000001,0x000000002即成员变量_b_val,_b1_val,接下来依次类推,得到第二个类的虚表指针,和继承自第二个类的成员变量,最后一项是子类 D 的成员变量。如果在这里去 sizeof(b),得到的结果应该是28。
在这里,依旧没有能直接找到子类 D 自己的虚成员函数地址。接下来,分别打开两个虚表指针所指向的内存空间。
多说一点,这里两个虚表指针指向的内容是没有相同的函数指针的。
这里可以注意到,第一个继承的类的虚表中有五个地址,第二个继承的类中国有三个地址,但我们在定义类 B1 和 B2 时,是完全相同的,也都是直接共有继承,所以推断,我们的子类 D 中自己的虚函数指针是保存在了第一个继承的类的虚表中。还是沿用上一篇提到的用函数指针的方式去调用这些函数,代码如下:
typedef void(*p_fun)(); void Print_fun(p_fun* ppfun) { int i = 0; for (; ppfun[i] != NULL; i++) { ppfun[i](); } } void test() { D d; Print_fun((p_fun*)(*(int*)&d)); cout << "-------------------------------" << endl; Print_fun((p_fun*)*((int*)&d+3)); }
这里我是直接通过指针的偏移调用到每一个函数,是因为我这里在内存中知道了有多少个函数指针,观察输出结果,如下图:
因此我们可以得到在菱形非虚拟继承下的内存布局是下图这样的关系:
在第一个继承的类的虚表中,保存了被子类 D 重写的函数test1()、test2()、test4()、test6(),和未被改写的函数test3()。第二个继承的类的虚表中,保存了被子类 D 重写的函数test1()和未被改写的函数test5(),另外还有额外保存的一份test4()。很明显,test1()函数被保存了两份,成员变量_b_val也保存了两份,在实际的开发过程中,这必然会带来大量的数据冗余和二义性的问题,造成调用不明确。
我们尝试用虚继承的方式去解决
代码如下:
class B { public: B() :_b_val(1){} virtual void test1() { cout << "B::test1()" <<endl; } protected: int _b_val; }; class B1 : virtual public B { public: B1() :_b1_val(2){} virtual void test1() { cout << "B1::test1()" << endl; } virtual void test2() { cout << "B1::test2()" << endl; } virtual void test3() { cout << "B1::test3()" << endl; } protected: int _b1_val; }; class B2 : virtual public B { public: B2() :_b2_val(3){} virtual void test1() { cout << "B2::test1()" << endl; } virtual void test4() { cout << "B2::test4()" << endl; } virtual void test5() { cout << "B2::test5()" << endl; } protected: int _b2_val; }; class D: public B1, public B2 { public: D() :_d_val(4){} virtual void test1() { cout << "D::test1()" << endl; } virtual void test2() { cout << "D::test2()" << endl; } virtual void tets4() { cout << "D::test4()" << endl; } virtual void test6() { cout << "D::test6()" << endl; } protected: int _d_val; }; void test() { D d; cout << sizeof(d) << endl;//----------->40 }
代码说明:和第一个例子一样,不同之处在于类 B1 和 B2 虚拟继承了公共子类 B。
接下来,切换到监视窗口,单步调试,如下图:
说实话,刚开始看到这里的时候,内心是很拒绝的,因为感觉又多出来好多虚表指针,而且更乱了,我们对类 D 实例化出的对象求 sizeof,得到的结果是40 ,比之前未使用虚继承的时候占用的空间更多。不过按照上面给出的箭头缕缕,可以发现,尽管在原来的基础上多出了三个__vfptr,但庆幸的是三个指向了同一块空间,而且这一次,我们成员变量并没有出现出现多次的情况。
仔细观察的话,会发现绿色箭头指向的虚表指针,下面还跟了一个地址,再次走进去看看,如图:
可以看到,通过偏移,我们的指针确实是可以找到我们监视窗口中出现了三次一样的__vfptr。表明在VS环境下,我们虚继承解决菱形继承带来问题,实际上是通过指针的偏移实现的。
为了确认,我们使用我们的函数指针对函数进行调用,代码如下:
typedef void(*p_fun)(); void Print_fun(p_fun* ppfun) { int i = 0; for (; ppfun[i] != NULL; i++) { ppfun[i](); } } void test() { D d; Print_fun((p_fun*)(*(int*)&d)); cout << "-------------------------------" << endl; //Print_fun((p_fun*)*((int*)&d+3));//测试有误,更改如下 ((p_fun*)*((int*)&d+3))[0](); cout << "-------------------------------" << endl; Print_fun((p_fun*)(*(int*)&d+8));
打印结果如图:
至此,关于虚继承对菱形继承带来问题的解决已经全部说明完毕,在VS环境下,通过指针的偏移解决了代码的冗余和二义性问题,实际开发过程中,怎么说呢。。能避免尽量避免,这并不是一种很好的做法。引入效率的下降是它的死穴,以空间和时间的代价去解决自己制造的麻烦并不是一个明智之举。如果关于虚继承,还是有不理解的地方,建议先看http://11331490.blog.51cto.com/11321490/1841930,多态的实现是C++一个很出色的地方,但有时候引入的问题还是要值得深究。
---------------------------------------muhuizz-------------------------------------------
http://11331490.blog.51cto.com/11321490/1841930