程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++多態篇2——虛函數表詳解之從內存布局看函數重載,函數覆蓋,函數隱藏

C++多態篇2——虛函數表詳解之從內存布局看函數重載,函數覆蓋,函數隱藏

編輯:C++入門知識

C++多態篇2——虛函數表詳解之從內存布局看函數重載,函數覆蓋,函數隱藏


一、函數重載,覆蓋,隱藏,協變的概念和區別
1.函數重載
首先,什麼是函數重載?
成員函數被重載的特征
(1)相同的范圍(在同一個類中);
(2)函數名字相同;
(3)參數不同;
(4)virtual 關鍵字可有可無
相信對C++有一定了解的朋友都知道函數重載的條件是:
在同一個作用域內
在C++繼承詳解之二——派生類成員函數詳解(函數隱藏、構造函數與兼容覆蓋規則)的開頭我也提到了,在派生類中定義一個函數名相同,參數名不同的函數,不是與基類中同名函數進行了函數重載,而是發生了函數隱藏。大家可以去我那篇文章開頭看一下那個例子。
因為首先函數重載的第一個條件就沒有滿足,即:在相同的范圍中(在同一個類中),派生類和基類是兩個不同的類域,即不是同一個作用域,所以在繼承中,基類和派生類之間永遠不可能進行函數重載。

 

class Base
{
public:
    Base(int data = 0)
        :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名的函數構成重載
    //因為作用域不同
};

下面這個圖僅僅代表函數之間的關系,不代表內存布局!
這裡寫圖片描述
那麼上面的原則中提到:
virtual關鍵字在函數重載中可有可無
那麼我們看一下加不加virtual對函數重載的影響。
(1).不加virtual

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

運行結果為:
這裡寫圖片描述
(2).加virtual
a.一個函數加virtual

class Base
{
public:
    Base(int data = 0)
        :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;
};

運行結果為:
這裡寫圖片描述
我們對代碼進行一下反匯編查看,
這裡寫圖片描述
可以看到,我們Base b中b一共有八個字節,前四個字節為指向虛表的指針,保存的是虛表的地址,後四個字節是Base類中int b的值,關於虛表的問題可以去我的上一篇博文學習查看 C++多態篇1一靜態聯編,動態聯編、虛函數與虛函數表vtable。
看過我上一篇博文後,或者對虛表有一定了解後,我們可以參照匯編代碼看,我們可以看到在匯編代碼中,調用重載函數是根據地址不同調用的,調用B(1)時,是進入虛表中調用的,但是不影響函數重載。
有的人可能要問,那麼不加virtual的函數編譯器在哪尋找呢?
實際上,編譯器將類的對象存儲時是按下圖這樣存儲的
這裡寫圖片描述
成員函數是單獨存儲的,所以編譯器在存儲成員函數那尋找函數即可
b.兩個函數都加virtual

class Base
{
public:
    Base(int data = 0)
        :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;
};

