1. 程式人生 > >C++ 虛擬函式、純虛擬函式、繼承、虛表、多型原理相關知識點總結

C++ 虛擬函式、純虛擬函式、繼承、虛表、多型原理相關知識點總結

  • 虛擬函式與純虛擬函式

虛擬函式為了過載和多型的需要,在基類中是有定義的,即便定義是空,所以子類中可以重寫也可以不寫基類中的此函式!

純虛擬函式在基類中是沒有定義的,必須在子類中加以實現,很像java中的介面函式!

虛擬函式

引入原因:為了方便使用多型特性,我們常常需要在基類中定義虛擬函式。

class Cman
{
public:
virtual void Eat(){……};
void Move();
void Sleep();
private:
};
class CChild : public CMan
{
public:
virtual void Eat(){……};
void Sleep();
private:
};
CMan m_man;
CChild m_child;
CMan *p ;//這才是使用的精髓,如果不定義基類的指標去使用,沒有太大的意義
p = &m_man ;
p->Eat(); //始終呼叫CMan的Eat成員函式,不會呼叫 CChild 的
p = &m_child;
p->Eat(); //如果子類實現(覆蓋)了該方法,則始終呼叫CChild的Eat函式
//不會呼叫CMan 的 Eat 方法;如果子類沒有實現該函式,則呼叫CMan的Eat函式
p->Move(); //子類中沒有該成員函式,所以呼叫的是基類中的
p->Sleep();//


純虛擬函式

引入原因:

1、同“虛擬函式”;

2、在很多情況下,基類本身生成物件是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成物件明顯不合常理。

純虛擬函式就是基類只定義了函式體,沒有實現過程,定義方法如: virtual void Eat() = 0; 不要 在cpp中定義;純虛擬函式相當於介面,不能直接例項話,需要派生類來實現函式定義;

有的人可能在想,定義這些有什麼用啊 ,我覺得很有用,比如你想描述一些事物的屬性給別人,而自己不想去實現,就可以定義為純虛擬函式。

虛擬函式和純虛擬函式區別

觀點一:

類裡宣告為虛擬函式的話,這個函式是實現的,哪怕是空實現,它的作用就是為了能讓這個函式在它的子類裡面可以被過載,這樣的話,這樣編譯器就可以使用後期繫結來達到多型了

純虛擬函式只是一個介面,是個函式的宣告而已,它要留到子類裡去實現。

class A{
protected:
void foo();//普通類函式
virtual void foo1();//虛擬函式
virtual void foo2() = 0;//純虛擬函式
}

觀點二:

虛擬函式在子類裡面也可以不過載的;但純虛必須在子類去實現,這就像Java的介面一樣。通常我們把很多函式加上virtual,是一個好的習慣,雖然犧牲了一些效能,但是增加了面向物件的多型性,因為你很難預料到父類裡面的這個函式不在子類裡面不去修改它的實現

觀點三:

虛擬函式的類用於“實作繼承”,繼承介面的同時也繼承了父類的實現。當然我們也可以完成自己的實現。純虛擬函式的類用於“介面繼承”,主要用於通訊協議方面。關注的是介面的統一性,實現由子類完成。一般來說,介面類中只有純虛擬函式的。

觀點四:

帶純虛擬函式的類叫虛基類,這種基類不能直接生成物件,而只有被繼承,並重寫其虛擬函式後,才能使用。這樣的類也叫抽象類。

虛擬函式是為了繼承介面和預設行為

純虛擬函式只是繼承介面,行為必須重新定義

  • 虛表與動態繫結技術、C++多型性原理

1. virtual關鍵字申明的函式叫做虛擬函式,虛擬函式肯定是類的成員函式。
2. 存在虛擬函式的類都有一個一維的虛擬函式表叫做虛表。類的物件有一個指向虛表開始的虛指標。虛表是和類對應的,虛表指標是和物件對應的。
3. 多型性是一個介面多種實現,是面向物件的核心。分為類的多型性和函式的多型性。
4. 多型用虛擬函式來實現,結合動態繫結。
5. 純虛擬函式是虛擬函式再加上= 0
6. 抽象類是指包括至少一個純虛擬函式的類。

純虛擬函式:virtual void breathe()=0;即抽象類!必須在子類實現這個函式!即先有名稱,沒內容,在派生類實現內容!

我們先看一個例子:

[cpp] view plaincopy
1.  #include <iostream.h>  
2.  class animal  
3.  {  
4.  public:  
5.         void sleep()  
6.         {  
7.                cout<<"animal sleep"<<endl;  
8.         }  
9.         void breathe()  
10.        {  
11.               cout<<"animal breathe"<<endl;  
12.        }  
13. };  
14. class fish:public animal  
15. {  
16. public:  
17.        void breathe()  
18.        {  
19.               cout<<"fish bubble"<<endl;  
20.        }  
21. };  
22. void main()  
23. {  
24.        fish fh;  
25.        animal *pAn=&fh; // 隱式型別轉換  
26.        pAn->breathe();  
27. }  


