c++基礎知識 7 -8 虛函數

7.用c++設計一個不能被繼承的類

在Java  中定義了關鍵字 final  ,被 final  修飾的類不能被繼承。但在 C++  中沒

有final  這個關鍵字,要實現這個要求還是需要花費一些精力。 

首先想到的是在 C++ 中,子類的構造函數會自動調用父類的構造函數。同樣,

子類的析構函數也會自動調用父類的析構函數。要想一個類不能被繼承,我

們只要把它的構造函數和析構函數都定義爲私有函數。那麼當一個類試圖從

它那繼承的時候,必然會由於試圖調用構造函數、析構函數而導致編譯錯誤。  

可是這個類的構造函數和析構函數都是私有函數了,我們怎樣才能得到該類

的實例呢?這難不倒我們,我們可以通過定義靜態來創建和釋放類的實例。

基於這個思路,我們可以寫出如下的代碼: 

//////////////////////////////////////////////////////////////////

///// 

// Define a class which can't be derived from 

//////////////////////////////////////////////////////////////////

///// 

class FinalClass1 

public : 

  static FinalClass1* GetInstance() 

  { 

    return new FinalClass1; 

  } 

 

  static void DeleteInstance( FinalClass1* pInstance) 

  { 

    delete pInstance; 

    pInstance = 0; 

  } 

 

private : 

  FinalClass1() {} 

  ~FinalClass1() {} 

}; 

這個類是不能被繼承,但在總覺得它和一般的類有些不一樣,使用起來也有

點不方便。比如,我們只能得到位於堆上的實例,而得不到位於棧上實例。 

能不能實現一個和一般類除了不能被繼承之外其他用法都一樣的類呢?辦法

總是有的,不過需要一些技巧。請看如下代碼: 

//////////////////////////////////////////////////////////////////

///// 

// Define a class which can't be derived from 

//////////////////////////////////////////////////////////////////

///// 

template <typename T> class MakeFinal 

  friend T; 

 

private : 

  MakeFinal() {} 

  ~MakeFinal() {} 

}; 

 

class FinalClass2 : virtual public MakeFinal<FinalClass2> 

public : 

  FinalClass2() {} 

  ~FinalClass2() {} 

}; 

這個類使用起來和一般的類沒有區別,可以在棧上、也可以在堆上創建實例。

儘管類 MakeFinal <FinalClass2> 的構造函數和析構函數都是私有的,但由

於類 FinalClass2 是它的友元函數,因此在 FinalClass2 中調用 MakeFinal 

<FinalClass2> 的構造函數和析構函數都不會造成編譯錯誤。 

但當我們試圖從 FinalClass2 繼承一個類並創建它的實例時,卻不同通過編

譯。 

class Try : public FinalClass2 

public : 

  Try() {} 

  ~Try() {} 

}; 

 

Try temp;   

由於類 FinalClass2 是從類 MakeFinal  <FinalClass2> 虛繼承過來的,在調

用 Try 的構造函數的時候,會直接跳過 FinalClass2 而直接調用 MakeFinal 

<FinalClass2> 的構造函數。非常遺憾的是, Try 不是 MakeFinal 

<FinalClass2> 的友元,因此不能調用其私有的構造函數。 

基於上面的分析,試圖從 FinalClass2 繼承的類,一旦實例化,都會導致編

譯錯誤,因此是 FinalClass2 不能被繼承。這就滿足了我們設計要求


8.虛函數

8.1

首先:強調一個概念
定義一個函數爲虛函數,不代表函數爲不被實現的函數。
定義他爲虛函數是爲了允許用基類的指針來調用子類的這個函數。
定義一個函數爲純虛函數,才代表函數沒有被實現。

定義純虛函數是爲了實現一個接口,起到一個規範的作用,規範繼承這個類的程序員必須實現這個函數。
1、簡介
假設我們有下面的類層次:

  1. class A  
  2. {  
  3. public:  
  4.     virtual void foo()  
  5.     {  
  6.         cout<<"A::foo() is called"<<endl;  
  7.     }  
  8. };  
  9. class B:public A  
  10. {  
  11. public:  
  12.     void foo()  
  13.     {  
  14.         cout<<"B::foo() is called"<<endl;  
  15.     }  
  16. };  
  17. int main(void)  
  18. {  
  19.     A *a = new B();  
  20.     a->foo();   // 在這裏,a雖然是指向A的指針,但是被調用的函數(foo)卻是B的!  
  21.     return 0;  
  22. }  
     這個例子是虛函數的一個典型應用,通過這個例子,也許你就對虛函數有了一些概念。它虛就虛在所謂“推遲聯編”或者“動態聯編”上,一個類函數的調用並不是在編譯時刻被確定的,而是在運行時刻被確定的。由於編寫代碼的時候並不能確定被調用的是基類的函數還是哪個派生類的函數,所以被成爲“虛”函數。
    虛函數只能藉助於指針或者引用來達到多態的效果。

