多重继承MI(multiple inheritance):通过继承多个基类来生成一个新类。只要继承图简单,MI应该也不复杂。但是MI会引入许多二义性和奇怪的现象。
子对象重复
派生类从基类继承时,派生类中就获得了基类所有数据成员的副本,该副本称为子对象。假若对类d1和类d2进行多重继承而形成类mi,类mi会包含d1的子对象和d2的子对象,所以mi对象看上去如下面左图所示。
如果d1和d2都是从相同基类base派生的,那么会发生什么呢?在上面的右图中,d1和d2都包含base的子对象,所以mi包含基的两个子对象。从继承图形状上看,该继承层次结构有时称为“菱形”。没有菱形情况时,多重继承相当简单,但是只要菱形一出现,由于新类中存在重叠的子对象,麻烦就开始了。重叠的子对象增加了存储空间,这种额外开销是否成为一个问题取决于我们的设计,而且它同时又引入了二义性。
假若把一个指向mi的指针转型给一个指向base的指针时,将会发生什么呢?base有两个子对象,因此会映射成哪一个的地址呢?我们看下面的代码:
这里存在两个问题。首先,由于d1和d2分别对vf()定义这会导致一个冲突,所以不能生成MI类。其次,在对b[]的数组定义中试图创建一个new MI并将类型转化为base*,但是由于无法知道使用d1子对象的base还是d2子对象的base作为转型后的地址,所以编译器将不会受理。// MI & ambiguity #include <iostream> #include <vector> using namespace std; class MBase { public: virtual char* vf() const = 0; virtual ~MBase() {} }; class D1 : public MBase { public: char* vf() const { return "D1"; } }; class D2 : public MBase { public: char* vf() const { return "D2"; } }; // Causes error: ambiguous override of vf(): //! class MI : public D1, public D2 {}; int main() { vector<MBase*> b; b.push_back(new D1); b.push_back(new D2); // Cannot upcast: which subobject?: //! b.push_back(new MI); for(int i = 0; i < b.size(); i++) cout << b[i]->vf() << endl; }
虚基类
为了解决第一个问题,必须对类MI中的函数vf()进行重新定义以消除二义性。对于第二个问题的解决应着眼于语言扩展,这就是对virtual赋予新的含义。假若以virtual的方式继承一个基类,则仅仅会出现一个基类子对象。即使用虚基类。
#include <iostream> #include <vector> using namespace std; class MBase { public: virtual char* vf() const = 0; virtual ~MBase() {} }; class D1 : virtual public MBase { public: char* vf() const { return "D1"; } }; class D2 : virtual public MBase { public: char* vf() const { return "D2"; } }; class MI : public D1, public D2 { public: char* vf() const { return D1::vf(); } }; int main() { vector<MBase*> b; b.push_back(new D1); b.push_back(new D2); b.push_back(new MI); for(int i = 0; i < b.size(); i++) cout << b[i]->vf() << endl; }
虚基类初始化
编译器是无法知道使用d1初始化base还是使用d2来初始化base,因而我们总是被迫在最晚辈派生类中具体指出。注意,仅仅这个被指出的虚基构造函数会被调用。打算使用一个虚基类时,最晚辈派生类的构造函数的职责是对虚基类进行初始化。这意味着该类不管离虚基类多远,都有责任对虚基类进行初始化。
在上面的例子中,d1和d2在它们的构造函数中都必须对base初始化,但mi和x也都如此,尽管它们和base相隔好几个层次。这是因为每一个都能成为最晚辈派生类。class MBase { public: MBase(int) {} virtual char* vf() const = 0; virtual ~MBase() {} }; class D1 : virtual public MBase { public: D1() : MBase(1) {} char* vf() const { return "D1"; } }; class D2 : virtual public MBase { public: D2() : MBase(2) {} char* vf() const { return "D2"; } }; class MI : public D1, public D2 { public: MI() : MBase(3) {} char* vf() const { return D1::vf(); // MUST disambiguate } }; class X : public MI { public: // You must ALWAYS init the virtual base: X() : MBase(4) {} };
最好通过创建一个虚基类的缺省构造函数,使最晚辈派生类自动调用这个缺省构造函数。如下面的例子:
class MBase { public: // Default constructor removes responsibility: MBase(int = 0) {} virtual char* vf() const = 0; virtual ~MBase() {} }; class D1 : virtual public MBase { public: D1() : MBase(1) {} char* vf() const { return "D1"; } }; class D2 : virtual public MBase { public: D2() : MBase(2) {} char* vf() const { return "D2"; } }; class MI : public D1, public D2 { public: MI() {} // 调用MBase的缺省构造函数 char* vf() const { return D1::vf(); // MUST disambiguate } }; class X : public MI { public: X() {} // 调用MBase的缺省构造函数 };
额外的开销
VS2008中的输出结果是:// Virtual base class overhead #include <fstream> using namespace std; ofstream out("overhead.out"); class MBase { public: virtual void f() const {}; virtual ~MBase() {} }; class NonVirtualInheritance : public MBase {}; class VirtualInheritance : virtual public MBase {}; class VirtualInheritance2 : virtual public MBase {}; class MI : public VirtualInheritance, public VirtualInheritance2 {}; #define WRITE(ARG) \ out << #ARG << " = " << ARG << endl; int main() { MBase b; WRITE(sizeof(b)); NonVirtualInheritance nonv_inheritance; WRITE(sizeof(nonv_inheritance)); VirtualInheritance v_inheritance; WRITE(sizeof(v_inheritance)); MI mi; WRITE(sizeof(mi)); }
sizeof(b) = 4
sizeof(nonv_inheritance) = 4
sizeof(v_inheritance) = 8
sizeof(mi) = 12
向上转型
无论是通过创建成员对象还是通过继承把一个类的子对象嵌入一个新类中,每一个子对象都有自己的this指针,在处理成员对象的时候可以直接简单。但是只要引入多重继承,有趣的现象就会出现:由于对象在向上映射期间出现多个类,因而对象存在多个this指针。下面就是这种情况的例子:
可以清楚地看到,能在一个相同的对象中获得两个不同的this指针。输出结果:// MI and the "this" pointer #include <fstream> using namespace std; ofstream out("mithis.out"); class Base1 { char c[0x10]; public: void printthis1() { out << "Base1 this = " << this << endl; } }; class Base2 { char c[0x10]; public: void printthis2() { out << "Base2 this = " << this << endl; } }; class Member1 { char c[0x10]; public: void printthism1() { out << "Member1 this = " << this << endl; } }; class Member2 { char c[0x10]; public: void printthism2() { out << "Member2 this = " << this << endl; } }; class MI : public Base1, public Base2 { Member1 m1; Member2 m2; public: void printthis() { out << "MI this = " << this << endl; printthis1(); printthis2(); m1.printthism1(); m2.printthism2(); } };
sizeof(mi) = 40 hex
MI this = 0027FBD4
Base1 this = 0027FBD4
Base2 this = 0027FBE4
Member1 this = 0027FBF4
Member2 this = 0027FC04
Base 1 pointer = 0027FBD4
Base 2 pointer = 0027FBE4派生对象的起始地址和它的基类列表中的第一个类的地址是一致的,之后是第二个基类的地址,接着根据声明的次序安排成员对象的地址。当向base1和base2进行映射时,虽然表面上它们是指向同一个对象,但实际上它们有不同this指针,只有这样,正确的起始地址才能够被传给相应子对象的成员函数。当对多重继承的子对象调用一个成员函数时,只有隐式向上转型才能使程序正确工作。
避免MI
1) 我们有必要同时使用两个类的公共接口吗,是否可在一个类中用成员函数包含这些接口呢?
2) 我们需要向上映射到两个基类上吗?如果我们对以上两个问题都能以“不”来回答,那么就可以避免使用MI。
修复接口
使用多重继承的最好的理由之一是使用你无法控制的代码(别人的或未公开的)。比如说要使用一个有头文件和编译的成员函数的库,但是没有成员函数的源代码。该库具有虚函数的类层次,它含有一些使用库中基类指针的全局函数,这就是说它多态地使用库对象。现在,我们围绕着该库创建了一个应用程序并且利用基类的多态方式编写了自己的代码。在随后的项目开发或维护期间,我们发现基类接口和供应商所提供的不兼容:我们需要某虚函数但提供的却是非虚的,或者基本虚函数在接口中根本不存在。由于没有源代码,只有大量的依赖最初接口的已生成的代码,这时多重继承则是极好的解决方法。假设下面是头文件:
下面是未提供给我们的源代码(用来测试)class Vendor { public: virtual void v() const; void f() const; ~Vendor(); }; class Vendor1 : public Vendor { public: void v() const; void f() const; ~Vendor1(); }; void A(const Vendor&); void B(const Vendor&);
我们可以这样添加使用多重继承来实现我们的功能://: Vendor.cpp // Implementation of VENDOR.H // This is compiled and unavailable to you #include "Vendor.h" #include <fstream> using namespace std; extern ofstream out; // For trace info void Vendor::v() const { out << "Vendor::v()\n"; } void Vendor::f() const { out << "Vendor::f()\n"; } Vendor::~Vendor() { out << "~Vendor()\n"; } void Vendor1::v() const { out << "Vendor1::v()\n"; } void Vendor1::f() const { out << "Vendor1::f()\n"; } Vendor1::~Vendor1() { out << "~Vendor1()\n"; } void A(const Vendor& V) { // ... V.v(); V.f(); //.. } void B(const Vendor& V) { // ... V.v(); V.f(); //.. }
#include "Vendor.h" #include <fstream> using namespace std; ofstream out("paste.out"); class MyBase { // Repair Vendor interface public: virtual void v() const = 0; virtual void f() const = 0; // New interface function: virtual void g() const = 0; virtual ~MyBase() { out << "~MyBase()\n"; } }; class Paste1 : public MyBase, public Vendor1 { public: void v() const { out << "Paste1::v()\n"; Vendor1::v(); } void f() const { out << "Paste1::f()\n"; Vendor1::f(); } void g() const { out << "Paste1::g()\n"; } ~Paste1() { out << "~Paste1()\n"; } }; int main() { Paste1& p1p = *new Paste1; MyBase& mp = p1p; // Upcast out << "calling f()\n"; mp.f(); // Right behavior out << "calling g()\n"; mp.g(); // New behavior out << "calling A(p1p)\n"; A(p1p); // Same old behavior out << "calling B(p1p)\n"; B(p1p); // Same old behavior out << "delete mp\n"; // Deleting a reference to a heap object: delete ∓ // Right behavior }