天天看點

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  上一篇C++多态篇1一靜态聯編,動态聯編、虛函數與虛函數表vtable中,我在最後分析了虛函數與虛函數表的記憶體布局,在下一篇詳細剖析虛函數及虛函數表的過程中,我發現有關函數重載,函數覆寫,函數重寫和函數協變的知識也要了解清楚才能對虛函數表在記憶體中的布局,對派生類的對象模型以及對多态的實作有更深的了解。

  是以這一篇我作為一篇過渡篇,也同時對我以前寫過的一篇博文進行一個收尾。在C++繼承詳解之二——派生類成員函數詳解(函數隐藏、構造函數與相容覆寫規則)文章中,我對函數覆寫,重載,重寫提了一下,但是沒有深入記憶體中檢視記憶體布局,是以這一篇對前面剩下的問題做一個總結和詳細解答。

  注意:

  因為都是我自己畫的圖,因為圖很多,截圖也挺多,寫一篇文章不容易,是以有的圖我畫的挺大但是上傳出來可能就有點小,或者顔色搭配不合理導緻看不清,大家諒解一下。。ctrl+滑輪向上或者ctrl+向上鍵将網頁放大一下看吧,感謝感謝

一、函數重載,覆寫,隐藏,協變的概念和差別

1.函數重載

  首先,什麼是函數重載?

成員函數被重載的特征

(1)相同的範圍(在同一個類中);

(2)函數名字相同;

(3)參數不同;

(4)virtual 關鍵字可有可無

相信對C++有一定了解的朋友都知道函數重載的條件是:

  在同一個作用域内

  在C++繼承詳解之二——派生類成員函數詳解(函數隐藏、構造函數與相容覆寫規則)的開頭我也提到了,在派生類中定義一個函數名相同,參數名不同的函數,不是與基類中同名函數進行了函數重載,而是發生了函數隐藏。大家可以去我那篇文章開頭看一下那個例子。

  因為首先函數重載的第一個條件就沒有滿足,即:在相同的範圍中(在同一個類中),派生類和基類是兩個不同的類域,即不是同一個作用域,是以在繼承中,基類和派生類之間永遠不可能進行函數重載。

class Base
{
public:
    Base(int data = )
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void B()
    {
        cout << "Base::B()" << endl;
    }
    void B(int b)
    {
        cout << "Base::B(int)" << endl;
    }
    //B()與B(int b)構成了函數重載
    //因為上面兩個函數是在同一作用域中
    int b;
};
class Derive :public Base
{
public:
    Derive()
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void B(int a, int b)
    {
        cout << "Derive::B(int,int)" << endl;
    }
    //不會與Base類中的兩個B名的函數構成重載
    //因為作用域不同
};
           

下面這個圖僅僅代表函數之間的關系,不代表記憶體布局!

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

那麼上面的原則中提到:

virtual關鍵字在函數重載中可有可無

  那麼我們看一下加不加virtual對函數重載的影響。

(1).不加virtual

//定義一個測試函數
void Test()
{
    Base b;
    b.B();
    b.B();
}
//main函數調用測試函數
           

運作結果為:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

(2).加virtual

a.一個函數加virtual

class Base
{
public:
    Base(int data = )
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void B()
    {
        cout << "Base::B()" << endl;
    }
    virtual void B(int b)
    {
        cout << "Base::B(int)" << endl;
    }
    //B()與B(int b)構成了函數重載
    //因為上面兩個函數是在同一作用域中
    int b;
};
           

  運作結果為:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  我們對代碼進行一下反彙編檢視,

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  可以看到,我們Base b中b一共有八個位元組,前四個位元組為指向虛表的指針,儲存的是虛表的位址,後四個位元組是Base類中int b的值,關于虛表的問題可以去我的上一篇博文學習檢視 C++多态篇1一靜态聯編,動态聯編、虛函數與虛函數表vtable。

  看過我上一篇博文後,或者對虛表有一定了解後,我們可以參照彙編代碼看,我們可以看到在彙編代碼中,調用重載函數是根據位址不同調用的,調用B(1)時,是進入虛表中調用的,但是不影響函數重載。

  有的人可能要問,那麼不加virtual的函數編譯器在哪尋找呢?

  實際上,編譯器将類的對象存儲時是按下圖這樣存儲的

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  成員函數是單獨存儲的,是以編譯器在存儲成員函數那尋找函數即可