注意,在例1-1的程式中沒有定義虛擬函式。考慮一下例1-1的程式執行的結果是什麼?
答案是輸出:animal breathe
我們在main()函式中首先定義了一個fish類的物件fh,接著定義了一個指向animal類的指標變數pAn,將fh的地址賦給了指標變數pAn,然後利用該變數呼叫pAn->breathe()。許多學員往往將這種情況和C++的多型性搞混淆,認為fh實際上是fish類的物件,應該是呼叫fish類的breathe(),輸出“fish bubble”,然後結果卻不是這樣。下面我們從兩個方面來講述原因。
1編譯的角度
C++編譯器在編譯的時候,要確定每個物件呼叫的函式(要求此函式是非虛擬函式)的地址,這稱為早期繫結(early binding),當我們將fish類的物件fh的地址賦給pAn時,C++編譯器進行了型別轉換,此時C++編譯器認為變數pAn儲存的就是animal物件的地址。當在main()函式中執行pAn->breathe()時,呼叫的當然就是animal物件的breathe函式。
2記憶體模型的角度
我們給出了fish物件記憶體模型,如下圖所示:

我們構造fish類的物件時,首先要呼叫animal類的建構函式去構造animal類的物件,然後才呼叫fish類的建構函式完成自身部分的構造,從而拼接出一個完整的fish物件。當我們將fish類的物件轉換為animal型別時,該物件就被認為是原物件整個記憶體模型的上半部分,也就是圖1-1中的“animal的物件所佔記憶體。那麼當我們利用型別轉換後的物件指標去呼叫它的方法時,當然也就是呼叫它所在的記憶體中的方法。因此,輸出animal breathe,也就順理成章了。正如很多學員所想,在例1-1的程式中,我們知道pAn實際指向的是fish類的物件,我們希望輸出的結果是魚的呼吸方法,即呼叫fish類的breathe方法。這個時候,就該輪到虛擬函式登場了。前面輸出的結果是因為編譯器在編譯的時候,就已經確定了物件呼叫的函式的地址,要解決這個問題就要使用遲繫結(late binding)技術。當編譯器使用遲繫結時,就會在執行時再去確定物件的型別以及正確的呼叫函式。而要讓編譯器採用遲繫結,就要在基類中宣告函式時使用virtual關鍵字(注意,這是必須的,很多學員就是因為沒有使用虛擬函式而寫出很多錯誤的例子),這樣的函式我們稱為虛擬函式。一旦某個函式在基類中宣告為virtual,那麼在所有的派生類中該函式都是virtual,而不需要再顯式地宣告為virtual下面修改例1-1的程式碼,將animal類中的breathe()函式宣告為virtual,如下:

[cpp] view plaincopy
1.  #include <iostream.h>  
2.  class animal  
3.  {  
4.  public:  
5.      void sleep()  
6.      {  
7.          cout<<"animal sleep"<<endl;  
8.      }  
9.      virtual void breathe()  
10.     {  
11.         cout<<"animal breathe"<<endl;  
12.     }  
13. };  
14.   
15. class fish:public animal  
16. {  
17. public:  
18.     void breathe()  
19.     {  
20.         cout<<"fish bubble"<<endl;  
21.     }  
22. };  
23. void main()  
24. {  
25.     fish fh;  
26.     animal *pAn=&fh; // 隱式型別轉換  
27.     pAn->breathe();  
28. }  


大家可以再次執行這個程式,你會發現結果是“fish bubble”,也就是根據物件的型別呼叫了正確的函式。
那麼當我們將breathe()宣告為virtual時,在背後發生了什麼呢?
編譯器在編譯的時候,發現animal類中有虛擬函式,此時編譯器會為每個包含虛擬函式的類建立一個虛表(即vtable),該表是一個一維陣列,在這個陣列中存放每個虛擬函式的地址。對於例1-2的程式,animalfish類都包含了一個虛擬函式breathe(),因此編譯器會為這兩個類都建立一個虛表,(即使子類裡面沒有virtual函式,但是其父類裡面有,所以子類中也有了)如下圖所示:

