說說C++多重繼承

QingLiXueShi發表於2015-03-29

儘管大多數應用程式都使用單個基類的公用繼承,但有些時候單繼承是不夠用的,因為可能無法為問題域建模或對模型帶來不必要的複雜性。在這種情況下,多重繼承可以更直接地為應用程式建模。

一、基本概念

多重繼承是從多於一個直接基類派生類的能力,多重繼承的派生類繼承其父類的屬性。

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;
}

下面我們先來看TEST1的結果:

這個結果是毫無疑問的,先呼叫基類建構函式,再呼叫派生類。

接著,我們來看TEST2的結果:

首先呼叫預設建構函式構造一個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()。

相關文章