最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

一篇文章徹底弄懂C++虛函數的實現機制

 更新時間:2021年06月30日 12:57:12   作者:彼 方  
C++中的虛函數的作用主要是實現了多態(tài)的機制,基類定義虛函數,子類可以重寫該函數,在派生類中對基類定義的虛函數進行重寫時,需要在派生類中聲明該方法為虛方法,這篇文章主要給大家介紹了關于如何通過一篇文章徹底弄懂C++虛函數的實現機制,需要的朋友可以參考下

1、虛函數簡介

C++中有兩種方式實現多態(tài),即重載和覆蓋。

  • 重載:是指允許存在多個同名函數,而這些函數的參數表不同(參數個數不同、參數類型不同或者兩者都不同)。
  • 覆蓋:是指子類重新定義父類虛函數的做法,簡而言之就是用父類型別的指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數。這種技術可以讓父類的指針擁有“多種形態(tài)”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的代碼來實現可變的算法,比如:模板元編程是在編譯期完成的泛型技術,RTTI、虛函數則是在運行時完成的泛型技術。

關于虛函數的具體使用方法,建議大家先去閱讀相關的C++的書籍,本文只剖析虛函數的實現機制,讓大家對虛函數有一個更加清晰的認識,并不對虛函數的具體使用方法作過多介紹。本文是依據個人經驗和查閱相關資料最終編寫的,如有錯漏,希望大家多多指正。

2、虛函數表簡介

學過C++的人都應該知道虛函數(Virtual Function)是通過虛函數表(Virtual Table,簡稱為V-Table)來實現的。虛函數表主要存儲的是指向一個類的虛函數地址的指針,通過使用虛函數表,繼承、覆蓋的問題都都得到了解決。假如一個類有虛函數,當我們構建這個類的實例時,將會額外分配一個指向該類虛函數表的指針,當我們用父類的指針來操作一個子類的時候,這個指向虛函數表的指針就派上用場了,它指明了此時應該使用哪個虛函數表,而虛函數表本身就像一個地圖一樣,為編譯器指明了實際所應該調用的函數。指向虛函數表的指針是存在于對象實例中最前面的位置(這是為了保證取到虛函數表的有最高的性能——如果有多層繼承或是多重繼承的情況下),這就意味著理論上我們可以通過對象實例的地址得到這張?zhí)摵瘮当恚▽嶋H上確實可以做到),然后對虛函數表進行遍歷,并調用其中的函數。

前面說了一大堆理論,中看不中用,下面還是通過一個實際的例子驗證一下前面講的內容,首先定義一個Base類,該類有三個虛函數,代碼如下:

#include <iostream>
#include <string>

typedef void (*Fun)(void);

class Base
{
public:
    virtual void f()
    {
        std::cout << "Base::f()" << std::endl;
    }

    virtual void g()
    {
        std::cout << "Base::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base::h()" << std::endl;
    }
};

接下來按照前面的說法,我們通過Base類的實例對象base來獲取虛函數表,代碼如下:

int main(int argc, char* argv[])
{
    Base base;
    Fun fun = nullptr;

    std::cout << "指向虛函數表指針的地址:" << (long*)(&base) << std::endl;
    std::cout << "虛函數表的地址:" << (long*)*(long*)(&base) << std::endl;

    fun = (Fun)*((long*)*(long*)(&base));
    std::cout << "虛函數表中第一個函數的地址:" << (long*)fun << std::endl;
    fun();

    fun = (Fun)*((long*)*(long*)(&base) + 1);
    std::cout << "虛函數表中第二個函數的地址:" << (long*)fun << std::endl;
    fun();

    fun = (Fun)*((long*)*(long*)(&base) + 2);
    std::cout << "虛函數表中第三個函數的地址:" << (long*)fun << std::endl;
    fun();
}

運行結果圖2-1所示(Linux 3.10.0 + GCC 4.8.5):