C++純虛函數
一、定義
 純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛函數的方法是在函數原型後加“=0”
 virtual void funtion1()=0
二、引入原因
  1、爲了方便使用多態特性,我們常常需要在基類中定義虛擬函數。
  2、在很多情況下,基類本身生成對象是不合情理的。例如,動物作爲一個基類可以派生出老虎、孔雀等子類,但動物本身生成對象明顯不合常理。
  爲了解決上述問題,引入了純虛函數的概念,將函數定義爲純虛函數(方法:virtual ReturnType Function()= 0;),則編譯器要求在派生類中必須予以重寫以實現多態性。同時含有純虛擬函數的類稱爲抽象類,它不能生成對象。這樣就很好地解決了上述兩個問題。

聲明瞭純虛函數的類是一個抽象類。所以,用戶不能創建類的實例,只能創建它的派生類的實例。
純虛函數最顯著的特徵是:它們必須在繼承類中重新聲明函數(不要後面的=0,否則該派生類也不能實例化),而且它們在抽象類中往往沒有定義。
定義純虛函數的目的在於,使派生類僅僅只是繼承函數的接口。
純虛函數的意義,讓所有的類對象(主要是派生類對象)都可以執行純虛函數的動作,但類無法爲純虛函數提供一個合理的缺省實現。所以類純虛函數的聲明就是在告訴子類的設計者,“你必須提供一個純虛函數的實現,但我不知道你會怎樣實現它”。


抽象類的介紹
抽象類是一種特殊的類,它是爲了抽象和設計的目的爲建立的,它處於繼承層次結構的較上層。
(1)抽象類的定義:  稱帶有純虛函數的類爲抽象類。
(2)抽象類的作用:
抽象類的主要作用是將有關的操作作爲結果接口組織在一個繼承層次結構中,由它來爲派生類提供一個公共的根,派生類將具體實現在其基類中作爲接口的操作。所以派生類實際上刻畫了一組子類的操作接口的通用語義,這些語義也傳給子類,子類可以具體實現這些語義,也可以再將這些語義傳給自己的子類。
(3)使用抽象類時注意:

•   抽象類只能作爲基類來使用,其純虛函數的實現由派生類給出。如果派生類中沒有重新定義純虛函數,而只是繼承基類的純虛函數,則這個派生類仍然還是一個抽象類。如果派生類中給出了基類純虛函數的實現,則該派生類就不再是抽象類了,它是一個可以建立對象的具體的類。
•   抽象類是不能定義對象的。


總結:

1、純虛函數聲明如下: virtual void funtion1()=0; 純虛函數一定沒有定義,純虛函數用來規範派生類的行爲,即接口。包含純虛函數的類是抽象類,抽象類不能定義實例,但可以聲明指向實現該抽象類的具體類的指針或引用。
2、虛函數聲明如下:virtual ReturnType FunctionName(Parameter);虛函數必須實現,如果不實現,編譯器將報錯,錯誤提示爲:
error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"
3、對於虛函數來說,父類和子類都有各自的版本。由多態方式調用的時候動態綁定。
4、實現了純虛函數的子類,該純虛函數在子類中就編程了虛函數,子類的子類即孫子類可以覆蓋該虛函數,由多態方式調用的時候動態綁定。
5、虛函數是C++中用於實現多態(polymorphism)的機制。核心理念就是通過基類訪問派生類定義的函數。
6、在有動態分配堆上內存的時候,析構函數必須是虛函數,但沒有必要是純虛的。
7、友元不是成員函數,只有成員函數纔可以是虛擬的,因此友元不能是虛擬函數。但可以通過讓友元函數調用虛擬成員函數來解決友元的虛擬問題。
8、析構函數應當是虛函數,將調用相應對象類型的析構函數,因此,如果指針指向的是子類對象,將調用子類的析構函數,然後自動調用基類的析構函數。

