虛函數
虛繼承了一個函數類型的映射元素,按照虛繼承的說法,應該是間接獲得此函數的地址,但結果卻是間接獲得this參數的值。為了間接獲得函數的地址,C++又提出了一種文法--虛函數。在類型定義符“{}”中書寫函式宣告或定義時,在聲明或定義語句前加上關鍵字virtual即可,如下:
struct A { long a; virtual void ABC(), BCD(); };
void A::ABC() { a = 10; } void A::BCD() { a = 5; }
上面等同於下面:
struct A { void ( A::*pF )(); long a; void ABC(), BCD(); A(); };
void A::ABC() { a = 10; } void A::BCD() { a = 5; }
void ( A::*AVF[] )() = { A::ABC, A::BCD }; void A::A() { pF = AVF; }
這裡A的成員A::pF和之前的虛類表一樣,是一個指標,指向一個數組,這個數組被稱作虛函數表(Virtual Function Table),是一個函數指標的數組。這樣使用A::ABC時,將通過給出A::ABC在A::pF中的序號,由A::pF間接獲得,因此A a; a.ABC();將等同於( a.*( a.pF[0] ) )();。因此結構A的長度是8位元組,再看下面的代碼:
struct B : public A { long b; void ABC(); }; struct C : public A { long c; virtual void
ABC(); };
struct BB : public B { long bb; void ABC(); }; struct CC : public C { long cc; void
ABC(); };
void main() { BB bb; bb.ABC(); CC cc; cc.cc = 10; }
首先,上面執行bb.ABC()但沒有給出BB::ABC或B::ABC的定義,因此上面雖然編譯通過,但串連時將失敗。其次,上面沒有執行cc.ABC();但串連時卻會說CC::ABC未定義以表示這裡需要CC::ABC的地址,為什麼?因為產生了CC的執行個體,而CC::pF就需要在編譯器自動為CC產生的預設建構函式中被正確初始化,其需要CC::ABC的地址來填充。接著,給出如下的各函數定義。
void B::ABC() { b = 13; } void C::ABC() { c = 13; }
void BB::ABC() { bb = 13; b = 10; } void CC::ABC() { cc = 13; c = 10; }
如上後,對於bb.ABC();,等同於bb.BB::ABC();,雖然有三個BB::ABC的映射元素,但只有一個映射元素的類型為void( BB:: )(),其映射BB::ABC的地址。由於BB::ABC並沒有用virtual修飾,因此上面將等同於bb.BB::ABC();而不是( bb.*( pF[0] ) )();,bb將為13。對於cc.ABC();也是同樣的,cc將為13。
對於( ( B* )&bb )->ABC();,因為左側類型為B*,因此將為( ( B* )&bb )->B::ABC();,由於B::ABC並沒被定義成虛函數,因此這裡等同於( ( B* )&bb )->B::ABC();,b將為13。對於( ( C* )&cc )->ABC();,同樣將為( ( C* )&cc )->C::ABC();,但C::ABC被修飾成虛函數,則前面等同於C *pC = &cc; ( pC->*( pC->pF[0] ) )();。這裡先將cc轉換成C的執行個體,位移0。然後根據pC->pF[0]來間接獲得函數的地址,為CC::ABC,c將為10。因為cc是CC的執行個體,在其被構造時將填充cc.pF。
那麼如下:
void ( CC::*CCVF[] )() = { CC::ABC, CC::BCD }; CC::CC() { cc.pF = &CCVF; }
因此導致pC->ABC();結果調用的竟是CC::ABC而不是C::ABC,這正是由於虛的緣故而間接獲得函數地址導致的。同樣道理,對於( ( A* )&cc )->ABC();和( ( A* )&bb )->ABC();都將分別調用CC::ABC和BB::ABC。但請注意,( pC->*( pC->pF[0] ) )();中,pC是C*類型的,而pC->pF[0]返回的CC::ABC是void( CC:: )()類型的,而上面那樣做將如何進行執行個體的隱式類型轉換?如果不進行將導致操作錯誤的成員。可以像前面所說,讓CCVF的每個成員的長度為8個位元組,另外4個位元組記錄需要進行的位移。但大多數類其實並不需要位移(如上面的CC執行個體轉成A執行個體就位移0),此法有些浪費資源。VC對此給出的方法如下,假設CC::ABC對應的地址為6000,並假設下面標號P處的地址就為6000,而CC::A_thunk對應的地址為5990。
void CC::A_thunk( void *this )
{
this = ( ( char* )this ) + diff;
P:
// CC::ABC的正常代碼
}
因此pC->pF[0]的值為5990,而並不是CC::ABC對應的6000。上面的diff就是相應的偏
移,對於上面的例子,diff應該為0,所以實際中pC->pF[0]的值還是6000(因為位移為0,沒
必要是5990)。此法被稱作thunk,表示完成簡易功能的短小代碼。對於多重繼承,如下:
struct D : public A { long d; };
struct E : public B, public C, public D { long e; void ABC() { e = 10; } };
上面將有三個虛函數表,因為B、C和D都各內建了一個虛函數表(因為從A派生)。
結果上面等同於:
struct E
{
void ( E::*B_pF )(); long B_a, b;
void ( E::*C_pF )(); long C_a, c;
void ( E::*D_pF )(); long D_a, d; long e; void ABC() { e = 10; } E();
void E_C_thunk_ABC() { this = ( E* )( ( ( char* )this ) - 12 ); ABC(); }
void E_D_thunk_ABC() { this = ( E* )( ( ( char* )this ) - 24 ); ABC(); }
};
void ( E::*E_BVF[] )() = { E::ABC, E::BCD };
void ( E::*E_CVF[] )() = { E::E_C_thunk_ABC, E::BCD };
void ( E::*E_DVF[] )() = { E::E_D_thunk_ABC, E::BCD };
E::E() { B_pF = E_BVF; C_pF = E_CVF; D_pF = E_DVF; }
結果E e; C *pC = &e; pC->ABC(); D *pD = &e; pD->ABC();,假設e的地址為3000,則pC的值為3012,pD的值為3024。結果pC->pF的值就是E_CVF,pD->pF的值就是E_DVF,如此就解決了位移問題。同樣,對於前面的虛繼承,當類裡有多個虛類表時,如:
struct A {};
struct B : virtual public A{}; struct C : virtual public A{}; struct D : virtual public A{};
struct E : public B, public C, public D {};
這是E將有三個虛類表,並且每個虛類表都將在E的預設建構函式中被正確初始化以保證虛繼承的含義--間接獲得。而上面的虛函數表的初始化之所以那麼複雜也都只是為了保證間接獲得的正確性。
應注意上面將E_BVF的類型定義為void( E::*[] )()只是由於示範,希望在代碼上盡量符合文法而那樣寫,並不表示虛函數的類型只能是void( E:: )()。實際中的虛函數表只不過是一個數組,每個元素的大小都為4位元組以記錄一個地址而已。因此也可如下:
struct A { virtual void ABC(); virtual float ABC( double ); };
struct B : public A { void ABC(); float ABC( double ); };
則B b; A *pA = &b; pA->ABC();將調用類型為void( B:: )()的B::ABC,而pA->ABC( 34 );將調用類型為float( B:: )( double )的B::ABC。它們屬於重載函數,即使名字相同也都是兩個不同的虛函數。還應注意virtual和之前的public等,都只是從文法上提供給編譯器一些資訊,它們給出的資訊都是針對某些特殊情況的,而不是所有在使用數位地方都適用,因此不能作為數位類型。所以virtual不是類型修飾符,它修飾一個成員函數只是告訴編譯器在運用那個成員函數的地方都應該間接獲得其地址。
為什麼要提供虛這個概念?即虛函數和虛繼承的意義是什麼?出於篇幅限制,將在本文的下篇給出它們意義的討論,即時說明多態性和執行個體複製等問題。