那麼如何定位虛表呢?編譯器另外還為每個類的物件提供了一個虛表指標(即vptr),這個指標指向了物件所屬類的虛表。在程式執行時,根據物件的型別去初始化vptr,從而讓vptr正確的指向所屬類的虛表,從而在呼叫虛擬函式時,就能夠找到正確的函式。對於例1-2的程式,由於pAn實際指向的物件型別是fish,因此vptr指向的fish類的vtable,當呼叫pAn->breathe()時,根據虛表中的函式地址找到的就是fish類的breathe()函式。正是由於每個物件呼叫的虛擬函式都是通過虛表指標來索引的,也就決定了虛表指標的正確初始化是非常重要的。換句話說,在虛表指標沒有正確初始化之前,我們不能夠去呼叫虛擬函式。那麼虛表指標在什麼時候,或者說在什麼地方初始化呢?答案是在建構函式中進行虛表的建立和虛表指標的初始化。還記得建構函式的呼叫順序嗎,在構造子類物件時,要先呼叫父類的建構函式,此時編譯器只看到了父類,並不知道後面是否後還有繼承者,它初始化父類物件的虛表指標,該虛表指標指向父類的虛表。當執行子類的建構函式時,子類物件的虛表指標被初始化,指向自身的虛表。對於例2-2的程式來說,當fish類的fh物件構造完畢後,其內部的虛表指標也就被初始化為指向fish類的虛表。在型別轉換後,呼叫pAn->breathe(),由於pAn實際指向的是fish類的物件,該物件內部的虛表指標指向的是fish類的虛表,因此最終呼叫的是fish類的breathe()函式。要注意:對於虛擬函式呼叫來說,每一個物件內部都有一個虛表指標,該虛表指標被初始化為本類的虛表。所以在程式中,不管你的物件型別如何轉換,但該物件內部的虛表指標是固定的,所以呢,才能實現動態的物件函式呼叫,這就是C++多型性實現的原理。

總結(基類有虛擬函式):
1.
每一個類都有虛表。
2.
虛表可以繼承,如果子類沒有重寫虛擬函式,那麼子類虛表中仍然會有該函式的地址,只不過這個地址指向的是基類的虛擬函式實現。如果基類有3個虛擬函式,那麼基類的虛表中就有三項(虛擬函式地址),派生類也會有虛表,至少有三項,如果重寫了相應的虛擬函式,那麼虛表中的地址就會改變,指向自身的虛擬函式實現。如果派生類有自己的虛擬函式,那麼虛表中就會新增該項。
3.
派生類的虛表中虛擬函式地址的排列順序和基類的虛表中虛擬函式地址排列順序相同。

這就是C++中的多型性。當C++編譯器在編譯的時候,發現animal類的breathe()函式是虛擬函式,這個時候C++就會採用遲繫結(late binding)技術。也就是編譯時並不確定具體呼叫的函式,而是在執行時,依據物件的型別(在程式中,我們傳遞的fish類物件的地址)來確認呼叫的是哪一個函式,這種能力就叫做C++的多型性(動態繫結)。我們沒有在breathe()函式前加virtual關鍵字時,C++編譯器在編譯時就確定了哪個函式被呼叫,這叫做早期繫結(early binding)。

C++的多型性是通過遲繫結技術來實現的。

C++的多型性用一句話概括就是:在基類的函式前加上virtual關鍵字,在派生類中重寫該函式,執行時將會根據物件的實際型別來呼叫相應的函式。如果物件型別是派生類,就呼叫派生類的函式;如果物件型別是基類,就呼叫基類的函式。

如果基類函式前沒有加virtual關鍵字,則根據指標、引用型別來呼叫相應的函式。

虛擬函式是在基類中定義的,目的是不確定它的派生類的具體行為。例:
定義一個基類:class Animal//動物。它的函式為breathe()//呼吸。
再定義一個類class Fish//。它的函式也為breathe()
再定義一個類class Sheep //羊。它的函式也為breathe()
為了簡化程式碼,將Fish,Sheep定義成基類Animal的派生類。
然而FishSheepbreathe不一樣,一個是在水中通過水來呼吸,一個是直接呼吸空氣。所以基類不能確定該如何定義breathe,所以在基類中只定義了一個virtual breathe,它是一個空的虛擬函式。具本的函式在子類中分別定義。程式一般執行時,找到類,如果它有基類,再找它的基類,最後執行的是基類中的函式,這時,它在基類中找到的是virtual標識的函式,它就會再回到子類中找同名函式。派生類也叫子類。基類也叫父類。這就是虛擬函式的產生,和類的多型性(breathe)的體現。

這裡的多型性是指類的多型性。函式的多型性是指一個函式被定義成多個不同引數的函式,它們一般被存在標頭檔案中,當你呼叫這個函式,針對不同的引數,就會呼叫不同的同名函式。例:Rect()//矩形。它的引數可以是兩個座標點(pointpoint)也可能是四個座標(x1,y1,x2,y2)這叫函式的多型性與函式的過載。

類的多型性,是指用虛擬函式和延遲繫結來實現的。函式的多型性是函式的過載。

一般情況下(沒有涉及virtual函式),當我們用一個指標/引用呼叫一個函式的時候,被呼叫的函式是取決於這個指標/引用的型別。即如果這個指標/引用是基類物件的指標/引用就呼叫基類的方法;如果指標/引用是派生類物件的指標/引用就呼叫派生類的方法,當然如果派生類中沒有此方法,就會向上到基類裡面去尋找相應的方法。這些呼叫在編譯階段就確定了。

當設計到多型性的時候,採用了虛擬函式和動態繫結,此時的呼叫就不會在編譯時候確定而是在執行時確定。不在單獨考慮指標/引用的型別而是看指標/引用的物件的型別來判斷函式的呼叫,根據物件中虛指標指向的虛表中的函式的地址來確定呼叫哪個函式。