一、菱形继承
那就先从菱形继承开始复习
如下代码:
#include<iostream> using namespace std; class A { public: int _a; }; class B : public A { public: int _b; }; class C : public A { public: int _c; }; class D : public C, public B { public: int _d; }; int main() { D dd; cout << sizeof(dd) << endl; dd.B::_a = 1; dd._b = 3; dd.C::_a = 2; dd._c = 4; dd._d = 5; B bb; C cc; cout << sizeof(bb) << endl; system("pause"); return 0; }
B、C中的_a都是来自于A,其实两个_a是同一个,但是因为B、C各自继承,产生了两份,造成了数据冗余。由于D继承于B、C,B、C继承于A,所以D中的_a不知道是来自B还是来自C,于是产生了二义性。
二、菱形虚拟继承
解决上述问题的方法就是将B和C的继承变为虚继承,在子类继承父类时,在访问限定符前加上virtual就可以虚继承。代码如下:
#include<iostream> using namespace std; class A { public: int _a; }; class B :virtual public A { public: int _b; }; class C :virtual public A { public: int _c; }; class D : public C, public B { public: int _d; }; int main() { D dd; cout << sizeof(dd) << endl; dd._a = 1; dd._b = 3; dd._a = 2; dd._c = 4; dd._d = 5; B bb; C cc; cout << sizeof(bb) << endl; return 0; }
从上图可以发现对象dd的内存布局与虚继承之前有很大的区别,首先cc对象和bb对象的内存空间中都分别多了一个存储着一个地址的空间,而把它们_a变量放在了成员变量的最底下,使_a成为一个公共的变量。显而易见,加了virtual之后的继承比普通继承的类多了4个字节,而在内存中看到在加了virtual的类中多了一个地址,请看以下分析:
分析二:
由以上分析,发现虚表里的上面的地址存储的是寻找自己的虚表的偏移量
下面的地址存储的是寻找公共的基类的偏移量
C的虚基表里找公共基类的偏移量是20,B的虚基表里找公共基类的偏移量是12,从各自的虚基表往下数
各自的偏移量就可以找到公共的基类A。用一个公共的位置管理公共继承的基类,这样就解决了二义性。
三、带有虚函数的菱形继承
在类的成员函数前加上virtual关键字,则这个成员函数称为虚函数。先看看代码:
#include<iostream> using namespace std; //定义一个可以指向对象里函数的函数指针 typedef void(*func)(); //打印虚函数表 void PrintVtable(int* vtable) { printf("vtable:0x%p\n",vtable); for (size_t i = 0; vtable[i] != 0; ++i) { printf("第%d个虚函数地址:0x%p,->", i, vtable[i]); func f = (func)vtable[i]; f(); } cout <<"======================================="<< endl; } class A { public: int _a; virtual void func1() { cout << "A::func1()" << endl; } virtual void func2() { cout << "A::func2()" << endl; } }; class B :public A { public: int _b; virtual void func1() { cout << "B::func1()" << endl; } virtual void func3() { cout << "B::func3()" << endl; } }; class C :public A { public: int _c; virtual void func1() { cout << "C::func1()" << endl; } virtual void func3() { cout << "C::func3()" << endl; } }; class D : public B,public C { public: int _d; virtual void func4() { cout << "D::func4()" << endl; } }; int main() { D dd; dd.B::_a = 1; dd.C::_a = 2; dd._b = 3; dd._c = 4; dd._d = 5; //D类里继承B类的虚表 PrintVtable(*(int**)&dd); system("pause"); return 0; }
当发生继承时,如果派生类重写了基类的虚函数,那么派生类的对象中会修改基类的虚表,虚表中的函数指针会指向派生类自己重写的函数,如果派生类没有重写基类的虚函数,那么派生类不会改变那个虚函数的指向只是把它继承下来。
四、带有虚函数的菱形虚拟继承
在菱形虚拟继承的基础上加上了虚函数,代码如下:
#include<iostream> using namespace std; //定义一个可以指向对象里函数的函数指针 typedef void(*func)(); //打印虚函数表 void printvtable(int* vtable) { cout << "虚表地址>" << vtable << endl; for (int i = 0; vtable[i] != 0; ++i) { printf("第%d个虚函数地址:0x%x,->", i, vtable[i]); func f = (func)vtable[i]; f(); } cout << endl; } class A { public: int _a; virtual void func1() { cout << "A::func1()" << endl; } }; class B :virtual public A { public: int _b; virtual void func2() { cout << "B::func2()" << endl; } virtual void func1() { cout << "B::func1()" << endl; } }; class C :virtual public A { public: int _c; virtual void func1() { cout << "C::func1()" << endl; } }; class D : public C,public B { public: int _d; virtual void func1() { cout << "D::func1()" << endl; } virtual void func3() { cout << "D::func3()" << endl; } }; int main() { D dd; B bb; C cc; A aa; cout << sizeof(dd) << endl; dd._a = 1; dd._b = 3; dd._a = 2; dd._c = 4; dd._d = 5; cout << sizeof(bb) << endl; //D类里继承B类的虚表 cout << "D:bb:0x" << ⅆ int* vtabledd = (int*)(*(int*)&dd); printvtable(vtabledd); //D类里继承C类的虚表 cout << "D:cc:0x" << ((int*)&dd + 3); int* vtablecc = (int*)(*((int*)&dd + 3)); printvtable(vtablecc); //D类里继承A类的虚表 cout << "D:aa:0x" << ((int*)&dd + 6); int* vtableaa = (int*)(*((int*)&dd +6)); printvtable(vtableaa); return 0; }
当给bb对象里加了个fun2函数后发现dd的对象模型又多了四个字节,这四个字节是在dd对象中继承的bb的内存空间中多出来的,就是用来存放bb对象自己的虚表地址。
因为aa对象被两个对象所继承,所以aa的虚表被被公用,因此bb和cc对象的虚函数就不能放在他们公共的虚表里,只能创建自己的虚表,把自己的虚函数放在自己的虚表。
1.dd对象继承重写aa对象的虚函数存储在继承来的aa对象的虚函数表里。
2.dd对象里自己的虚函数(非继承来的)存在第一个继承来的对象的虚表里。
3.因为aa对象被两个对象所继承,所以aa的虚表被被公用,因此bb和cc对象的虚函数就不能放在他们公共的虚表里,只能创建自己的虚表,把自己的虚函数放在自己的虚表。