继承作为面向对象编程的一种基本特征,其使用频率非常高。而继承包含了虚拟继承和普通继承,在可见性上分为public、protected、private。可见性继承比较简单,而虚拟继承对学习c++的难度较大。
首先,虚拟继承与普通继承的区别有:
假设derived 继承自base类,那么derived与base是一种“is a”的关系,即derived类是base类,而反之错误;
假设derived 虚继承自base类,那么derivd与base是一种“has a”的关系,即derived类有一个指向base类的vptr。(貌似有些牵强!某些编译器确实如此,关于虚继承与普通继承的差异见:c++ 虚继承与继承的差异 )
因此虚继承可以认为不是一种继承关系,而可以认为是一种组合的关系。正是因为这样的区别,下面我们针对虚拟继承来具体分析。虚拟继承中遇到最广泛的是菱形结构。下面从菱形虚继承结构说起吧:
class stream { public: stream(){cout<<"stream::stream()!"<<endl;} }; class iistream:virtual stream { public: iistream(){cout<<"istream::istream()!"<<endl;} }; class oostream:virtual stream { public: oostream(){cout<<"ostream::ostream()!"<<endl;} }; class iiostream:public iistream,public oostream { public: iiostream(){cout<<"iiostream::iiostream()!"<<endl;} }; int main(int argc, const char * argv[]) { iiostream oo; }
stream::stream()!
istream::istream()!
ostream::ostream()!
iiostream::iiostream()!
输出这样的结果是毫无悬念的!本来虚拟继承的目的就是当多重继承出现重复的基类时,其只保存一份基类。减少内存开销。其继承结构为:
stream
/ \
istream ostream
\ /
iiostream
这样子的菱形结构,使公共基类只产生一个拷贝。
而现在我们换种方式使用虚继承:
class stream { public: stream(){cout<<"stream::stream()!"<<endl;} }; class iistream:public stream { public: iistream(){cout<<"istream::istream()!"<<endl;} }; class oostream:public stream { public: oostream(){cout<<"ostream::ostream()!"<<endl;} }; class iiostream:virtual iistream,virtual oostream { public: iiostream(){cout<<"iiostream::iiostream()!"<<endl;} }; int main(int argc, const char * argv[]) { iiostream oo; }
stream::stream()!
istream::istream()!
stream::stream()!
ostream::ostream()!
iiostream::iiostream()!
从结果可以看到,其构造过程中重复出现基类stream的构造过程。这样就完全没有达到虚拟继承的目的。其继承结构为:
stream stream
\ /
istream ostream
\ /
iiostream
从继承结构可以看出,如果iiostream对象调用基类stream中的成员方法,会导致方法的二义性。因为iiostream含有指向其虚继承基类 istream,ostream的vptr。而 istream,ostream包含了stream的空间,所以导致iiostream不知道导致是调用那个stream的方法。要解决改问题,可以指定vptr,即在调用成员方法是需要加上作用域,例如
class stream { void f(){cout<<"here!"<<endl;} } main() { iiostream ii; ii.f(); }
编译器提示调用f方法错误。而采用
ii.istream::f();
编译通过,并且会调用istream类vptr指向的f()方法。 前面说了这么多,在实际的应用中虚拟继承的胡乱使用,更是会导致继承顺序以及基类构造顺序的混乱。如下面的代码:
class B1 { public: B1(){cout<<"B1::B1()!<"<<endl;} void f() {cout<<"i'm here!"<<endl;} }; class V1: public B1 { public: V1(){cout<<"V1::V1()!<"<<endl;} }; class D1: virtual public V1 { public: D1(){cout<<"D1::D1()!<"<<endl;} }; class B2 { public: B2(){cout<<"B2::B2()!<"<<endl;} }; class B3 { public: B3(){cout<<"B3::B3()!<"<<endl;} }; class V2:public B1, public B2 { public: V2(){cout<<"V2::V2()!<"<<endl;} }; class D2:virtual public V2, public B3 { public: D2(){cout<<"D2::D2()!<"<<endl;} }; class M1 { public: M1(){cout<<"M1::M1()!<"<<endl;} }; class M2 { public: M2(){cout<<"M2::M2()!<"<<endl;} }; class X:public D1, public D2 { M1 m1; M2 m2; }; int main(int argc, const char * argv[]) { X x; }
上面的代码是来自《Exceptional C++ Style》中关于继承顺序的一段代码。可以看到,上面的代码继承关系非常复杂,而且层次不是特别的清楚。而虚继承的加入更是让继承结构更加无序。不管怎么样,我们还是可以根据c++的标准来分析上面代码的构造顺序。c++对于创建一个类类型的初始化顺序是这样子的:
1.最上层派生类的构造函数负责调用虚基类子对象的构造函数。所有虚基类子对象会按照深度优先、从左到右的顺序进行初始化;
2.直接基类子对象按照它们在类定义中声明的顺序被一一构造起来;
3.非静态成员子对象按照它们在类定义体中的声明的顺序被一一构造起来;
4.最上层派生类的构造函数体被执行。
根据上面的规则,可以看出,最先构造的是虚继承基类的构造函数,并且是按照深度优先,从左往右构造。因此,我们需要将继承结构划分层次。显然上面的代码可以认为是4层继承结构。其中最顶层的是B1,B2类。第二层是V1,V2,V3。第三层是D1,D2.最底层是X。而D1虚继承V1,D2虚继承V2,且D1和D2在同一层。所以V1最先构造,其次是V2.在V2构造顺序中,B1先于B2.虚基类构造完成后,接着是直接基类子对象构造,其顺序为D1,D2.最后为成员子对象的构造,顺序为声明的顺序。构造完毕后,开始按照构造顺序执行构造函数体了。所以其最终的输出结果为:
B1::B1()!<
V1::V1()!<
B1::B1()!<
B2::B2()!<
V2::V2()!<
D1::D1()!<
B3::B3()!<
D2::D2()!<
M1::M1()!<
M2::M2()!<
从结果也可以看出其构造顺序完全符合上面的标准。而在结果中,可以看到B1重复构造。还是因为没有按照要求使用virtual继承导致的结果。要想只构造B1一次,可以将virtual全部改在B1上,如下面的代码:
class B1 { public: B1(){cout<<"B1::B1()!<"<<endl;} void f() {cout<<"i'm here!"<<endl;} }; class V1: virtual public B1 //public修改为virtual { public: V1(){cout<<"V1::V1()!<"<<endl;} }; class D1: public V1 { public: D1(){cout<<"D1::D1()!<"<<endl;} }; class B2 { public: B2(){cout<<"B2::B2()!<"<<endl;} }; class B3 { public: B3(){cout<<"B3::B3()!<"<<endl;} }; class V2:virtual public B1, public B2 //public B1修改为virtual public B1 { public: V2(){cout<<"V2::V2()!<"<<endl;} }; class D2: public V2, public B3 { public: D2(){cout<<"D2::D2()!<"<<endl;} }; class M1 { public: M1(){cout<<"M1::M1()!<"<<endl;} }; class M2 { public: M2(){cout<<"M2::M2()!<"<<endl;} }; class X:public D1, public D2 { M1 m1; M2 m2; };
根据上面的代码,其输出结果为:
B1::B1()!<
V1::V1()!<
D1::D1()!<
B2::B2()!<
V2::V2()!<
B3::B3()!<
D2::D2()!<
M1::M1()!<
M2::M2()!<
由于虚继承导致其构造顺序发生比较大的变化。不管怎么,分析的规则还是一样。
上面分析了这么多,我们知道了虚继承有一定的好处,但是虚继承会增大占用的空间。这是因为每一次虚继承会产生一个vptr指针。空间因素在编程过程中,我们很少考虑,而构造顺序却需要小心,因此使用未构造对象的危害是相当大的。因此,我们需要小心的使用继承,更要确保在使用继承的时候保证构造顺序不会出错。下面我再着重强调一下基类的构造顺序规则:
1.最上层派生类的构造函数负责调用虚基类子对象的构造函数。所有虚基类子对象会按照深度优先、从左到右的顺序进行初始化;
2.直接基类子对象按照它们在类定义中声明的顺序被一一构造起来;
3.非静态成员子对象按照它们在类定义体中的声明的顺序被一一构造起来;
4.最上层派生类的构造函数体被执行。