C 中建構函式和析構函式避免呼叫虛函式的問題

2021-06-21 01:57:55 字數 3071 閱讀 9417

gdc注:在看《c++ primer中文版第4版》第15.4.5小節 「建構函式和析構函式中的虛函式」時,沒怎麼理解。到網上搜了搜,發現了該篇比較好的文章。

該篇文章首先給出乙個例子,然後分析**的執行流程,然後我就理解了。最後,該篇文章給出了一些理論知識。值得一看。

一、建構函式避免呼叫虛函式的問題

在建構函式中呼叫虛成員函式,雖然這是個不很常用的技術,但研究一下可以加深對虛函式機制及物件構造過程的理解。這個問題也和一般直觀上的認識有所差異。先看看下面的兩個類定義。

struct c180

virtual foo()

};struct c190 : public c180

virtual foo()

};父類中有乙個虛函式,並且父類在它的建構函式中呼叫了這個虛函式,呼叫時它採用了兩種方法一種是直接呼叫,一種是通過this指標呼叫。同時子類又重寫了這個虛函式。

我們可以來**一下如果構造乙個c190的物件會發生什麼情況。

我們知道,在構造乙個物件時,過程是這樣的:

1) 首先會按物件的大小得到一塊記憶體(在heap上或在stack上),

2) 把指向這塊記憶體的指標做為this指標來呼叫類的建構函式,對這塊記憶體進行初始化。

3) 如果物件有父類就會先呼叫父類的建構函式(並依次遞迴),如果有多個父類(多重繼承)會依次對父類的建構函式進行呼叫,並會適當的調整this指標的位置。

在呼叫完所有的父類的建構函式後,再執行自己的**。

照上面的分析構造c190時也會呼叫c180的建構函式,這時在c180建構函式中的第乙個foo呼叫為靜態繫結會呼叫到c180::foo()函式。

二個foo呼叫是通過指標呼叫的,這時多型行為會發生,應該呼叫的是c190::foo()函式。

執行如下**:

c190 obj;

obj.foo();

結果為:

<< c180.foo this: 0012f7a4 vtadr: 0045c404

<< c180.foo this: 0012f7a4 vtadr: 0045c404

<< c190.foo this: 0012f7a4 vtadr: 0045c400

和我們的分析大相徑庭。第一行是在c180中執行foo()函式得到的,這裡的foo()當然是呼叫c180中的foo()函式。

第二行是呼叫c190中的this->foo()得到的,此時this指向的應該是c190的虛表位址,按照呼叫規則,應該是動態繫結,即,此時若派生類對該虛函式實現過,則應該呼叫派生類的虛函式,這裡是乙個例外,下面會詳細講到。 至此,c190的父類的建構函式執行完畢,轉而執行c190的建構函式,但是這裡c190的建構函式什麼都沒有。第三行是在main函式中呼叫obj.foo()得到的,這裡直接進入c190執行就可以了。 這裡必須注意一點,就是前兩行和第三行的虛表是不同的,這是因為前兩行的虛表是c180的虛表,而第三行的虛表是c190的虛表。 其實這正是奧秘所在。

為此我查了一下c++標準規範。在12.7.3條中有明確的規定。這是一種特例,在這種情況下,即在構造子類時呼叫父類的建構函式,而父類的建構函式中又呼叫了虛成員函式,這個虛成員函式即使被子類重寫,也不允許發生多型的行為。即,這時必須要呼叫父類的虛

函式,而不子類重寫後的虛函式。

我想這樣做的原因是因為在呼叫父類的建構函式時,物件中屬於子類部分的成員變數是肯定還沒有初始化的,因為子類建構函式中的**還沒有被執行。如果這時允許多型的行為,即通過父類的構造函式呼叫到了子類的虛函式,而這個虛函式要訪問屬於子類的資料成員時就有可能出錯。

二、為什麼建構函式不可以呼叫虛的函式?

第一,在概念上,建構函式的工作是把物件變成存在物。在任何建構函式中,物件可能只是部分被形成—我們只能知道基類已被初始化了,但不知道哪個類是從這個基類繼承來的。然而,虛函式是「向前」和「向外」進行呼叫。它能呼叫在派生類中的函式。如果我們在建構函式中也這樣做,那麼我們所呼叫的函式可能操作還沒有被初始化的成員,這將導致災難的發生。

第二,當乙個建構函式被呼叫時,它做的首要的事情之一是初始化它的vptr。因此,它只能知道它是「當前」類的,而完全忽視這個物件後面是否還有繼承者。當編譯器為這個建構函式產生**時,它是為這個類的建構函式產生**--既不是為基類,也不是為它的派生類(因為類不知道誰繼承它)。

所以它使用的vptr必須是對於這個類的vtable。而且,只要它是最後的構造函式呼叫,那麼在這個物件的生命期內,vptr將保持被初始化為指向這個vtable。但如果接著還有乙個更晚派生的建構函式被呼叫,這個建構函式又將設定vptr指向它的vtable,等直到最後的建構函式結束。vptr的狀態是由被最後呼叫的建構函式確定的。這就是為什麼構造函式呼叫是從基類到更加派生類順序的另乙個理由。

但是,當這一系列構造函式呼叫正發生時,每個建構函式都已經設定vptr指向它自己的vtable。如果函式呼叫使用虛機制,它將只產生通過它自己的vtable的呼叫,而不是最後的vtable(所有建構函式被呼叫後才會有最後的vtable)。

另外,許多編譯器認識到,如果在建構函式中進行虛函式呼叫,應該使用早**,因為它們知道晚**將只對本地函式產生呼叫。無論哪種情況,在建構函式中呼叫虛函式都沒有結果。

所以,建構函式不能是虛的,然而,對於析構函式來說他常常是,而且最好是虛的!這個此處暫時不議..

三、析構函式中呼叫虛函式

在物件的析構期間,存在與上面同樣的邏輯。一旦乙個派生類的析構器執行起來,該物件的派生類資料成員就被假設為是未定義的值,這樣以來,c++就把它們當做是不存在一樣。一旦進入到基類的析構器中,該物件即變為乙個基類物件,c++中各個部分(虛函式,dynamic_cast運算子等等

)都這樣處理。dynamic_cast只是有安全檢查,不能強制將子類變成父類!

C 中建構函式和析構函式避免呼叫虛函式的問題

一 建構函式避免呼叫虛函式的問題 在建構函式中呼叫虛成員函式,雖然這是個不很常用的技術,但研究一下可以加深對虛函式機制及物件構造過程的理解。這個問題也和一般直觀上的認識有所差異。先看看下面的兩個類定義。struct c180 virtual foo struct c190 public c180 v...

C 中建構函式和析構函式避免呼叫虛函式的問題

一 建構函式避免呼叫虛函式的問題 在建構函式中呼叫虛成員函式,雖然這是個不很常用的技術,但研究一下可以加深對虛函式機制及物件構造過程的理解。這個問題也和一般直觀上的認識有所差異。先看看下面的兩個類定義。include using namespace std struct c180 virtual v...

C 顯示呼叫建構函式和析構函式

建構函式和析構函式可不可以顯示呼叫 class a a void main 此時的輸出結果是 a constructor a constructor 顯示呼叫建構函式的結果。a destrucotr 顯示呼叫析構函式的結果,此時物件並沒有銷毀。a destructor 物件銷毀時自動呼叫析構函式。總...