C++ 多型的實現及原理

黨偉_90發表於2018-05-04

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

  1:用virtual關鍵字申明的函式叫做虛擬函式,虛擬函式肯定是類的成員函式。  

  2:存在虛擬函式的類都有一個一維的虛擬函式表叫做虛表,類的物件有一個指向虛表開始的虛指標。虛表是和類對應的,虛表指標是和物件對應的。  

  3:多型性是一個介面多種實現,是物件導向的核心,分為類的多型性和函式的多型性。  

  4:多型用虛擬函式來實現,結合動態繫結.  

  5:純虛擬函式是虛擬函式再加上 = 0;  

  6:抽象類是指包括至少一個純虛擬函式的類。

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

我們先看個例子

#include "stdafx.h"
#include <iostream> 
#include <stdlib.h>
using namespace std; 

class Father
{
public:
    void Face()
    {
        cout << "Father's face" << endl;
    }

    void Say()
    {
        cout << "Father say hello" << endl;
    }
};


class Son:public Father
{
public:     
    void Say()
    {
        cout << "Son say hello" << endl;
    }
};

void main()
{
    Son son;
    Father *pFather=&son; // 隱式型別轉換
    pFather->Say();
}

輸出的結果為:

我們在main()函式中首先定義了一個Son類的物件son,接著定義了一個指向Father類的指標變數pFather,然後利用該變數呼叫pFather->Say().估計很多人往往將這種情況和c++的多型性搞混淆,認為son實際上是Son類的物件,應該是呼叫Son類的Say,輸出"Son say hello",然而結果卻不是.

 

  從編譯的角度來看:

    c++編譯器在編譯的時候,要確定每個物件呼叫的函式(非虛擬函式)的地址,這稱為早期繫結,當我們將Son類的物件son的地址賦給pFather時,c++編譯器進行了型別轉換,此時c++編譯器認為變數pFather儲存的就是Father物件的地址,當在main函式中執行pFather->Say(),呼叫的當然就是Father物件的Say函式

 從記憶體角度看

    

Son類物件的記憶體模型如上圖

我們構造Son類的物件時,首先要呼叫Father類的建構函式去構造Father類的物件,然後才呼叫Son類的建構函式完成自身部分的構造,從而拼接出一個完整的Son類物件。當我們將Son類物件轉換為Father型別時,該物件就被認為是原物件整個記憶體模型的上半部分,也就是上圖中“Father的物件所佔記憶體”,那麼當我們利用型別轉換後的物件指標去呼叫它的方法時,當然也就是呼叫它所在的記憶體中的方法,因此,輸出“Father Say hello”,也就順理成章了。

  正如很多人那麼認為,在上面的程式碼中,我們知道pFather實際上指向的是Son類的物件,我們希望輸出的結果是son類的Say方法,那麼想到達到這種結果,就要用到虛擬函式了。

  前面輸出的結果是因為編譯器在編譯的時候,就已經確定了物件呼叫的函式的地址,要解決這個問題就要使用晚繫結,當編譯器使用晚繫結時候,就會在執行時再去確定物件的型別以及正確的呼叫函式,而要讓編譯器採用晚繫結,就要在基類中宣告函式時使用virtual關鍵字,這樣的函式我們就稱之為虛擬函式,一旦某個函式在基類中宣告為virtual,那麼在所有的派生類中該函式都是virtual,而不需要再顯式地宣告為virtual。

  程式碼稍微改動一下,看一下執行結果

#include "stdafx.h"
#include <iostream> 
#include <stdlib.h>
using namespace std; 

class Father
{
public:
    void Face()
    {
        cout << "Father's face" << endl;
    }

    virtual void Say()
    {
        cout << "Father say hello" << endl;
    }
};


class Son:public Father
{
public:     
    void Say()
    {
        cout << "Son say hello" << endl;
    }
};

void main()
{
    Son son;
    Father *pFather=&son; // 隱式型別轉換
    pFather->Say();
}

