1. 程式人生 > 其它 >[深度探索C++物件模型] 第二章 建構函式語義學

[深度探索C++物件模型] 第二章 建構函式語義學

1. 預設建構函式在需要時會被編譯器創建出來。被誰需要?做什麼事情?

有兩種情況需要建構函式,程式需要和編譯器需要。如果是程式需要,那是程式設計師的責任,因此在程式需要時,編譯器不會自動建立建構函式。當編譯器需要的時候,才會自動建立,且只會執行編譯器需要的動作,不會執行額外的動作。

在以下情況,編譯器需要為程式提供一個預設建構函式:

1) 類內部有其他成員物件,編譯器需要一個預設建構函式來初始化內部物件,即呼叫內部物件的建構函式。注意這個自動生成的預設建構函式不會初始化其他非物件成員,如int,指標等。若類中已有建構函式,編譯器會在編譯時,在其中插入一些程式碼以執行預設建構函式的初始化動作,即擴張使用者的建構函式

2) 類似的,若父類有建構函式,但是子類沒有,則會為子類新增預設建構函式以便在初始化時呼叫父類的建構函式

3) 帶有虛擬函式的類,因為需要在建構函式中構造虛擬函式表

4) 繼承自虛基類,在編譯過程中可能無法確認具體的類,需要在建構函式中為類物件安插類似虛指標的成員,用於確定具體類物件

2. 在編譯器合成的預設建構函式中,僅初始化基類子物件和類物件成員,其他非靜態資料成員都不會操作。

3. 關於預設建構函式的2個誤區:

1) 沒有建構函式時,編譯器一定會合成一個。錯!僅在編譯器需要時才會合成。

2) 預設建構函式會初始化資料成員。錯!不會對資料成員進行初始化,只會初始化類物件成員。

4. 呼叫拷貝建構函式的三種情況

1) 明確的用一個物件初始化另一個類物件, 如 obj1 = obj2

2) 物件作為函式引數, 如 foo(obj)

3) 物件作為返回值,return obj;

拷貝建構函式也是在編譯器需要時會自動建立

5. (拷貝建構函式中)成員的初始化:簡而言之,就是深拷貝和淺拷貝的問題

如果類成員中只有基本資料型別和指標,那麼沒必要生成拷貝建構函式,資料成員按位拷貝即可。這就是按位逐次拷貝

6. 有四種情況不會出現按位逐次拷貝,即需要生成預設拷貝建構函式:

1) 類內的成員物件含有拷貝建構函式

2) 類繼承自其它基類,並且該基類含有拷貝建構函式

3) 類聲明瞭一個或多個虛擬函式

4) 類派生自一個繼承鏈,其中有一個或多個虛基類

前兩種情況是因為編譯器必須將基類的拷貝建構函式安插在自己生成的預設拷貝建構函式中。

對於虛擬函式或虛基類的情況,之所以不能用按位拷貝,是因為對於每個物件而言,由於繼承關係,指向虛擬函式表的指標可能是不同的,直接拷貝可能會出現錯誤的虛擬函式表

7. 以下情況應使用初始化列表:

1) 初始化引用成員時

2) 初始化常量成員時

3) 呼叫一個基類的建構函式並且其擁有引數

4) 呼叫一個成員類的建構函式且其擁有一組引數

使用函式初始化列表效率更高,因為賦值操作會構造臨時物件,而且還要呼叫拷貝建構函式和賦值操作符

8. 建構函式初始化列表不是一組函式呼叫

9. 注意:建構函式初始化列表中,成員的初始化順序是和成員宣告的順序一致,而不是和列表中寫的順序一致

一個典型的bug

class Temp {
public:
    int i;
    int j;
    Temp(int val) : j(val), i(j) { }

};

int main() {
    Temp t(100);
    cout << t.i << endl;
    cout << t.j << endl;

    return 0;
}

VS2017也不會針對此情況報錯,但是因為i是在j之前宣告,在使用初始化列表時,會先初始化i,也就是把尚未初始化的j賦值給i

可以通過以下程式碼解決

class Temp {
public:
    int i;
    int j;

    Temp(int val) : j(val) {
        i = j;
    }
};

10. 建構函式初始化列表中也可以呼叫函式,但是並不建議如此,因為無法預知該函式對建構函式的依賴程度,畢竟此時建構函式體尚未執行,也就是說資料成員還未初始化完畢,此時呼叫其他方法可能有問題

11. 題外話:this指標並不儲存在物件中,當物件的記憶體分配完畢時,就有this指標了,並不需要物件初始化之後才有