多重繼承的建構


如〈繼承共同行為〉中看過的,在單一繼承時,情況比較單純,建構子類實例時,會先執行父類建構式,接著是子類建構式,而解構的時候相反,會先執行子類解構式,接著才是父類解構式。

多重繼承時,若繼承來源之一有狀態定義,另一個沒有狀態定義,就如〈純虛擬函式(二)〉、〈模版與繼承〉中的範例,因為另一來源沒有狀態定義,也就不用考慮該來源的初始或銷毀問題,這時只要考量有狀態定義的繼承來源的建構與解構,如同單一繼承,問題就可以單純化。

在進一步看到多重繼承的建構與解構之前,先來看個單一繼承時 this 實際位址在哪的示範:

#include <iostream>
using namespace std;

class P {
    int x;

public:
    P(int x) : x(x) {
        cout << "P:" << this << endl;
    }
};

class C : public P {
public:
    C(int x) : P(x) {
        cout << "C:" << this << endl;
    }
};

int main() { 
    C c(10);

    cout << &c << endl;

    return 0;
}

顯示的結果會是同一位址:

P:0x61febc
C:0x61febc
0x61febc

如果是多重繼承的話呢?

#include <iostream>
using namespace std;

class P1 {
    int x;

public:
    P1(int x) : x(x) {
        cout << "P1:" << this << endl;
    }
};

class P2 {
    int x;

public:
    P2(int x) : x(x) {
        cout << "P2:" << this << endl;
    }
};

class C : public P1, public P2 {
public:
    C(int x) : P1(x), P2(x) {
        cout << "C:" << this << endl;
    }
};

int main() { 
    C c(10);

    cout << &c << endl;

    return 0;
}

多重繼承時,建構式的執行順序會與繼承的順序有關(而不是呼叫父類建構式的順序),C 因為繼承時的順序是 P1P2,建構式執行順序會是 P1P2C,至於解構式的執行順序,會是與建構式執行相反的順序,從執行結果中,可以發現 this 的位址會是不同:

P1:0x61feb8
P2:0x61febc
C:0x61feb8
0x61feb8

多重繼承時,C 實例的起始位址是 0x61feb8,而 P1 位址的偏移量是 0,P2 位址的偏移量是 4,因此 P1P2 中雖然都定義了 x 成員,若在個別的類別中寫 this->x,因為 this 位址不同,取得就會是各自不同的 x,因為是各自不同的位址,建構時也是個別地初始化在不同的位址,從執行結果中也可以看到,衍生類別實例的位址會用來初始第一個繼承的父類。

多重繼承時,個別類別中的 this 位址不同的事實,也會反應在以父類型態參考子類實例之時:

...略

int main() { 
    C c(10);

    cout << &c << endl;

    P1 &p1 = c;
    P2 &p2 = c;

    cout << "p1:" << &p1 << endl;
    cout << "p2:" << &p2 << endl;

    return 0;
}

執行時有關 p1p2 位址的顯示結果會是不同的:

...
p1:0x61feb0
p2:0x61feb4

取址的時候也是,在以下的範例中,都是 &c,然而 p1p2 儲存的位址並不同,知道這個事實後,就會知道將 p1 指標的位址指定給 p2 是不可行的,會造成編譯錯誤:

... 略

int main() { 
    C c(10);

    P1 *p1 = &c;
    P2 *p2 = &c;

    p2 = static_cast<P2*>(p1); // error: invalid static_cast from type 'P1*' to type 'P2*'

    return 0;
}

如果強硬地使用 C 風格轉型,也就是 p2 = (P2*) p1 的話,雖然可以通過編譯,結果是造成 p2 儲存了 p1 的位址,雖然 p2 的型態是 P2*,若透過 p2 操作 P2 定義的方法,會造成方法中的 this 指向的是 p1 的位址,結果會是不可預期的,當然,P1P2 本來就是不同繼承體系、不同型態,試圖在兩者之間轉換,本來就是錯誤的。