圖2-1 程序運行結果

在上面的例子中我們通過把&base強制轉換成long *,來取得指向虛函數表的指針的地址,然后對這個地址取值就可以得到對應的虛函數表了。得到對應虛函數表的首地址后,就可以通過不斷偏移該地址,依次得到指向真實虛函數的指針了。這么說有點繞也有點暈,下面通過一幅圖解釋一下前面說的內容,詳見圖2-2

圖2-2 基類虛函數表內存布局

當然,上述內容也可以在GDB中調試驗證,后續(xù)的內容也將全部在GDB下直接驗證,調試的示例見圖2-3:

圖2-3 GDB查看基類虛函數表內存布局

3、有繼承關系的虛函數表剖析

前面分析虛函數表的場景是沒有繼承關系的,然而在實際開發(fā)中,沒有繼承關系的虛函數純屬浪費表情,所以接下來我們就來看看有繼承關系下虛函數表會呈現出什么不一樣的特點,分析的時候會分別就單繼承無虛函數覆蓋、單繼承有虛函數覆蓋、多重繼承、多層繼承這幾個場景進行說明。

3.1、單繼承無虛函數覆蓋的情況

先定義一個Base類,再定義一個Derived類,Derived類繼承于Base類,代碼如下:

#include <iostream>
#include <string>

class Base
{
public:
    virtual void f()
    {
        std::cout << "Base::f()" << std::endl;
    }

    virtual void g()
    {
        std::cout << "Base::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base::h()" << std::endl;
    }
};

class Derived : public Base
{
public:
    virtual void f1()
    {
        std::cout << "Derived::f1()" << std::endl;
    }

    virtual void g1()
    {
        std::cout << "Derived::g1()" << std::endl;
    }

    virtual void h1()
    {
        std::cout << "Derived::h1()" << std::endl;
    }
};

繼承關系如圖3-1所示:

圖3-1 類繼承關系UML圖

測試的代碼如下,因為等下要使用GDB來驗證,所以就隨便寫點,定義個Derived類實例就行了

int main(int argc, char* argv[])
{
    Derived derived;
    derived.f();
}

派生類Derived的虛函數表內存布局如圖3-2所示:

圖3-2 單繼承無虛函數覆蓋情況下派生類虛函數表內存布局

接下來就用GDB調試一下,驗證上圖的內存布局是否正確,如圖3-3所示:

圖3-3 GDB查看單繼承無虛函數覆蓋情況下派生類虛函數表內存布局

從調試結果可以看出圖3-2是正確的,Derived的虛函數表中先放Base的虛函數,再放Derived的虛函數。

3.2、單繼承有虛函數覆蓋的情況

派生類覆蓋基類的虛函數是很有必要的事情,不這么做的話虛函數的存在將毫無意義。下面我們就來看一下如果派生類中有虛函數覆蓋了基類的虛函數的話,對應的虛函數表會是一個什么樣子。還是老規(guī)矩先定義兩個有繼承關系的類,注意一下我這里只覆蓋了基類的g()

#include <iostream>
#include <string>

class Base
{
public:
    virtual void f()
    {
        std::cout << "Base::f()" << std::endl;
    }

    virtual void g()
    {
        std::cout << "Base::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base::h()" << std::endl;
    }
};

class Derived : public Base
{
public:
    virtual void f1()
    {
        std::cout << "Derived::f1()" << std::endl;
    }

    virtual void g()
    {
        std::cout << "Derived::g()" << std::endl;
    }

    virtual void h1()
    {
        std::cout << "Derived::h1()" << std::endl;
    }
};

繼承關系如圖3-4所示:

圖3-4 類繼承關系UML圖

測試的代碼如下,因為等下要使用GDB來驗證,所以就隨便寫點,定義個Derived類實例就行了

int main(int argc, char* argv[])
{
    Derived derived;
    derived.g();
}

派生類Derived的虛函數表內存布局如圖3-5所示:

