C 合成預設建構函式的真相

2022-01-21 21:41:29 字數 4672 閱讀 6144

對於c++預設建構函式,我曾經有兩點誤解:

第乙個誤解來自於我學習c++的第一本書 《c++ primer》,在書中392頁:「只有當乙個類沒有定義建構函式時,編譯器才會自動生成乙個預設建構函式」。

實際上這句話也沒有說錯,它說明了預設建構函式定義的必要非充分條件,然而卻給當時初學c++的我造成了一定的誤解。

第二個誤解依舊來自於primer中的一句話:「合成的預設建構函式使用與變數初始化相同的規則來初始化成員。具有類型別的成員通過執行各自的預設建構函式來進行初始化」。然而這也是我理解的片面,因為primer也說到了:「如果類包含內建或復合型別的成員,則該類不應該依賴於合成的預設建構函式」,言下之意就是合成的預設建構函式並不會初始化內建或復合型別的成員。

總結了我有這些誤解的原因,第一是初學時知識體系沒形成,對primer中所說的內容沒有真正的理解,第二就是primer在某種程度上的確不是c++初學者能看懂的書,或許看時覺得懂了,卻是遺漏了很多知識。也說明了primer 是座寶庫,常常回顧將會有新的感悟。

讓我對上面兩個觀點產生疑惑,是在看《effective c++》時,條款05《了解c++預設編寫並呼叫哪些函式》中說到「….惟有當這些函式被需要(被呼叫),它們才會被編譯器建立出來。」 (「這些函式「指的是編譯器版本的複製建構函式、賦值操作符和析構函式,還包括了預設建構函式。)也就是說,預設建構函式「被需要」的時候編譯器才會幫我們合成,那什麼情況才是預設建構函式」被需要「呢?這個問題《effective c++》並沒有給出答案,直到看了《深度探索c++物件模型》,才明白了編譯器何時才會幫我們合成乙個預設建構函式。

我寫這篇文章的目的是給和我有同樣誤解或疑惑的c++初學者看的,如果你對合成預設建構函式已有充分的認識,請忽略本文的內容。

預設建構函式是可以不用實參進行呼叫的建構函式,它包括了以下兩種情況:

沒有帶明顯形參的建構函式。

提供了預設實參的建構函式。

類設計者可以自己寫乙個預設建構函式。編譯器幫我們寫的預設建構函式,稱為「合成的預設建構函式」。強調「沒有帶明顯形參」的原因是,編譯器總是會為我們的建構函式形參表插入乙個隱含的this指標,所以」本質上」是沒有不帶形參的建構函式的,只有不帶明顯形參的建構函式,它就是預設建構函式。

如果定義乙個物件時沒有提供初始化式,就使用預設建構函式。例如:

class

a; //

預設建構函式

bool

istrue;

intnum;

};int

main()

前面提到在《effective c++》中指出惟有預設建構函式」被需要「的時候編譯器才會合成預設建構函式。關鍵字眼是」被需要「。被誰需要?做什麼事情?像下面這段**,預設建構函式」被需要「了嗎?

classa;

intmain()

你可能認為這裡定義類物件a的時候沒有提供引數且a沒有定義預設建構函式,編譯器肯定是合成了乙個預設建構函式並呼叫它來初始化a的資料成員,實則不是。當你試圖檢視合成預設建構函式把資料成員num初始化為什麼值的時候,你會發現編譯器甚至都讓你執行不了程式:當類只含有內建型別或復合型別的成員時,編譯器是不會為類合成預設建構函式的,這種類並不符合」被需要「的條件,甚至當類滿足「被需要」條件,編譯器合成了預設建構函式時,類中內建型別與復合型別資料成員依然不會在預設建構函式中進行初始化。primer中也有提到:「如果類包含內建或復合型別的成員,則該類不應該依賴於合成的預設建構函式「。上面**中,預設建構函式」被需要「是對程式來說的,程式需要istrue被初始化以便可以進行條件判斷,需要num被初始化以便可以輸出。然而這種需要並不會促使編譯器合成預設建構函式。惟有被編譯器所需要時,編譯器才會合成預設建構函式。那怎樣的類才是編譯器需要合成預設建構函式的呢?

總結:

合成預設建構函式總是不會初始化類的內建型別及復合型別的資料成員。

分清楚預設建構函式被程式需要與被編譯器需要,只有被編譯器需要的預設建構函式,編譯器才會合成它。

以下四種情況的類,編譯器總是需要預設建構函式完成某些工作:

1. 含有類物件資料成員,該類物件型別有預設建構函式。

如果乙個類沒有任何建構函式,但是它含有乙個類物件資料成員,且該類物件型別有預設建構函式,那麼編譯器就會為該類合成乙個預設建構函式,不過這個合成操作只有在建構函式真正需要被呼叫的時候才會發生。舉個例子,編譯器將為類b合成乙個預設建構函式:

