盡管大多數應用程序都使用單個基類的公用繼承,但有些時候單繼承是不夠用的,因為可能無法為問題域建模或對模型帶來不必要的復雜性。在這種情況下,多重繼承可以更直接地為應用程序建模。
一、基本概念
多重繼承是從多於一個直接基類派生類的能力,多重繼承的派生類繼承其父類的屬性。
class ZooAnimal{
};
class Bear : public ZooAnimal{
};
class Endangered{
};
class Panda : public Bear, public Endangered{
};
注意:
(1)與單繼承一樣,只有在定義之後,類才可以用作多重繼承的基類。
(2)對於類可以繼承的基類的數目,沒有語言強加的限制,但在一個給定派生列表中,一個基類只能出現一次。
1、多重繼承的派生類從每個基類中繼承狀態
Panda ying_yang("ying_yang");
對象ying_yang包含一個Bear子類對象、一個Endangered子類對象以及Panda類中聲明的非static數據成員。如下圖所示:
2、派生類構造函數初始化所有基類
派生類構造函數可以早構造函數初始化式中給零個或多個基類傳遞值。
Panda::Panda(string name, bool onExhibit)
: Bear(name, onExhibit, "Panda"),
Endangered(Endangered::critical){}
構造函數初始化式只能控制用於初始化基類的值,不能控制基類的構造次序。基類構造函數按照基類構造函數在類派生列表中的出現次序調用。對於Panda,基類初始化次序是:
(1)ZooAnimal。
(2)Bear,第一個直接基類。
(3)Endangered,第二個直接基類,它本身沒有基類。
(4)Panda,初始化本身成員,然後運行它的構造函數的函數體。
注意:構造函數調用次序既不受構造函數初始化列表中出現的基類的影響,也不受基類在構造函數初始化列表中的出現次序的影響。例如:
Panda::Panda() : Endangered(Endangered::critical){}
這個構造函數將隱式調用Bear的默認構造函數,盡管它不出現在構造函數初始化列表中,但仍然在Endangered類構造函數之前調用。
3、析構的次序
按照構造函數運行的逆序調用析構函數。Panda、Endangered、Bear,ZooAnimal。
二、轉換與多個基類
單個基類情況下,派生類的指針或引用可自動轉換為基類的指針或引用。對於多重繼承,派生類的指針或引用可以轉換為其任意基類的指針或引用。
注意:在多重繼承情況下,遇到二義性轉換的可能性更大。編譯器不會試圖根據派生類轉換來區別基類間的轉換,轉換到每個基類都一樣好。例如:
void print(const Bear&);
void print(const Endangered&);
通過Panda對象調用print時,會導致一個編譯時錯誤。
1、基於指針或引用類型的查找
與單繼承一樣,用基類的指針或引用只能訪問基類中定義(或繼承)的成員,不能訪問派生類中引入的成員。當一個類派生於多個基類的時候,那些基類之間沒有隱含的關系,不允許使用一個基類的指針訪問其他基類的成員。例如:
class ZooAnimal
{
public:
virtual void print(){}
virtual ~ZooAnimal(){}
};
class Bear : public ZooAnimal
{
public:
virtual void print()
{
cout << "I am Bear" << endl;
}
virtual void toes(){}
};
class Endangered
{
public:
virtual void print(){}
virtual void highlight()
{
cout << "I am Endangered.highlight" << endl;
}
virtual ~Endangered(){}
};
class Panda : public Bear, public Endangered
{
public:
virtual void print()
{
cout << "I am Panda" << endl;
}
virtual void highlight()
{
cout << "I am Panda.highlight" << endl;
}
virtual void toes(){}
virtual void cuddle(){}
virtual ~Panda()
{
cout << "Goodby Panda" << endl;
}
};
當有如下調用發生時:
int main()
{
Bear *pb = new Panda();
pb->print(); //ok: Panda::print
// pb->cuddle(); //error: not part of Bear interface
// pb->highlight(); //error: not part of Bear interface
delete pb; //Panda::~Panda
Endangered *pe = new Panda();
pe->print(); //ok: Panda::print
// pe->toes(); //error: not part of Endangered interface
// pe->cuddle(); //error: not part of Endangered interface
pe->highlight(); //ok: Panda::highlight
delete pe; //Panda::~Panda
return 0;
}
2、確定使用哪個虛析構函數
我們假定所有根基類都將它們的析構函數定義為虛函數,那麼通過下面幾種刪除指針方法,虛析構函數處理都是一致的。
delete pz; //pz is a ZooAnimal*
delete pb; //pb is a Bear*
delete pp; //pp is a Panda*
delete pe; //pe is a Endangered*
假定上面四個指針都指向Panda對象,則每種情況發生完全相同的析構函數調用次序,即與構造次序是逆序的:通過虛機制調用Panda析構函數,再依次調用Endangered、Bear,ZooAnimal的析構函數。
三、多重繼承派生類的復制控制
多重繼承的派生類使用基類自己的復制構造函數、賦值操作符,析構函數隱式構造、賦值或撤銷每個基類。下面我們做幾個小實驗:
1 class ZooAnimal
2 {
3 public:
4 ZooAnimal()
5 {
6 cout << "I am ZooAnimal default constructor" << endl;
7 }
8 ZooAnimal(const ZooAnimal&)
9 {
10 cout << "I am ZooAnimal copy constructor" << endl;
11 }
12 virtual ~ZooAnimal()
13 {
14 cout << "I am ZooAnimal destructor" << endl;
15 }
16 ZooAnimal& operator=(const ZooAnimal&)
17 {
18 cout << "I am ZooAnimal copy operator=" << endl;
19
20 return *this;
21 }
22 };
23 class Bear : public ZooAnimal
24 {
25 public:
26 Bear()
27 {
28 cout << "I am Bear default constructor" << endl;
29 }
30 Bear(const Bear&)
31 {
32 cout << "I am Bear copy constructor" << endl;
33 }
34 virtual ~Bear()
35 {
36 cout << "I am Bear destructor" << endl;
37 }
38 Bear& operator=(const Bear&)
39 {
40 cout << "I am Bear copy operator=" << endl;
41
42 return *this;
43 }
44 };
45 class Endangered
46 {
47 public:
48 Endangered()
49 {
50 cout << "I am Endangered default constructor" << endl;
51 }
52 Endangered(const Endangered&)
53 {
54 cout << "I am Endangered copy constructor" << endl;
55 }
56 virtual ~Endangered()
57 {
58 cout << "I am Endangered destructor" << endl;
59 }
60 Endangered& operator=(const Endangered&)
61 {
62 cout << "I am Endangered copy operator=" << endl;
63
64 return *this;
65 }
66 };
67 class Panda : public Bear, public Endangered
68 {
69 public:
70 Panda()
71 {
72 cout << "I am Panda default constructor" << endl;
73 }
74 Panda(const Panda&)
75 {
76 cout << "I am Panda copy constructor" << endl;
77 }
78 virtual ~Panda()
79 {
80 cout << "I am Panda destructor" << endl;
81 }
82 Panda& operator=(const Panda&)
83 {
84 cout << "I am Panda copy operator=" << endl;
85
86 return *this;
87 }
88 };
還是前面的類,只不過我將沒有必要的虛函數去掉了。下面我執行以下操作:
int main()
{
cout << "TEST 1" << endl;
Panda ying_ying;
cout << endl << endl;
cout << "TEST 2" << endl;
Panda zing_zing = ying_ying;
cout << endl << endl;
cout << "TEST 3" << endl;
zing_zing = ying_ying;
cout << endl << endl;
return 0;
}
這個結果是毫無疑問的,先調用基類構造函數,再調用派生類。
首先調用默認構造函數構造一個zing_zing對象,然後調用拷貝構造函數,將ying_ying拷貝至zing_zing。注意:這裡用的是拷貝構造函數,而不是賦值操作符,那什麼時候用賦值操作符呢?我們接著看TEST3的結果:
這種情況才調用賦值操作符:就是兩個對象都已經分配內存後,再進行賦值。這裡有個疑問,基類也定義了operator=了,為什麼不調用基類的operator=呢?我們將Panda類的operator=注釋掉,重新來做TEST3,好玩的結果出現了:
Panda的合成賦值操作符調用了兩個基類的operator=。
我們得出以下結論:如果派生類定義了自己的復制構造函數或賦值操作符,則負責復制(賦值)所有的基類子部分,而不再調用基類相應函數。只有派生類使用合成版本的復制構造函數或賦值操作符,才自動調用基類部分相應的函數。
最後我們來看一下析構函數的表現:
析構函數的行為是符合我們預期的,這裡有一點我沒有體現出來就是zing_zing是ying_ying之後定義的對象,所以zing_zing的構造函數先執行(前4行),後4行代表ying_ying構造函數的執行。如果具有多個基類的類定義了自己的析構函數,則該析構函數只負責清除派生類。
四、多重繼承下的類作用域
在多重繼承下,多個基類作用域可以包圍派生類作用域。查找時,同時檢查所有基類繼承子樹,例如:並行查找Endangered和Bear/ ZooAnimal子樹。如果在多個子樹上找到該名字,那個名字必須顯式指定使用哪個基類。否則,該名字的使用是二義性的。
例如:Endangered類和Bear類都有print函數,則ying_ying.print()將導致編譯時錯誤。
注意:
(1)Panda類的派生導致有兩個名為print的成員是合法的。派生只是導致潛在的二義性,如果沒有Panda對象調用print,就可避免這個二義性。你可以Bear::print或Endangered::print來調用。
(2)當然,如果只在一個基類子樹上找到聲明是不會出錯的。
下面仍然有個小實驗要做:
class ZooAnimal
{
public:
//void print(int x){}
};
class Bear : public ZooAnimal
{
public:
void print(int x){}
};
class Endangered
{
public:
void print(){}
};
class Panda : public Bear, public Endangered
{
public:
};
TEST1:將兩個基類Bear和Endangered兩個print的形參表設為不同。
TEST2:將Bear中的print去掉,在ZooAnimal中增加print。
TEST3:將Endangered中print設置為private訪問。
以上三種情況下,當我這樣調用ying_ying.print()或ying_ying.print(1)時,都顯示編譯時錯誤(二義性)。
我們的得出這樣的結論:名字查找的過程是這樣的,首先編譯器找到一個匹配的聲明(找到兩個匹配的聲明,這導致二義性),然後編譯器才確定所找到的聲明是否合法。
所以說,當我們調用這樣的函數時,應該這樣ying_ying.Bear::print()。