萬盛學電腦網

 萬盛學電腦網 >> 網絡編程 >> 編程語言綜合 >> C++學習從零開始(四)

C++學習從零開始(四)

  虛函數

  虛繼承了一個函數類型的映射元素,按照虛繼承的說法,應該是間接獲得此函數的地址,但結果卻是間接獲得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不是類型修飾符,它修飾一個成員函數只是告訴編譯器在運用那個成員函數的地方都應該間接獲得其地址。

  為什麼要提供虛這個概念?即虛函數和虛繼承的意義是什麼?出於篇幅限制,將在本文的下篇給出它們意義的討論,即時說明多態性和實例復制等問題。

copyright © 萬盛學電腦網 all rights reserved