class

a; //

預設建構函式

bool

istrue;

intnum;

};classb;

intmain()

被合成的預設建構函式做了什麼事情?大概如下面這樣:

b::b()

被合成的預設建構函式內只含必要的**,它完成了對資料成員a的初始化,但不產生任何**來初始化b::b。正如上面所說,初始化類的內建型別或復合型別成員是程式的責任而不是編譯器的責任。為了滿足程式的需要,我們一般會自己寫建構函式來對b::b進行初始化,像這樣:

b::b()

如果類中有多種類物件成員,則編譯器按照這些類物件成員宣告的順序,在建構函式按順序插入呼叫各個類預設建構函式的**。當乙個類派生自乙個含有預設建構函式的基類時,該類也符合編譯器需要合成預設建構函式的條件。編譯器合成的預設建構函式將根據基類宣告順序呼叫上層的基類預設建構函式。同樣的道理,如果設計者定義了多個建構函式,編譯器將不會重新定義乙個合成預設建構函式,而是把合成預設建構函式的內容插入到每乙個建構函式中去。類帶有虛函式可以分為兩種情況:

類本身定義了自己的虛函式

類從繼承體系中繼承了虛函式(成員函式一旦被宣告為虛函式,繼承不會改變虛函式的」虛性質「)。

這兩種情況都使乙個類成為帶有虛函式的類。這樣的類也滿足編譯器需要合成預設建構函式的類,原因是含有虛函式的類物件都含有乙個虛表指標vptr,編譯器需要對vptr設定初值以滿足虛函式機制的正確執行,編譯器會把這個設定初值的操作放在預設建構函式中。如果設計者沒有定義任何乙個預設建構函式,則編譯器會合成乙個預設建構函式完成上述操作,否則,編譯器將在每乙個建構函式中插入**來完成相同的事情。

虛基類的概念是存在於類與類之間的,是一種相對的概念。例如類a虛繼承於類x,則對於a來說,類x是類a的虛基類,而不能說類x就是乙個虛基類。虛基類是為了解決多重繼承下確保子類物件中每個父類只含有乙個副本的問題,比如菱形繼承。如下圖:

於是,類a物件中含有乙份類x物件,類c中也含有乙份類x物件,當我們遇上如下**時:

class x  ;

class a : public

virtual x;

class b : public

virtual x;

class c : public a, public b;

void function(a *pa)

intmain()

函式function引數pa的真正型別是可以改變的,既可以把a物件指標賦值給pa,也可以把物件指標賦值給pa,在編譯階段並無法確定pa儲存的i是屬於a還是c的虛基類物件。為了解決這問題,編譯器將產生乙個指向虛基類x的指標,使得程式得以在執行期確定經由pa而訪問的x::i的實際儲存位置。這個指標的安插,編譯器將會在合成預設建構函式中完成,同樣的,如果設計者已經寫了多個建構函式,那麼編譯器不會重新寫預設建構函式,而是把虛基類指標的安插**插入已有的建構函式中。a)   任何類如果沒有定義建構函式,則編譯器會幫我們合成乙個預設建構函式。b)   合成預設建構函式會對類中的每乙個資料成員進行初始化。

只有在編譯器需要預設建構函式來完成編譯任務的時候,編譯器才會為沒有任何建構函式的類合成乙個預設建構函式,或者是把這些操作插入到已有的建構函式中去。

編譯器需要預設建構函式的四種情況,總結起來就是:

a)   呼叫物件成員或基類的預設建構函式。

b)   為物件初始化虛表指標與虛基類指標。

預設建構函式和合成預設建構函式

當我們沒有為類中的物件提供初始值,此時就會執行預設初始化,類會通過乙個特殊的建構函式來控制預設初始化過程,這個函式叫做預設建構函式,這個函式並不需要任何的實參,但是如果我們的類沒有顯式地定義建構函式,那麼編譯器就會為我們隱式地定義乙個預設建構函式 只要沒有顯式定義建構函式,編譯器就會提供預設建構函式...

C 預設合成的函式的誤解

最近看 深度探索c 物件模型 時,對物件的構造有了粗略的了解,同時也發現了其中的一些內容和我在其他書本中了解的有些不同的地方。在這裡記錄下來。effective c 一書中 條款05 了解c 編寫並呼叫哪些函式 當然這些內容在 c primer 也有講到。其中講的是class empty 乙個空類在...

C 預設建構函式

c 預設建構函式 一直認為若程式設計師沒有自己定義無引數的建構函式,那麼編譯器會自動生成預設建構函式,來進行對成員函式的初始化,但這種認為是有誤的,不全面的.預設的建構函式分為有用的和無用的,所謂無用的預設建構函式就是乙個空函式 什麼操作也不做,而有用的預設建構函式是可以初始化成員的函式。對建構函式...