一、前言

    學習C++的同學一般都知道有建構函式這個東西,我相信很多同學的理解就是建構函式是用來初始化類成員的,是的,建構函式的本質確實是這樣的,但很多同學會有以下兩個誤解:

        (1)任何class如果沒有定義任何建構函式,編譯器就會幫你自動生成一個;

        (2)編譯器用合成出來的預設建構函式會“Class內的每一個data member”;

先不說這兩個觀點對不對,但至少他不嚴謹。

二、建構函式與預設建構函式

    這裡首先引出C++Primer裡面的關於建構函式的介紹:建構函式的任務是初始化類物件的資料成員,無論什麼時候只要class的物件被建立,就會執行建構函式。我的理解是這裡所說的建構函式是User Constructor Function,即使用者定義的建構函式,與其相對應的還有一種叫做預設建構函式。在網上看到關於預設建構函式的定義為:可以不用實參進行呼叫的建構函式,其包括兩種情況:(1)沒有帶明顯形參的建構函式;(2)提供了預設實參的建構函式。

    這裡之所以要介紹預設建構函式,是因為使用者可以自己定義一個預設建構函式,而編譯器也可以為我們合成一個預設建構函式,其實編譯器合成的建構函式確實是都不帶明顯形參的,因此我們經常把編譯器合成的建構函式稱為“合成預設建構函式”

三、編譯器需要的建構函式和程式需要的建構函式

    使用者定義一個以下類和它的建構函式

class A
{
public:

private:
    int a;
    int b;
};

class A沒有定義建構函式,按照前言中,大多數同學的第一個誤解,即任何class如果沒有定義預設建構函式,編譯器就會幫你自動生成一個,OK,那麼生成一個預設建構函式幹什麼呢?同學的潛意識裡是要求編譯器用生成的這個建構函式去初始化成員變數a和b。那這麼說是不是就能用這個類去例項化一個物件呢?因為只有例項化物件的時候才需要呼叫建構函式啊,這才是你自以為的編譯器合成的建構函式的用武之地啊。下面在VS2010IDE中進行驗證,結果是這樣的:

#include <iostream>
using namespace std;
class A
{
public:

public:
    int a;
    int b;
	
};

void main()
{
    A obj1;
    cout<<"obj1.a = "<<obj1.a<<endl;
}


    What?為什麼結果是這樣的呢?物件obj1沒有被初始化?不是說編譯器會生成一個預設建構函式來初始化他的成員變數嗎?其實實際上編譯器並沒有合成預設建構函式,因為你所謂的編譯器會合成一個建構函式來初始化其成員變數,那是你認為,自以為的,並不是編譯器以為的,也就不是編譯器需要的,而是程式的本身需要,你要訪問物件obj1的成員,程式當然需要先對其初始化,但是程式需要並不等於編譯器需要

    因此,這就可以推翻同學們的第一個誤解,很多時候編譯器並沒有幫你生成預設建構函式,即使你沒有定義任何建構函式,因為這不是編譯器的必須工作(儘管編譯器揹著你幹了很多有利於你的事情,但它也不是傻子,不是它的事情,它肯定不會幹)

    那既然編譯器不會幫你合成預設建構函式,那為什麼有很多人這樣說呢?我想這應該是斷章取義的結果。在《深入理解C++物件模型》中,侯捷大師引用C++ standard很明確的給了我們答案。第一個說明是:對於Class X,如果沒有任何使用者宣告的建構函式,那麼會有一個預設建構函式被隱式宣告出來,……一個被隱式宣告的預設建構函式是trivial(淺薄無能的、沒啥用的)建構函式。這就和同學的第一點誤解有很大的相似性,但僅僅是相似,而完全不同,因為這裡說的只是宣告出一個trivial的建構函式,並不是為誤解了的會合成出一個預設建構函式,聲明瞭並不代表一定要合成(即定義)

    那麼是不是都不會合成呢?也不是,候老師給的解答是:預設建構函式只有在需要的時候被編譯器產生(合成、定義)出來。關鍵字眼是“在需要的時候”,被誰需要?那當然是被編譯器需要,那什麼情況下編譯器才是需要的呢?就是下面四種情況:

    (1)帶有預設建構函式的類成員物件。即如果一個Class A沒有定義任何建構函式,但它含有一個Class B物件成員,而Class B有預設建構函式,那麼Class A的預設建構函式就是被編譯器需要的,因此編譯會合成Class A的預設建構函式。這裡Class A和B分別定義如下

class B
{
public:
    B()
    {
        m=1;
        n=2;
    }
private:
    int m;
    int n;
};
class A
{
public:
public:
    int a;
    int b;
    B obj2;
};
現在再訪問Class A的物件,如
void main()
{
	A obj1;
	cout<<obj1.a<<endl;
}
不會再出現Class obj1未定義的錯誤,但是輸出的結果為未知數,比如我的執行結果是:a=-858999460;這是一個未定義的整數,為什麼呢?程式沒有報錯,說明Class A的物件obj1已經正確被構造,也說明編譯器已經為我們合成了預設建構函式,初始化了Class B的物件obj2,但為什麼沒有初始化自己的成員物件a和b呢?還是那句話,編譯器只做自己該做的事情,不是它的事情它不會做,而成員變數初始化時程式需要的,則應該有程式設計師來完成,就算編譯器合成了一個有用的預設建構函式,它也不會初始化變數a和b,它只負責呼叫Class B的預設建構函式來初始化Class B的物件obj2。如下面的程式執行
void main()
{
    A obj1;
    cout<<"obj1.a = "<<obj1.a<<endl;
    cout<<"obj2.m = "<<obj1.obj2.m<<endl;
}
結果為:(能充分說明上述的分析)

上述結論,也能推翻同學們對前言中的第二個誤解,編譯器用合成出來的預設建構函式會“Class內的每一個data member”,其實編譯器它只做自己該做的事情。

    (2)帶有預設建構函式的基類。如果一個沒有任何建構函式的Class是繼承於一個帶有預設建構函式的基類,那麼派生類的預設建構函式也是被編譯器需要的,編譯器需要為使用者合成一個預設建構函式。

        因為構造子類之前需要先構造基類,即使派生類沒有定義任何建構函式,編譯器也會合成一個預設建構函式,用來呼叫基類的預設建構函式。

    (3)帶有虛擬函式的Class。即如果一個Class定義了虛擬函式,那麼編譯器也要合成預設建構函式。

    (4)虛擬繼承體系中的派生類,編譯器也需要合成預設建構函式。

        對於情況(3)和(4),編譯器之所以要生成預設建構函式,是因為編譯器需要在合成的預設建構函式中對虛擬函式和虛擬繼承機制進行支援,即需要設定或者重置虛擬函式表或者虛基類指標。

.