圖3-5 單繼承有虛函數覆蓋情況下派生類虛函數表內存布局

接下來就用GDB調試一下,驗證上圖的內存布局是否正確,如圖3-6所示:

圖3-6 GDB查看單繼承有虛函數覆蓋情況下派生類虛函數表內存布局

從調試結果可以看出圖3-5是正確的,并且可以得到以下幾點信息:

覆蓋的g()被放到了虛表中原來父類虛函數的位置沒有被覆蓋的虛函數位置排序依舊不變

有了前面的理論基礎,我們可以知道對于下面的代碼,由base所指的內存中的虛函數表的Base::g()的位置已經被Derived::g()所取代,于是在實際調用發(fā)生時,調用的是Derived::g(),從而實現了多態(tài)

int main(int argc, char* argv[])
{
    Base* base = new Derived();
    base->f();
    base->g();
    base->h();
}

輸出結果如圖3-7所示:

圖3-7 程序運行結果

注意:在前面的例子中,我們分配內存的實例對象的類型是Derived,但是卻用Base的指針去引用它,這個過程中數據并沒有發(fā)生任何的轉換,實例的真實類型依舊是Derived,但是由于我們使用時用的是Base類型,所以函數調用要依據Base類來,不能胡亂調用,比如說我們此時是無法調用Derived的f1()和h1()的。由于這個是個單繼承,不存在虛函數表選擇問題,相對比較簡單。

3.3、多重繼承的情況

多重繼承就不分開講有覆蓋和無覆蓋的情況了,其實結合前面講的就差不多知道是什么個情況了,下面的例子中會設計成派生類既有自己的虛函數,又有用于覆蓋基類的虛函數,這樣就能兼顧有覆蓋和無覆蓋的情況了。

類的設計如下:

#include <iostream>
#include <string>

class Base1
{
public:
    virtual void f()
    {
        std::cout << "Base1::f()" << std::endl;
    }

    virtual void g()
    {
        std::cout << "Base1::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base1::h()" << std::endl;
    }
};

class Base2
{
public:
    virtual void f()
    {
        std::cout << "Base2::f()" << std::endl;
    }

    virtual void g()
    {
        std::cout << "Base2::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base2::h()" << std::endl;
    }
};

class Base3
{
public:
    virtual void f()
    {
        std::cout << "Base3::f()" << std::endl;
    }

    virtual void g()
    {
        std::cout << "Base3::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base3::h()" << std::endl;
    }
};

class Derived : public Base1, public Base2, public Base3
{
public:
    virtual void f()
    {
        std::cout << "Derived::f()" << std::endl;
    }

    virtual void g1()
    {
        std::cout << "Derived::g1()" << std::endl;
    }

    virtual void h1()
    {
        std::cout << "Derived::h1()" << std::endl;
    }
};

繼承關系如圖3-8所示:

圖3-8 類繼承關系UML圖

測試的代碼如下:

int main(int argc, char* argv[])
{
    Derived* d = new Derived();
    Base1* b1 = d;
    Base2* b2 = d;
    Base3* b3 = d;
    std::cout << (long*)(*(long*)b1) << std::endl;
    std::cout << (long*)(*(long*)b2) << std::endl;
    std::cout << (long*)(*(long*)b3) << std::endl;
}

輸出結果如圖3-9所示:

圖3-9 程序運行結果

輸出信息非常有趣,明明b1、b2、b3指向的都是d,但是它們各自取出來的虛函數表的地址卻完全不同,按理來說不是應該相同嗎?別急,下面我們通過圖3-10來看一看多繼承下派生類虛函數表的內存布局是什么樣的

圖3-10 多重繼承情況下派生類虛函數表內存布局

從圖3-10中可以看出以下幾點信息:

  • 在派生類中,每個基類都有一個屬于自己的虛函數表
  • 派生類自己特有的虛函數被放到了第一個基類的表中(第一個基類是按照繼承順序來確定的)

