深入理解C++對象模型之構造函數

一、前言

    學習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),編譯器之所以要生成默認構造函數,是因爲編譯器需要在合成的默認構造函數中對虛函數和虛擬繼承機制進行支持,即需要設定或者重置虛函數表或者虛基類指針。



發佈了40 篇原創文章 · 獲贊 127 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章