http://anwj336.blog.163.com/blog/static/8941520920106791516915/
一、建構函式避免調用虛函數的問題
在建構函式中調用虛成員函數,雖然這是個不很常用的技術,但研究一下可以加深對虛函數機制及物件建構過程的理解。這個問題也和一般直觀上的認識有所差異。先看看下面的兩個類定義。
struct C180
{
C180() {
foo();
this->foo();
}
virtual foo() {
cout << "<< C180.foo this: " << this << " vtadr: " << *(void**)this << endl;
}
};
struct C190 : public C180
{
C190() {}
virtual foo() {
cout << "<< C190.foo this: " << this << " vtadr: " << *(void**)this << endl;
}
};
父類中有一個虛函數,並且父類在它的建構函式中調用了這個虛函數,調用時它採用了兩種方法一種是直接調用,一種是通過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只是有安全檢查,不能強制將子類變成父類!