這里我們就會得出一個新問題了,對于上面例子中的b1,這個沒啥問題,因為它的類型Base1就是第一個被繼承的,所以我們當然可以認為這個不會出任何問題,但是對于b2呢,它被繼承的位置可不是第一個啊,運行時要怎么確定它的虛函數表呢?它有沒有可能一不小心找到Base1的虛函數去?恰好這個例子中幾個基類的虛函數名字和參數又都是完全相同的。這里其實就涉及到編譯器的處理了,當我們執(zhí)行賦值操作Base2* b2 = d;時,編譯器會自動把b2的虛函數表指針指向正確的位置,這個過程應該是編譯器做的,所以虛函數所實現的多態(tài)應該是“靜動結合”的,有部分工作需要在編譯時期完成的。

下面我們依然借助GDB來看一下實際的內存布局,詳見圖3-11,從調試信息中可以看出此時確實有三張?zhí)摵瘮当?,對應三個基類

圖3-11 GDB查看多重繼承情況下派生類虛函數表內存布局

第一張表的數據如圖3-12所示,可以看到和圖3-10描述的內容是一致的,Derived自己特有的虛函數確實被加入到了第一張表中了,這里指示虛函數表結束的表示好像是那個0xfffffffffffffff8,不知道是不是固定的,有知道的小伙伴麻煩評論區(qū)告訴我一下謝謝

圖3-12 派生類第一張?zhí)摵瘮当?/center>

第二張表的數據如圖3-13所示,這里的結束符變成了0xfffffffffffffff0,搞不懂

圖3-13 派生類第二張?zhí)摵瘮当?/center>

第三張表的數據如圖3-14所示,這里的結束符終于是0x0了

圖3-14 派生類第三張?zhí)摵瘮当?/center>

補充說明:如果繼承的某個類沒有虛函數的話,比如說將上面的Base2修改為以下格式:

class Base2
{
public:
    void f()
    {
        std::cout << "Base2::f()" << std::endl;
    }

    void g()
    {
        std::cout << "Base2::g()" << std::endl;
    }

    void h()
    {
        std::cout << "Base2::h()" << std::endl;
    }
};

main函數不變,再運行以下程序,輸出結果如圖3-15所示,說明此時就沒有指向Base2虛函數表的指針了,因為它本來就沒有虛函數表

圖3-15 程序運行結果

3.4、多層繼承的情況

多層繼承的在有前面的基礎上來理解就非常簡單了,測試程序如下:

#include <iostream>
#include <string>

class Base
{
public:
    virtual void f()
    {
        std::cout << "Base::f()" << std::endl;
    }

    virtual void g()
    {
        std::cout << "Base::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base::h()" << std::endl;
    }
};

class Derived : public Base
{
public:
    virtual void f()
    {
        std::cout << "Derived::f()" << std::endl;
    }

    virtual void g1()
    {
        std::cout << "Derived::g1()" << std::endl;
    }
};

class DDerived : public Derived
{
public:
    virtual void f()
    {
        std::cout << "DDerived::f()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "DDerived::h()" << std::endl;
    }

    virtual void g2()
    {
        std::cout << "DDerived::g2()" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    DDerived dd;
    dd.f();
}

繼承關系如圖3-16所示:

圖3-16 類繼承關系UML圖

派生類DDerived的虛函數表內存布局如圖3-17所示:

圖3-17 多層繼承情況下派生類虛函數表內存布局

多層繼承的情況這里就不使用GDB去看內存布局了,比較簡單,大家可以自行去測試一下。

4、總結

本文先對虛函數的概念進行了簡單介紹,引出了虛函數表這個實現虛函數的關鍵要素,然后對不同繼承案例下虛函數表的內存布局進行說明,并使用GDB進行實戰(zhàn)驗證。相信看完這篇文章后聰明的你會對虛函數有更加深刻的理解了。

到此這篇關于C++虛函數實現機制的文章就介紹到這了,更多相關C++虛函數實現機制內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • C++類中的運算符重載過程