我們發現結果是"Son say hello"也就是根據物件的型別呼叫了正確的函式,那麼當我們將Say()宣告為virtual時,背後發生了什麼。

  編譯器在編譯的時候,發現Father類中有虛擬函式,此時編譯器會為每個包含虛擬函式的類建立一個虛表(即 vtable),該表是一個一維陣列,在這個陣列中存放每個虛擬函式的地址,

  

那麼如何定位虛表呢?編譯器另外還為每個物件提供了一個虛表指標(即vptr),這個指標指向了物件所屬類的虛表,在程式執行時,根據物件的型別去初始化vptr,從而讓vptr正確的指向了所屬類的虛表,從而在呼叫虛擬函式的時候,能夠找到正確的函式,對於第二段程式碼程式,由於pFather實際指向的物件型別是Son,因此vptr指向的Son類的vtable,當呼叫pFather->Son()時,根據虛表中的函式地址找到的就是Son類的Say()函式.

  正是由於每個物件呼叫的虛擬函式都是通過虛表指標來索引的,也就決定了虛表指標的正確初始化是非常重要的,換句話說,在虛表指標沒有正確初始化之前,我們不能夠去呼叫虛擬函式,那麼虛表指標是在什麼時候,或者什麼地方初始化呢?

  答案是在建構函式中進行虛表的建立和虛表指標的初始化,在構造子類物件時,要先呼叫父類的建構函式,此時編譯器只“看到了”父類,並不知道後面是否還有繼承者,它初始化父類物件的虛表指標,該虛表指標指向父類的虛表,當執行子類的建構函式時,子類物件的虛表指標被初始化,指向自身的虛表。

  

  總結(基類有虛擬函式的):

  1:每一個類都有虛表

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

  3:派生類的虛表中虛地址的排列順序和基類的虛表中虛擬函式地址排列順序相同。

  這就是c++中的多型性,當c++編譯器在編譯的時候,發現Father類的Say()函式是虛擬函式,這個時候c++就會採用晚繫結技術,也就是編譯時並不確定具體呼叫的函式,而是在執行時,依據物件的型別來確認呼叫的是哪一個函式,這種能力就叫做c++的多型性,我們沒有在Say()函式前加virtual關鍵字時,c++編譯器就確定了哪個函式被呼叫,這叫做早期繫結。

  c++的多型性就是通過晚繫結技術來實現的。

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

  虛擬函式是在基類中定義的,目的是不確定它的派生類的具體行為,例如:

  定義一個基類:class Animal //動物,它的函式為breathe()

  再定義一個類class Fish //魚。它的函式也為breathe()

  再定義一個類class Sheep //羊,它的函式也為breathe()

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

  這裡的多型性是指類的多型性。

  函式的多型性是指一個函式被定義成多個不同引數的函式。當你呼叫這個函式時,就會呼叫不同的同名函式。

 

一般情況下(不涉及虛擬函式),當我們用一個指標/引用呼叫一個函式的時候,被呼叫的函式是取決於這個指標/引用的型別。

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

 

現在我們看一個體現c++多型性的例子,看看輸出結果:

#include "stdafx.h"
#include <iostream> 
#include <stdlib.h>
using namespace std; 

class CA 
{ 
public: 
    void f() 
    { 
        cout << "CA f()" << endl; 
    } 
    virtual void ff() 
    { 
        cout << "CA ff()" << endl; 
        f(); 
    } 
}; 

class CB : public CA 
{ 
public : 
    virtual void f() 
    { 
        cout << "CB f()" << endl; 
    } 
    void ff() 
    { 
        cout << "CB ff()" << endl; 
        f(); 
        CA::ff(); 
    } 
}; 
class CC : public CB 
{ 
public: 
    virtual void f() 
    { 
        cout << "C f()" << endl; 
    } 
}; 

int main() 
{ 
    CB b; 
    CA *ap = &b; 
    CC c; 
    CB &br = c; 
    CB *bp = &c; 

    ap->f(); 
    cout << endl;

    b.f(); 
    cout << endl;

    br.f(); 
    cout << endl;

    bp->f(); 
    cout << endl;

    ap->ff(); 
    cout << endl;

    bp->ff(); 
    cout << endl;

    return 0; 
}



相關文章