b.兩個函數都加virtual

class Base
{
public:
    Base(int data = )
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void B()
    {
        cout << "Base::B()" << endl;
    }
    virtual void B(int b)
    {
        cout << "Base::B(int)" << endl;
    }
    //B()與B(int b)構成了函數重載
    //因為上面兩個函數是在同一作用域中
    int b;
};
           

  運作結果依然是:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  我們進行反彙編和在記憶體中檢視可以得到:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  我們可以看到,因為B名的函數均為虛函數,是以均在虛表中存儲。

當編譯器調用時,就在虛表中查找調用。

c.多個函數加virtual

  因為在函數重載中,在不同類域中是不構成函數重載的。是以上面我們都隻分析了在基類中的重載,并且都已兩個重載函數作為例子,但是多個函數構成重載也是可以的,多個函數加virtual的情況等同于兩個函數都加virtual的情況,都會将虛函數加入虛函數表中,在調用時進入虛函數表中進行調用的。

  現在函數重載應該就沒有問題了吧~

二、函數覆寫

什麼是函數覆寫呢?

  覆寫是指派生類函數覆寫基類函數,特征是

(1)不同的範圍(分别位于派生類與基類);

(2)函數名字相同;

(3)參數相同;

(4)基類函數必須有virtual 關鍵字。

  當派生類對象調用子類中該同名函數時會自動調用子類中的覆寫版本,而不是父類中的被覆寫函數版本,這種機制就叫做覆寫。

  函數覆寫與我們上面說的函數重載有什麼差別呢?

  首先,函數重載要求在同一個作用域,而函數覆寫需要在不同範圍内。

  然後就是函數重載要求參數不相同,但是函數覆寫要求參數必須相同。

  最後一點就是函數重載中加不加virtual都可以,但是在函數覆寫中基類函數中必須要加virtual關鍵字。

  經過上面的分析我們知道了,我們在基類和派生類中分别定義名字相同,參數不同的函數,在後面調用的時候,編譯器無法将它處理為函數重載。

  那麼函數覆寫又是什麼情況呢。

  其實函數覆寫分為兩種情況:

1.對象調用函數的情況

  派生類對象調用的是派生類的覆寫函數

  基類的對象調用基類的函數

  下面看代碼:

class Base
{
public:
    Base(int data = )
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test()
    {
        cout << "Base::Test()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = )
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test()
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

int main()
{
    Derive d;
    d.Test();
    return ;
}
           

  我們在上面的代碼中,分别在基類和派生類中定義了同名同參數的函數Test(),看一下運作結果,看會調用基類的函數還是派生類的函數:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  因為我在基類和派生類的構造函數中都輸出了語句,而且是打斷點調試的,是以沒有調用析構函數。

  運作結果可以表明:

  這裡的Test()函數發生了函數覆寫。

  那我們進入記憶體中看一下:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  PS:因為是我自己截圖畫圖的,不知道為什麼傳上來就壓縮了,如果大家看不清,可以ctrl+向上鍵放大看一下。

  這張圖能夠更清楚地看到,在派生類的虛表中,隻有一個函數,就是Derive::Test(),沒有從Base類繼承下來的Test(),是以能夠更清楚的看到發生了函數的覆寫。

  如果這樣你還沒太了解,那麼我就再多加幾個函數。

  看下面的代碼:

class Base
{
public:
    Base(int data = )
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base::Test3()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = )
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test1()
    {
        cout << "Derive::Test1()" << endl;
    }   
    void Test2()
    {
        cout << "Derive::Test2()" << endl;
    }
    int d;
};

int main()
{
    Base b;
    b.Test1();
    b.Test2();
    b.Test3();
    Derive d;
    d.Test1();
    d.Test2();
    d.Test3();
    return ;
}
           

  從代碼可以看出在基類定義了三個虛函數,根據我們以前所說的知識,我們可以猜測基類會生成一個虛函數表,那麼派生類中我們定義了兩個同名同參數的函數,為了讓函數覆寫的現象更加明顯,我特意沒有将Test3()定義,那麼我們現在看一下運作結果:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  由結果可知,基類對象調用的是基類的函數。派生類對象調用的是什麼呢?