    C++類中的運算符重載過程

    文章介紹了運算符重載在C++中的重要性以及實現方法,包括加法運算符重載、左移運算符重載、遞增運算符重載、+=運算符重載、關系運算符重載和賦值運算符重載
    2024-11-11
  • C++ EasyX學習之鼠標操作詳解

    C++ EasyX學習之鼠標操作詳解

    EasyX是針對C/C++的圖形庫,可以幫助使用C/C++語言的程序員快速上手圖形和游戲編程。本文將為大家詳細講講EasyX的鼠標操作,需要的可以參考一下
    2022-07-07
  • C語言Static?關鍵字解析

    C語言Static?關鍵字解析

    這篇文章主要介紹了C語言Static?關鍵字解析,C語言中staic關鍵字很簡單,簡單到你的任何一個項目中可以不寫一個staic關鍵字也是沒有問題的。寫這篇章主要是一下自己的staic的理解和應用,當然在章開頭依舊要照本宣科簡述一下static關鍵字,需要的朋友可以參考一下
    2022-02-02
  • C語言新手練習題之求第n個斐波那契數

    C語言新手練習題之求第n個斐波那契數

    斐波那契數列這一個大一上C語言就有的問題大家應該都不陌生,下面這篇文章主要給大家介紹了關于C語言新手練習題之求第n個斐波那契數的相關資料,文中通過圖文以及實例代碼介紹的非常詳細,需要的朋友可以參考下
    2022-11-11
  • C語言由淺入深講解線程的定義

    C語言由淺入深講解線程的定義

    這篇文章主要介紹了C語言中線程的基礎知識,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-05-05
  • C++超細致講解隊列queue的使用

    C++超細致講解隊列queue的使用

    隊列先進先出,即只能在容器的末尾添加新元素,只能從頭部移除元素,下面這篇文章主要給大家介紹了關于C++中隊列queue用法的相關資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下
    2022-05-05
  • c語言中字符串函數(庫函數使用)和模擬實現圖文教程

    c語言中字符串函數(庫函數使用)和模擬實現圖文教程

    C語言中對字符和字符串的處理很是頻繁,但是C語言本身并沒有字符串類型,這篇文章主要給大家介紹了關于c語言中字符串函數(庫函數使用)和模擬實現的相關資料,需要的朋友可以參考下
    2024-01-01
  • C++實現商品管理程序

    C++實現商品管理程序

    這篇文章主要為大家詳細介紹了C++實現商品管理程序,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-12-12
  • c語言5個常用的排序算法實例代碼

    c語言5個常用的排序算法實例代碼

    這篇文章主要介紹了c語言5個常用的排序算法實例代碼,分享了相關代碼示例,小編覺得還是挺不錯的,具有一定借鑒價值,需要的朋友可以參考下
    2018-02-02
  • 數據結構 棧的操作實例詳解

    數據結構 棧的操作實例詳解

    這篇文章主要介紹了數據結構 順序棧的定義、初始化、空棧判斷、入棧、出棧操作的相關資料,需要的朋友可以參考下
    2017-06-06

最新評論

清流县| 宁化县| 成都市| 沈阳市| 汝阳县| 灵寿县| 郑州市| 阿克陶县| 桐柏县| 黔东| 石台县| 铜山县| 乡宁县| 诸暨市| 东平县| 东港市| 台中县| 澜沧| 砚山县| 丰城市| 响水县| 鄂伦春自治旗| 延安市| 宁夏| 海晏县| 孟连| 沧州市| 石泉县| 乌苏市| 阆中市| 湘潭县| 黄山市| 西和县| 永宁县| 丰都县| 柳林县| 镇远县| 五原县| 镇安县| 昌乐县| 庆城县|