有純虛函數的類是抽象類,不能生成對象,只能派生。他派生的類的純虛函數沒有被改寫,那麼,它的派生類還是個抽象類。
定義純虛函數就是爲了讓基類不可實例化化
因爲實例化這樣的抽象數據結構本身並沒有意義。
或者給出實現也沒有意義
實際上我個人認爲純虛函數的引入,是出於兩個目的
1、爲了安全,因爲避免任何需要明確但是因爲不小心而導致的未知的結果,提醒子類去做應做的實現。
2、爲了效率,不是程序執行的效率,而是爲了編碼的效率。

果一個類中至少有一個純虛函數,那麼就稱該類爲抽象類。所以上述中Graph類就是抽象類。對於抽象類有以下幾個注意點:(1)抽象類只能作爲其他類的基類來使用,不能建立抽象類對象;(2)不允許從具體類中派生出抽象類(不包含純虛函數的普通類);(3)抽象類不能用作函數的參數類型、返回類型和顯示轉化類型;(4)如果派生類中沒有定義純虛函數的實現,而只是繼承成了基類的純虛函數。那麼該派生類仍然爲抽象類。一旦給出了對基類中虛函數的實現,那麼派生類就不是抽象類了,而是可以建立對象的具體類;

8.2虛擬繼承

1.爲什麼要引入虛擬繼承

虛擬繼承是多重繼承中特有的概念。虛擬基類是爲解決多重繼承而出現的。如:類D繼承自類B1、B2,而類B1、B2都繼承自類A,因此在類D中兩次出現類A中的變量和函數。爲了節省內存空間,可以將B1、B2對A的繼承定義爲虛擬繼承,而A就成了虛擬基類。實現的代碼如下:

class A

class B1:public virtual A;

class B2:public virtual A;

class D:public B1,public B2;

虛擬繼承在一般的應用中很少用到,所以也往往被忽視,這也主要是因爲在C++中,多重繼承是不推薦的,也並不常用,而一旦離開了多重繼承,虛擬繼承就完全失去了存在的必要因爲這樣只會降低效率和佔用更多的空間。

 

2.引入虛繼承和直接繼承會有什麼區別呢

由於有了間接性和共享性兩個特徵,所以決定了虛繼承體系下的對象在訪問時必然會在時間和空間上與一般情況有較大不同。

2.1時間:在通過繼承類對象訪問虛基類對象中的成員(包括數據成員和函數成員)時,都必須通過某種間接引用來完成,這樣會增加引用尋址時間(就和虛函數一樣),其實就是調整this指針以指向虛基類對象,只不過這個調整是運行時間接完成的。

2.2空間:由於共享所以不必要在對象內存中保存多份虛基類子對象的拷貝,這樣較之多繼承節省空間。虛擬繼承與普通繼承不同的是,虛擬繼承可以防止出現diamond繼承時,一個派生類中同時出現了兩個基類的子對象。也就是說,爲了保證這一點,在虛擬繼承情況下,基類子對象的佈局是不同於普通繼承的。因此,它需要多出一個指向基類子對象的指針。

 

3.筆試,面試中常考的C++虛擬繼承的知識點

第一種情況:         第二種情況:          第三種情況            第四種情況:
class a           class a              class a              class a
{              {                {                 {
    virtual void func();      virtual void func();       virtual void func();        virtual void func();
};              };                  char x;              char x;
class b:public virtual a   class b :public a           };                };
{              {                class b:public virtual a      class b:public a
    virtual void foo();        virtual void foo();     {                 {
};              };                  virtual void foo();        virtual void foo();
                               };                };

如果對這四種情況分別求sizeof(a),  sizeof(b)。結果是什麼樣的呢?下面是輸出結果:(在vc6.0中運行)
第一種:4,12 
第二種:4,4
第三種:8,16
第四種:8,8

想想這是爲什麼呢?

因爲每個存在虛函數的類都要有一個4字節的指針指向自己的虛函數表,所以每種情況的類a所佔的字節數應該是沒有什麼問題的,那麼類b的字節數怎麼算呢?看“第一種”和“第三種”情況採用的是虛繼承,那麼這時候就要有這樣的一個指針vptr_b_a,這個指針叫虛類指針,也是四個字節;還要包括類a的字節數,所以類b的字節數就求出來了。而“第二種”和“第四種”情況則不包括vptr_b_a這個指針,這回應該木有問題了吧。

 




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章