  我們進入記憶體中檢視一下:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  由上圖我們可以看到,我們在派生類中定義了的函數,在派生類虛函數表中将基類函數覆寫了,即派生類虛函數表中綠色的部分,而派生類沒有定義的函數,即Test3(),基類和派生類的函數位址完全相同。

  這就更清楚的看出了,派生類中定義了同名同參數的函數後,發生了函數覆寫。

2.指針或引用調用函數的情況

  指向派生類的基類指針調用的也是派生類的覆寫函數

  還是上面的例子,我們将調用者換一下:

class Base
{
public:
    Base(int data = )
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test()
    {
        cout << "Base::Test()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = )
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test()
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

int main()
{
    Base *pb;
    Derive d;
    pb = &d;
    pb->Test();
    return ;
}
           

運作結果為:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

在記憶體布局為:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  由記憶體布局可以看出,指針pb指向的虛表就是派生類對象d所擁有的虛表,是以當然調用的是派生類已經覆寫了的函數。

  是以說:

  多态的本質:不是重載聲明而是覆寫。

  虛函數調用方式:通過基類指針或引用,執行時會根據指針指向的對象的類,決定調用哪個函數。

三、函數隐藏

  經過上面的分析我們知道,在不同的類域定義不同參數的同名函數,是無法構成函數重載的。

  那麼當我們這麼做的時候,會發生什麼呢。

  實際上,這種情況叫做函數隐藏。

“隐藏”是指派生類的函數屏蔽了與其同名的基類函數,規則如下

(1)如果派生類的函數與基類的函數同名,并且參數也相同,但是基類函數沒有virtual 關鍵字。此時,基類的函數被隐藏(注意别與覆寫混淆)

(2)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數将被隐藏(注意别與重載混淆)。

首先來看第一種情況。

1.同名同參數

  那麼在上面的例子中我們試一下不加virtual關鍵字看看。

  即将基類改為:

class Base
{
public:
    Base(int data = )
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void Test()
    {
        cout << "Base::Test()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = )
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test()
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

int main()
{
    Derive d;
    d.Test();
    return ;
}
           

運作結果還是:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

這就是發生了函數的隐藏

再看下第二種情況

2.同名不同參數

(1)基類函數不加virtual

class Base
{
public:
    Base(int data = )
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void Test()
    {
        cout << "Base::Test()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = )
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test(int a)
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

int main()
{
    Derive d;
    d.Test();
    return ;
}
           

  我們在基類中定義了Test()函數,在派生類中定義了Test(int a)函數,這就是同名不同參數情況。

編譯運作一下:

編譯器報錯:

Error   1   error C2660: 'Derive::Test' : function does not take 0 arguments    e:\demo\blog\project1\project1\source.cpp   105 1   Project1

           

  我們可以看出,編譯器報錯:Test函數不能為0參數。

  如果我們将main函數改變一下:

int main()
{
    Derive d;
    d.Test();
    return ;
}
           

運作成功,結果為:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

  這就是發生了函數隐藏~

(2)基類函數加virtual

class Base
{
public:
    Base(int data = )
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test()
    {
        cout << "Base::Test()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = )
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test(int a)
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

int main()
{
    Derive d;
    d.Test();
    return ;
}
           

編譯運作依然報錯:

Error   1   error C2660: 'Derive::Test' : function does not take 0 arguments    e:\demo\blog\project1\project1\source.cpp   105 1   Project1
           

那麼将main函數改變一下:

int main()
{
    Derive d;
    d.Test();
    return ;
}
           

運作成功,結果為:

C++多态篇2——虛函數表詳解之從記憶體布局看函數重載,函數覆寫,函數隐藏一、函數重載,覆寫,隐藏,協變的概念和差別二、函數覆寫三、函數隐藏

這也是發生了函數隐藏。

  現在函數隐藏應該沒有問題了吧~

  總結一下前面的:

  1.函數重載必須是在同一作用域的,在繼承與多态這裡,在基類與派生類之間是不能進行函數重載。

  2.函數覆寫是多态的本質

  在基類中的虛函數,在派生類定義一個同名同參數的函數,就可以用派生類新定義的函數對基類函數進行覆寫。

  3.函數隐藏是發生在基類和派生類之間的,當函數同名但是不同參數的時候,不論是不是虛函數,都會發生函數隐藏。

  如有問題歡迎批評指正,人無完人,文無完文,希望大家共同進步!