運行結果依然是:
這裡寫圖片描述
我們進行反匯編和在內存中查看可以得到:
這裡寫圖片描述
我們可以看到,因為B名的函數均為虛函數,所以均在虛表中存儲。
當編譯器調用時,就在虛表中查找調用。
c.多個函數加virtual<喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPjxiciAvPg0K0vLOqtTauq/K/dbY1NjW0KOs1Nqyu82swODT8tbQyseyu7m5s8m6r8r91tjU2LXEoaPL+dLUyc/D5s7Sw8e2vNa7t9bO9sHL1Nq7+cDg1tC1xNbY1NijrLKix9K2vNLRwb249tbY1Ni6r8r91/fOqsD919OjrLWryse24Lj2uq/K/bm5s8nW2NTY0rLKx7/J0tS1xKOstuC49rqvyv2803ZpcnR1YWy1xMfpv/a1yM2s09rBvbj2uq/K/ba8vNN2aXJ0dWFstcTH6b/2o6y2vLvhvavQ6bqvyv2808jr0Om6r8r9se3W0KOs1Nq199PDyrG9+Mjr0Om6r8r9se3W0L340NC199PDtcShozwvcD4NCjxwPs/W1Nq6r8r91tjU2NOmuMO+zcO709DOysziwcuwyX48YnIgLz4NCjxzdHJvbmc+tv6horqvyv24srjHPC9zdHJvbmc+PGJyIC8+DQrKssO0yse6r8r9uLK4x8TYo788YnIgLz4NCjxzdHJvbmc+uLK4x8rH1rjFycn6wOC6r8r9uLK4x7v5wOC6r8r9o6zM2NX3yscgPC9zdHJvbmc+PGJyIC8+DQqjqDGjqbK7zay1xLe2zqejqLfWsfDOu9PaxcnJ+sDg0+u7+cDgo6mjuzxiciAvPg0Ko6gyo6m6r8r9w/vX1s/gzayjuzxiciAvPg0Ko6gzo6myzsr9z+DNrKO7PGJyIC8+DQqjqDSjqbv5wOC6r8r9sdjQ69PQdmlydHVhbCC52Lz819ahozxiciAvPg0KtbHFycn6wOC21M/ztffTw9fTwODW0LjDzazD+7qvyv3Ksbvh19S2r7X308PX08Dg1tC1xLiyuMew5rG+o6y2+LK7yse4uMDg1tC1xLG7uLK4x7qvyv2w5rG+o6zV4tbWu/rWxr7NvdDX9riyuMehozxiciAvPg0Kuq/K/biyuMfT687Sw8fJz8Pmy7W1xLqvyv3W2NTY09DKssO0x/ix8MTYo788YnIgLz4NCsrXz8ijrLqvyv3W2NTY0qrH89TazazSu7j21/fTw9Pyo6y2+Lqvyv24srjH0OjSqtTasrvNrLe2zqfE2qGjPGJyIC8+DQrIu7rzvs3Kx7qvyv3W2NTY0qrH87LOyv2yu8/gzayjrLWryse6r8r9uLK4x9Kqx/Oyzsr9sdjQ68/gzayhozxiciAvPg0K1+6689K7teO+zcrHuq/K/dbY1NjW0LzTsru803ZpcnR1YWy2vL/J0tSjrLWrysfU2rqvyv24srjH1tC7+cDguq/K/dbQsdjQ69KqvNN2aXJ0dWFsudi8/NfWoaM8YnIgLz4NCr6tuf3Jz8PmtcS31s72ztLDx9aqtcDBy6OsztLDx9Tau/nA4LrNxcnJ+sDg1tC31rHwtqjS5cP719bP4M2so6yyzsr9srvNrLXEuq/K/aOs1Nq688PmtffTw7XEyrG68qOsseDS68b3zt63qL2ry/y0psDtzqq6r8r91tjU2KGjPGJyIC8+DQrEx8O0uq/K/biyuMfT1srHyrLDtMfpv/bE2KGjPGJyIC8+DQrG5Mq1uq/K/biyuMe31s6qwb3W1sfpv/ajujxiciAvPg0KPHN0cm9uZz4xLrbUz/O199PDuq/K/bXEx+m/9jwvc3Ryb25nPjxiciAvPg0KxcnJ+sDgttTP87X308O1xMrHxcnJ+sDgtcS4srjHuq/K/TxiciAvPg0Ku/nA4LXEttTP87X308O7+cDgtcS6r8r9PGJyIC8+DQrPwsPmv7S0+sLro7o8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;"> class Base { public: Base(int data = 1) :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 = 2) :d(data) { cout << "Derive()" << endl; } ~Derive() { cout << "~Derive()" << endl; } void Test() { cout << "Derive::Test()" << endl; } int d; }; int main() { Derive d; d.Test(); return 0; }

我們在上面的代碼中,分別在基類和派生類中定義了同名同參數的函數Test(),看一下運行結果,看會調用基類的函數還是派生類的函數:
這裡寫圖片描述
因為我在基類和派生類的構造函數中都輸出了語句,而且是打斷點調試的,所以沒有調用析構函數。
運行結果可以表明:
這裡的Test()函數發生了函數覆蓋。
那我們進入內存中看一下:
這裡寫圖片描述
PS:因為是我自己截圖畫圖的,不知道為什麼傳上來就壓縮了,如果大家看不清,可以ctrl+向上鍵放大看一下。
這張圖能夠更清楚地看到,在派生類的虛表中,只有一個函數,就是Derive::Test(),沒有從Base類繼承下來的Test(),所以能夠更清楚的看到發生了函數的覆蓋。
如果這樣你還沒太理解,那麼我就再多加幾個函數。
看下面的代碼:

