天天看點

C++ - 虛基類、虛函數與純虛函數

虛基類

       在說明其作用前先看一段代碼

class A

{

public:

   int iValue;

};

class B:public A

   void bPrintf(){cout<<"This is class B"<<endl;};

class C:public A

   void cPrintf(){cout<<"This is class C"<<endl;};

class D:public B,public C

   void dPrintf(){cout<<"This is class D"<<endl;};

void main()

   D d;

   cout<<d.iValue<<endl; //錯誤,不明确的通路

   cout<<d.A::iValue<<endl; //正确

   cout<<d.B::iValue<<endl; //正确

   cout<<d.C::iValue<<endl; //正确

}

 從代碼中可以看出類B C都繼承了類A的iValue成員,是以類B C都有一個成員變量iValue ,而類D又繼承了B C,這樣類D就有一個重名的成員 iValue(一個是從類B中繼承過來的,一個是從類C中繼承過來的).在主函數中調用d.iValue 因為類D有一個重名的成員iValue編譯器不知道調用 從誰繼承過來的iValue是以就産生的二義性的問題.正确的做法應該是加上作用域限定符 d.B::iValue 表示調用從B類繼承過來的iValue。不過 類D的執行個體中就有多個iValue的執行個體,就會占用記憶體空間。是以C++中就引用了虛基類的概念,來解決這個問題。

class B:virtual public A

class C:virtual public A

   cout<<d.iValue<<endl; //正确

 在繼承的類的前面加上virtual關鍵字表示被繼承的類是一個虛基類,它的被繼承成員在派生類中隻保留一個執行個體。例如iValue這個成員,從類 D這個角度上來看,它是從類B與類C繼承過來的,而類B C又是從類A繼承過來的,但它們隻保留一個副本。是以在主函數中調用d.iValue時就不 會産生錯誤。 

虛函數

       還是先看代碼

   void funPrint(){cout<<"funPrint of class A"<<endl;};

   void funPrint(){cout<<"funPrint of class B"<<endl;};

   A *p; //定義基類的指針

   A a;

   B b;

   p=&a;

   p->funPrint();

   p=&b;

 大家以為這段代碼的輸出結果是什麼?有的人可能會馬上回答funPrint of class A 與 funPrint of class B 因為第一次輸出是引用類A的實 例啊,第二次輸出是引用類B的執行個體啊。那麼我告訴你這樣想就錯啦,答案是funPrint of class A 與 funPrint of class A 至于為什麼輸出 這樣的結果不在本文讨論的範圍之内;你就記住,不管引用的執行個體是哪個類的當你調用的時候系統會調用左值那個對象所屬類的方法。比如說 上面的代碼類A B都有一個funPrint 函數,因為p是一個A類的指針,是以不管你将p指針指向類A或是類B,最終調用的函數都是類A的funPrint 函數。這就是靜态聯篇,編譯器在編譯的時候就已經确定好了。可是如果我想實作跟據執行個體的不同來動态決定調用哪個函數呢?這就須要用到 虛函數(也就是動态聯篇)。

   virtual void funPrint(){cout<<"funPrint of class A"<<endl;};

   virtual void funPrint(){cout<<"funPrint of class B"<<endl;};

多态性可分為兩類:靜态多态和動态多态。函數重載和運算符重載實作的多态屬于靜态多态,動态多态性是通過虛函數實作的。

每個含有虛函數的類有一張虛函數表(vtbl),表中每一項是一個虛函數的位址, 也就是說,虛函數表的每一項是一個虛函數的指針。

沒有虛函數的C++類,是不會有虛函數表的。

兩張圖:

