虚继承、虚基类
#include <iostream> using namespace std; class furniture { public: furniture( int nWeight ) { m_nWeight = nWeight; cout << "家具的构造" << endl; } int m_nWeight; }; //虚继承 //使派生类对象只有一份基类(furniture)的拷贝 //在菱形继承下,在下面的两个继承关系中,加上虚继承 class Safa : virtual public furniture { public: Safa( int nWeight ):furniture(nWeight) { cout << "沙发的构造" << endl; } void sit() { } }; class Bed : virtual public furniture { public: Bed( int nWeight ):furniture(nWeight) { cout << "床的构造" << endl; } void sleep() { } }; //构造顺序,先是虚基类 //再是非虚基类,按声明的顺序构造 class SafaBed :public Bed, public Safa { public: //注意Safa和Bed的构造顺序,按照声明顺序,不是初始化列表顺序 SafaBed( int nWeight ): Safa(nWeight), Bed(nWeight), furniture(nWeight) { Bed::m_nWeight = nWeight; cout << "沙发床的构造" << endl; } }; int main(int argc, char* argv[]) { SafaBed theSafaBed(100); furniture* pFurniture = (Bed*)&theSafaBed; theSafaBed.sit(); theSafaBed.sleep(); cout << sizeof(theSafaBed) << endl; getchar(); return 0; }
运行结果:
注意SafaBed的成员初始化列表中,需要显式初始化虚基类furniture。
如果把Bed和Safa类定义的时候的2个virtual去掉,那么也必须修改SafaBed的成员初始化列表,需要删除对furniture的初始化,否则编译器提示:对“furniture”的访问不明确。
运行结果为:
详细分析:
虚继承就是引入了虚基类的继承。引入虚基类的目的是为了解决类继承过程中产生的二义性问题。这种二义性问题常见于具有菱形继承关系的类继承体系中。比如:有四个类:A、B、C、D,它们之间的继承关系是:B继承A,C继承A,D继承B和C。这就形成了一个菱形的继承关系,具有这种继承关系的图叫做有向无环图。那么类D就有两条继承路径:D-->B-->A和D-->C-->A。而类A是派生类D的两条继承路径上的公共基类,那么这个公共基类就会在派生类D的对象中产生多个基类子对象,这个时候在D中引用基类A的成员时,就会产生明显的二义性。要解决这个二义性,就必须将这个基类A设定为虚基类。
class A: { public: void fun(); protected: int a; }; class B: virtual public A { protected: int b; }; class C: virtual pulic A { protected: int c; }; class D: public B, public C { public: int g(); protected: int d; };
这样的话,不同继承路径上的虚基类子对象在派生类中被合并成一个子对象了。这便是虚基类的作用。这样就可以消除合并之前出现的二义性问题。这时在派生类D的对象中只存在一个类A的子对象。C++中的二义性检查是在访问权限或类型检查之前进行的。
引进虚基类之后,派生类(子类)的对象中只存在一个虚基类的子对象。当一个类拥有虚基类的时候,编译系统会为这个类的对象定义一个指针成员,并让它指向虚基类的子对象。该指针被称为虚基类指针。这个概念与虚函数表指针不同。在内存中,一般情况下,虚基类子对象在派生类对象中是放置在派生类对象所占内存块的尾部,不过这是由编译器来决定的。
虚基类的构造函数
为了初始化虚基类的子对象,派生类的构造函数要调用基类的构造函数。由于派生类的对象中只有一个虚基类子对象,那么就必须要保证虚基类的子对象只被初始化一次。也就是说,虚基类的构造函数只能被调用一次。
由于继承的层次可能会很深,C++规定,真正创建对象时的类称为是最派生类,虚基类子对象是由最派生类的构造函数,在其成员初始化列表中,通过直接调用虚基类的构造函数进行初始化的。如果一个派生类有一个直接或间接的虚基类,那么派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的显式调用(除非虚基类有默认构造函数)。如果没有列出,则表示使用该虚基类的缺省构造函数来初始化派生类对象中的虚基类子对象。
从虚基类直接或间接继承的派生类的成员初始化列表中必须列出对该虚基类构造函数的调用。但是只有真正用于创建对象的那个最派生类的构造函数才会真正调用虚基类的构造函数。而最派生类到虚基类的中间类初始化时,它们所列出的对虚基类的构造函数的调用在实际执行中被忽略。这样就保证对虚基类的子对象只初始化一次。
C++又规定:在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,则虚基类的构造函数先于非虚基类的构造函数被执行。
在需要使用虚基类的场合,由于每一个继承类的初始化列表中,必须包含初始化虚基类的语句。而这些初始化语句仅仅只在最底层子类(最派生类)中才实际调用,这样就会使得某些上层子类得到的虚基类子对象的状态不是自己所期望的,因为自己的初始化语句被跳过了。所以,一般建议不要在虚基类中包含任何数据成员,即不要有状态,只可以作为接口类来使用。
关于内存布局参考链接中有,不过我没怎么看懂。
参考:http://bdxnote.blog.163.com/blog/static/844423520091130112659501/