class Base
{
public:
    Base(int data = 1)
        :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 = 2)
        :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 0;
}

從代碼可以看出在基類定義了三個虛函數,根據我們以前所說的知識,我們可以猜測基類會生成一個虛函數表,那麼派生類中我們定義了兩個同名同參數的函數,為了讓函數覆蓋的現象更加明顯,我特意沒有將Test3()定義,那麼我們現在看一下運行結果:
這裡寫圖片描述
由結果可知,基類對象調用的是基類的函數。派生類對象調用的是什麼呢?
我們進入內存中查看一下:
這裡寫圖片描述
由上圖我們可以看到,我們在派生類中定義了的函數,在派生類虛函數表中將基類函數覆蓋了,即派生類虛函數表中綠色的部分,而派生類沒有定義的函數,即Test3(),基類和派生類的函數地址完全相同。
這就更清楚的看出了,派生類中定義了同名同參數的函數後,發生了函數覆蓋。
2.指針或引用調用函數的情況
指向派生類的基類指針調用的也是派生類的覆蓋函數
還是上面的例子,我們將調用者換一下:

class Base
{
public:
    Base(int data = 1)
        :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 = 2)
        :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 0;
}

運行結果為:
這裡寫圖片描述
在內存布局為:
這裡寫圖片描述
由內存布局可以看出,指針pb指向的虛表就是派生類對象d所擁有的虛表,所以當然調用的是派生類已經覆蓋了的函數。
所以說:
多態的本質:不是重載聲明而是覆蓋。
虛函數調用方式:通過基類指針或引用,執行時會根據指針指向的對象的類,決定調用哪個函數。
三、函數隱藏
經過上面的分析我們知道,在不同的類域定義不同參數的同名函數,是無法構成函數重載的。
那麼當我們這麼做的時候,會發生什麼呢。
實際上,這種情況叫做函數隱藏。
* 隱藏”是指派生類的函數屏蔽了與其同名的基類函數,規則如下*
(1)如果派生類的函數與基類的函數同名,並且參數也相同,但是基類函數沒有virtual 關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆)
(2)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數將被隱藏(注意別與重載混淆)。
首先來看第一種情況。
1.同名同參數
那麼在上面的例子中我們試一下不加virtual關鍵字看看。
即將基類改為:

class Base
{
public:
    Base(int data = 1)
        :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 = 2)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test()
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

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

運行結果還是:
這裡寫圖片描述
這就是發生了函數的隱藏
再看下第二種情況
2.同名不同參數
(1)基類函數不加virtual

class Base
{
public:
    Base(int data = 1)
        :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 = 2)
        :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 0;
}

我們在基類中定義了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(1);
    return 0;
}

運行成功,結果為:
這裡寫圖片描述
這就是發生了函數隱藏~
(2)基類函數加virtual

class Base
{
public:
    Base(int data = 1)
        :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 = 2)
        :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 0;
}

編譯運行依然報錯:

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(1);
    return 0;
}

運行成功,結果為:
這裡寫圖片描述
這也是發生了函數隱藏。
現在函數隱藏應該沒有問題了吧~
總結一下前面的:
1.函數重載必須是在同一作用域的,在繼承與多態這裡,在基類與派生類之間是不能進行函數重載。
2.函數覆蓋是多態的本質,在基類中的虛函數,在派生類定義一個同名同參數的函數,就可以用派生類新定義的函數對基類函數進行覆蓋。
3.函數隱藏是發生在基類和派生類之間的,當函數同名但是不同參數的時候,不論是不是虛函數,都會發生函數隱藏。
這篇文章就暫且寫到這裡,本來是想對虛函數表進行深度剖析的,但是寫那一篇的時候發現,會用到這裡的知識,害怕初學者這裡還不清楚,所以現將這些問題整理一下,再更新下一篇文章。
如有問題歡迎批評指正,人無完人,文無完文,希望大家共同進步!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved