转载自:黑马程序员(因无链接可寻)
两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。
这种继承所带来的问题:
- 羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。
- 草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
class BigBase{ public: BigBase(){ mParam = 0; } void func(){ cout << "BigBase::func" << endl; } public: int mParam; };
class Base1 : public BigBase{}; class Base2 : public BigBase{}; class Derived : public Base1, public Base2{};
int main(){
Derived derived; //1. 对“func”的访问不明确 //derived.func(); //cout << derived.mParam << endl; cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl; cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;
//2. 重复继承 cout << "Derived size:" << sizeof(Derived) << endl; //8
return EXIT_SUCCESS; } |
上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?
对于这种菱形继承所带来的两个问题,c++为我们提供了一种方式,采用虚基类。那么我们采用虚基类方式将代码修改如下:
class BigBase{ public: BigBase(){ mParam = 0; } void func(){ cout << "BigBase::func" << endl; } public: int mParam; };
class Base1 : virtual public BigBase{}; class Base2 : virtual public BigBase{}; class Derived : public Base1, public Base2{};
int main(){
Derived derived; //二义性问题解决 derived.func(); cout << derived.mParam << endl; //输出结果:12 cout << "Derived size:" << sizeof(Derived) << endl;
return EXIT_SUCCESS; } |
以上程序Base1 ,Base2采用虚继承方式继承BigBase,那么BigBase被称为虚基类。
通过虚继承解决了菱形继承所带来的二义性问题。
但是虚基类是如何解决二义性的呢?并且derived大小为12字节,这是怎么回事?
4.7.6.3 虚继承实现原理
class BigBase{ public: BigBase(){ mParam = 0; } void func(){ cout << "BigBase::func" << endl; } public: int mParam; }; #if 0 //虚继承 class Base1 : virtual public BigBase{}; class Base2 : virtual public BigBase{}; #else //普通继承 class Base1 : public BigBase{}; class Base2 : public BigBase{}; #endif class Derived : public Base1, public Base2{}; |
通过对象布局图,我们发现普通继承和虚继承的对象内存图是不一样的。我们也可以猜测到编译器肯定对我们编写的程序做了一些手脚。
- BigBase 菱形最顶层的类,内存布局图没有发生改变。
- Base1和Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个vbptr (virtual base pointer),vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。
- Derived派生于Base1和Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量。
由此可知编译器帮我们做了一些幕后工作,使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义性的问题。现在模型就变成了Base1和 Base2 、Derived三个类对象共享了一份BigBase数据。
当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的)。即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到底谁应该负责完成初始化呢?C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。
class BigBase{ public: BigBase(int x){mParam = x;} void func(){cout << "BigBase::func" << endl;} public: int mParam; }; class Base1 : virtual public BigBase{ public: Base1() :BigBase(10){} //不调用BigBase构造 }; class Base2 : virtual public BigBase{ public: Base2() :BigBase(10){} //不调用BigBase构造 };
class Derived : public Base1, public Base2{ public: Derived() :BigBase(10){} //调用BigBase构造 }; //每一次继承子类中都必须书写初始化语句 int main(){ Derived derived; return EXIT_SUCCESS; } |
注意:
虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的. |
工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替。
Jerry Schwarz,输入输出流(iostream)的作者,曾在个别场合表示如何他重新设计iostream的话,很可能从iostream中去除多重继承。 |
这种继承所带来的问题:
- 羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。
- 草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
class BigBase{ public: BigBase(){ mParam = 0; } void func(){ cout << "BigBase::func" << endl; } public: int mParam; };
class Base1 : public BigBase{}; class Base2 : public BigBase{}; class Derived : public Base1, public Base2{};
int main(){
Derived derived; //1. 对“func”的访问不明确 //derived.func(); //cout << derived.mParam << endl; cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl; cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;
//2. 重复继承 cout << "Derived size:" << sizeof(Derived) << endl; //8
return EXIT_SUCCESS; } |