C++ - 虛基類、虛函數與純虛函數
C++ - 虛基類、虛函數與純虛函數

 在基類的成員函數前加virtual關鍵字表示這個函數是一個虛函數,所謂虛函數就是在編譯的時候不确定要調用哪個函數,而是動态決定将要調 用哪個函數,要實作虛函數必須派生類的函數名與基類相同,參數名參數類型等也要與基類相同。但派生類中的virtual關鍵字可以省略,也表 示這是一個虛函數。下面來解決一下代碼,聲明一個基類的指針(必須是基類,反之則不行)p,把p指向類A的執行個體a,調用funPrint函數,這 時系統會判斷p所指向的執行個體的類型,如果是A類的執行個體就調用A類的funPrint函數,如果是B類的執行個體就調用B類的funPrint函數。

純虛函數 

    與其叫純虛函數還不如叫抽象類,它隻是聲明一個函數但不實作它,讓派生類去實作它,其實這也很好了解。 

class Vehicle

   virtual void PrintTyre()=0; //純虛函數是這樣定義的

class Camion:public Vehicle

   virtual void PrintTyre(){cout<<"Camion tyre four"<<endl;};

class Bike:public Vehicle

   virtual void PrintTyre(){cout<<"Bike tyre two"<<endl;};

   Camion c;

   Bike b;

   b.PrintTyre();

   c.PrintTyre();

 如上代碼,定義了一個交通工具類(Vehicle),類中有一函數可列印出交通工具的輪胎個數,但交通工具很多輪胎個數自然也就不确定,是以 就把它定義為純虛函數,也就是光定義函數名不去實作它,類Camion繼承了Vehicle并實作了裡面的代碼,列印出有4個輪胎。Bike類也是一樣。 有一點須要注意一下,純虛函數不能實化化,但可以聲明指針。

總結

    虛基類 

    1, 一個類可以在一個類族中既被用作虛基類,也被用作非虛基類。 

    2, 在派生類的對象中,同名的虛基類隻産生一個虛基類子對象,而某個非虛基類産生各自的子對象。 

    3, 虛基類子對象是由最派生類的構造函數通過調用虛基類的構造函數進行初始化的。 

    4, 最派生類是指在繼承結構中建立對象時所指定的類。 

    5, 派生類的構造函數的成員初始化清單中必須列出對虛基類構造函數的調用;如果未列出,則表示使用該虛基類的預設構造函數。 

    6, 從虛基類直接或間接派生的派生類中的構造函數的成員初始化清單中都要列出對虛基類構造函數的調用。但隻有用于建立對象的最派生 類的構造函數調用虛基類的構造函數,而該派生類的所有基類中列出的對虛基類的構造函數的調用在執行中被忽略,進而保證對虛基類子對象 隻初始化一次。 

    7, 在一個成員初始化清單中同時出現對虛基類和非虛基類構造函數的調用時,虛基類的構造函數先于非虛基類的構造函數執行。 

    虛函數 

    1, 虛函數是非靜态的、非内聯的成員函數,而不能是友元函數,但虛函數可以在另一個類中被聲明為友元函數。 

    2, 虛函數聲明隻能出現在類定義的函數原型聲明中,而不能在成員函數的函數體實作的時候聲明。 

    3, 一個虛函數無論被公有繼承多少次,它仍然保持其虛函數的特性。 

    4, 若類中一個成員函數被說明為虛函數,則該成員函數在派生類中可能有不同的實作。當使用該成員函數操作指針或引用所辨別的對象時 ,對該成員函數調用可采用動态聯編。 

    5, 定義了虛函數後,程式中聲明的指向基類的指針就可以指向其派生類。在執行過程中,該函數可以不斷改變它所指向的對象,調用不同 版本的成員函數,而且這些動作都是在運作時動态實作的。虛函數充分展現了面向對象程式設計的動态多态性。 純虛函數 版本的成員函數,而且這些動作都是在運作時動态實作的。虛函數充分展現了面向對象程式設計的動态多态性。

    純虛函數 

    1, 當在基類中不能為虛函數給出一個有意義的實作時,可以将其聲明為純虛函數,其實作留待派生類完成。 

    2, 純虛函數的作用是為派生類提供一個一緻的接口。 

    3, 純虛函數不能實化化,但可以聲明指針。

繼續閱讀