C 特性探尋 建構函式和析構函式

2021-04-01 16:56:48 字數 3297 閱讀 4834

建構函式提供了一種機制,通過它有機會完成必要的初始化工作,從而使物件成為有意義

的存在物,而不僅僅只是一塊原始的空間。

但是,我們逐漸了解到,建構函式具有的地位,不僅對於使用者(程式設計師),對於編譯器履

行職責也極為重要。通過這個機制,它讓c++的一些基本的特性,如繼承、多型得到了正確

的貫徹和表現。

首先不難理解的一點是在建構函式中,要確保基類物件的正確構造,如果是從基類繼承的

話。因為繼承類物件至少可以被「低」看為乙個基類物件,具有後者的所有行為和表現,

所以基類的建構函式首先被呼叫。如果所謂的基類也是從其他類繼承過來的,這就形成了

乙個呼叫鏈。最後的情況是,最基礎的類的建構函式首先被執行,然後才是上一層的構造

函式,如此到最外層的繼承類。這個過程必須是嚴格有序的。如果沒有這個次序保證,繼

承類就有機會在基類還沒構建好的情況下就訪問基類的資料或函式,這將導致不可預料的

、災難性的後果。

只有首先確保基類的正確構造,接下來才能進行繼承類本身的構造。因為類中可能含有成

員物件,必須保證這些物件也被構造,按照一定的次序(一般就是變數宣告的次序)。可

能其中乙個或一些成員物件的構造需要引數,這需要在類的建構函式中提供所有需要的參

數(構成所謂顯式的「初始化列表」)。

看起來這就是建構函式的全部隱含(或半隱含)工作?不是的。我們忽視了乙個極為重要

的東西,我稱之為類關聯資訊(表)。類關聯資訊是編譯時生成的、為執行時所需的類的

附加資料(可以認為這些資料放在全域性資料區中)。我們熟悉的虛函式表(v-table),我

把它歸在類關聯資訊的範疇之中(最初,我以為虛函式表就是全部,但這個理解有點狹義

)。此外,還包含執行時型別資訊(rtti)。此外,不排除我們還不清楚的其它輔助資料

(總之,類關聯資訊是一種廣義的、統一的稱呼)。如果編譯器為類生成了類關聯資訊,

那麼毫無疑問,必須在建構函式中將它與當前物件(類的例項)關聯起來(也許簡單到只

需要設定乙個指標即可)。

例如,如果類中含有虛函式,或者,它覆蓋了基類中的虛函式(兩種情況下都意味著類有

自己的虛函式表)。那麼,設定正確的關聯後,將存在乙個指標(v-ptr),它正確地指向

了該虛函式表(v-table)。此後,多型才能表現出所期望的正確行為。

再次指出(次序的重要性),設定關聯,或者狹義地說,設定v-ptr必須發生在對基類構造

函式的呼叫之後。因為,繼承類如果有自己的虛函式表,那麼v-ptr會被改寫,以指向該錶

,即使此前v-ptr已經被基類所設定。這是合法的,也正是所期望的。但是語義上我們絕不

允許基類可以改寫繼承類所設定的v-ptr。如果v-ptr設定發生在基類構造呼叫之前,那麼

這種非法的一幕就會發生。

所有上述的事情完成之後(對於使用者來說它們幾乎是隱含的),才真正開始執行使用者的初

始化**。

在建構函式中呼叫虛函式,會發生什麼?發生的情況也許是始料未及的。當前類的物件正

在構建,v-ptr指向的是當前類的虛函式表。此時,還沒到繼承類執行它自己的**(如設

置v-ptr,執行初始化**)的時候,那一切發生在當前類的建構函式執行完畢之後。所以

,將要執行的是本類的虛函式版本,而不是可能被覆蓋的繼承類的版本。這裡有乙個反面

的證據。假如v-ptr設定發生在基類構造函式呼叫之前,讓我們有機會呼叫繼承類的虛函式

版本,這意味著什麼?繼承類還沒有完成初始化(因而物件還沒有構建好),我們企圖在

乙個沒有構建好的物件上執行它的成員函式,可以想見後果是災難性的。

類的建構函式何時被呼叫?在物件被建立的時候。物件可能位於棧上,全域性資料區,或堆

上。物件可能會在宣告的地方建立,這樣的物件位於棧上或全域性資料區。物件也可以使用

new操作符動態地建立,這樣的物件將位於堆上。

析構函式

析構函式提供了一種和建構函式相反的機制,允許在銷毀乙個物件之前(亦即**物件所

占用的空間),讓物件釋放自己所使用的資源。再次,這裡所關心的是語言底層所發生的

事情。與建構函式的執行次序剛好相反,析構函式從最外層的類開始執行,最基礎的類的析構函

數最後執行,看起來就象一層層的剝殼。這個次序要得到嚴格保證的理由也是明顯的(違

類析構函式。

析構函式在物件行將被銷毀時呼叫。當物件超出其作用域,例如乙個函式內部的區域性變數

,在函式返回時將被自動銷毀。對於在堆上建立的物件,我們只能通過物件指標p,執行d

elete p來銷毀它。現在就有乙個問題。p指向了某個型別(例如a)的物件,但是卻可能是

通過上溯造型(upcast)得來的,因此它指向的實際上是乙個b的物件,那麼將發生什麼?

顯然,正確的做法是把物件作為b的例項來銷毀。也就是說,呼叫b的析構函式。我們也許

很快想到,首先,在執行時刻可以知道這個物件「實際上」是什麼型別(那個最晚的派生

類,本例中假定就是b),例如通過rtti。然後,根據實際的型別,「找到」並呼叫該類的

析構函式。然而,這個想法不會自然實現,需要在編譯時把類的型別資訊和乙個指向析構

函式的指標關聯起來。不過,上述考慮似乎複雜了點。我們可以把這個析構函式指標放入

虛函式表中(某個特別的位置)。理由是,與虛函式非常相似,儲存這個析構函式指標的

slot可以為繼承類所覆蓋(從而指向繼承類的析構函式),而且繼承類應該總是覆蓋它(

然而下面將看到,實際的情況與「總是覆蓋」有出入)。

無論怎樣,析構函式指標是儲存在前面稱之的類關聯資訊(表)中,編譯器不難找到。因

而當通過物件指標(即使已經上溯造型)執行刪除操作時,總是能夠呼叫正確的析構函式

。但是,在多個編譯器的試驗發現,只有把基類的析構函式宣告為虛的,繼承類才會覆蓋

它。看來的確採用虛函式的技術來對待析構函式了。如果基類的析構函式沒有宣告為虛的

,則執行前面的delete p時,只有a的析構函式得到執行,而b的沒有執行!

這是令人驚訝的。竟然允許「不完全」析構的情況發生,把選擇權以及責任交給了程式設計師

!我不能確定這麼做的主要理由是什麼。也許還是因為c++如此看重效率,它迫使程式設計師在

必要的時候顯式宣告他的需求,因為類似虛函式的間接呼叫要占用額外的空間和時間。

前面考察了虛函式在建構函式中的行為,繼續來看一下在析構函式中的情形。因為析構過

程是自外向裡的,當前類在呼叫虛函式的時候,其繼承類此前已經完成了析構,不再是可

用的。因而,是不可以呼叫繼承類的虛函式版本的。結果,呼叫的仍然是本地版本(和構

造函式中的結論一樣,虛函式機制被忽略)。

note: 構造/析構函式中虛函式的行為,我又一次從「thingking in c++」中看到的。tha

nks to bruce eckel!

C 建構函式和析構函式

1.建構函式是類的一種特殊方法,每次建立類的例項都會呼叫它。在建立乙個類的例項時,建構函式就像乙個方法一樣被呼叫,但不返回值。語法格式 訪問修飾符 類名 特性 1 其名字必須與類名相同,例如 public class myclass 2 不能被直接呼叫,必須通過new運算子來 呼叫。publiccl...

C 建構函式和析構函式

建構函式 class rectangel rectangel int l,int w 這是帶引數的建構函式,建構函式都是沒有返回值,並且和該類同名 int area 這是另一種形式的內聯函式,把宣告和定義寫在一起的也是內聯函式 private int length int width 析構函式 cl...

c 建構函式和析構函式

1.c 的建構函式有預設建構函式,一般的建構函式,賦值建構函式,拷貝建構函式 複製建構函式 強制型別轉化建構函式。2.如果沒有定義建構函式和析構函式,則c 編譯器會按照 位拷貝 的方式提供預設的建構函式 不初始化 預設的賦值建構函式 淺賦值 預設的拷貝建構函式 淺拷貝 預設的析構函式。位拷貝要小心指...