C++語言虛函數實現多態的原理

2021-02-21 java那些事


自上一個帖子之間跳過了一篇總結性的帖子,之後再發,今天主要研究了c++語言當中虛函數對多態的實現,感嘆於c++設計者的精妙絕倫

C++中虛函數表的作用主要是實現了多態的機制。首先先解釋一下多態的概念,多態是C++的特點之一,關於多態,簡而言之就是 用父類的指針指向其子類的實例,然後通過父類的指針調用實際子類的成員函數。

這種方法呢,可以讓父類的指針具有多種形態,也就是說不需要改動很多的代碼就可以讓父類這一種指針,幹一些很多子類指針的事情,這裡是從虛函數的實現機制層面進行研究

在寫這篇帖子之前對於相關的文章進行了查閱,基本上是大段的文字,所以我的這一篇可能會用大量的圖形進行贅述(如果理解有誤的地方,煩請大佬能夠指出),

接下來就言歸正傳:

首先介紹一下為什麼會引進多態呢,基於c++的復用性和拓展性而言,同類的程序模塊進行大量重複,是一件無法容忍的事情,比如我設置了蘋果,香蕉,西瓜類,現在想把這些東西都裝到碗這個函數裡,那麼在主函數當中,聲明對象是必須的,但是每一次裝進碗裡對於水果來說,都要用自己的指針調用一次裝的功能。

那為什麼不把這些類抽象成一個水果類呢,直接定義一個水果類的指針一次性調用所有水果裝的功能呢,這個就是利用父類指針去調用子類成員,但是這個思想受到了指針指向類型的限制,也就是說表面指針指向了子類成員。

但實際上還是只能調用子類成員裡的父類成員,這樣的思想就變的毫無意義了,如果想要解決這個問題,只要在父類前加上virtual就可以解決了,這裡就是利用虛函數實現多態的實例。

首先還是作為舉例來兩個類,在之前基礎知識的帖子中提到過,空類的大小是一個字節(佔位符),函數,靜態變量都在編譯期就形成了,不用類去分配空間,但是做一個小實驗,看一看在定義了虛函數之後,類的大小是多少呢

#include<iostream>
using namespace std;
class CFather 
{
public:
    virtual void AA()  //虛函數標識符
    {
        cout << "CFather :: AA()" << endl;
    }
    void BB()
    {
        cout << "CFather  :: BB()" << endl;
    }
};
class CSon : public CFather
{

public:

    void AA()
    {
        cout << "CSon :: AA()" << endl;
    }
    void BB()
    {
        cout << "CSon :: BB()" << endl;
    }
};
int main()
{
    cout << sizeof(CFather) << endl;           //測試加了虛函數的類

    system("pause");
    return 0;
}

很明顯類裡裝了一個 4個字節的東西,除了整形int,就是指針了,沒錯這裡裝的就是函數指針

先把這個代碼,給抽象成圖形進行理解,在這CFather為A,CSon為B

 此時就是一個單純的繼承的情況,不存在虛函數,然後我new一個對象,A *p = new A;那麼 p -> AA(),必然是指向A類中的AA()函數,那麼函數的調用有兩種方式 一種函數名加()直接調用,一種是利用函數指針進行調用。

在這裡我想要調用子類的,就可以利用函數指針進行調用,假設出來兩個函數指針,來指向B類中的兩個成員函數,如果我父類想要調用子類成員,就可以通過 p指針去調用函數指針,再通過函數指針去調用成員函數

 每一個函數都可以用一個函數指針去指著,那麼每一類中的函數指針都可以形成自己的一個表,這個就叫做虛函數表

那麼在創建對象後,為什麼類中會有四個字節的內存空間呢?

在C++的標準規格說明書中說到,編譯器必需要保證虛函數表的指針存在於對象中最前面的位置(這是為了保證正確取到虛函數的偏移量)。這意味著我們通過對象實例的地址得到這張虛函數表,然後就可以遍歷其中函數指針,並調用相應的函數。

也就是說這四個字節的指針,代替了上圖中(p->*pfn)()的作用,指向了函數指針,也就是說,在使用了虛函數的父類成員函數,雖然寫的還是p->AA(),實際上卻是,(p->*(vfptr[0])),而指向哪個虛函數表就由,創建的對象來決定

至此,就能理解如何用虛函數這個機制來實現多態的了

下面,我將分別說明「無覆蓋」和「有覆蓋」時的虛函數表的樣子。沒有覆蓋父類的虛函數是毫無意義的。我之所以要講述沒有覆蓋的情況,主要目的是為了給一個對比。在比較之下,我們可以更加清楚地知道其內部的具體實現。

無虛數覆蓋

下面,再讓我們來看看繼承時的虛函數表是什麼樣的。假設有如下所示的一個繼承關係:

請注意,在這個繼承關係中,子類沒有重載任何父類的函數。那麼,在派生類的實例中,Derive d; 的虛函表:

我們可以看到下面幾點:

1)虛函數按照其聲明順序放於表中。

2)父類的虛函數在子類的虛函數前面。

有虛數覆蓋

覆蓋父類的虛函數是很顯然的事情,不然,虛函數就變得毫無意義。下面,我們來看一下,如果子類中有虛函數重載了父類的虛函數,會是一個什麼樣子?假設,我們有下面這樣的一個繼承關係。

為了讓大家看到被繼承過後的效果,在這個類的設計中,我只覆蓋了父類的一個函數:f()。那麼,對於派生類的實例,其虛函數表會是下面的一個樣子:

我們從表中可以看到下面幾點,

1)覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。

2)沒有被覆蓋的函數依舊。

這樣,我們就可以看到對於下面這樣的程序,

Base *b = new Derive();

b->f();

由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,於是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。

2019-05-28 00:15:30 編程小菜鳥自我反思,今天圖畫的太醜了,大家多多擔待,如果技術上有什麼偏差,大家可以踴躍批評我,謝謝!!!

 /*==========================手動分割線============================*/

感謝@奕韻風華提出的問題,現在將多繼承的虛函數實現多態的情況討論一下,另再加上從代碼層面上對這個機制有更深的理解

討論多繼承還是從有無虛函數覆蓋的情況來開始

無虛函數覆蓋

假設有下面這樣一個類的繼承關係。注意:子類並沒有覆蓋父類的函數。

對於子類實例中的虛函數表,是下面這個樣子:

我們可以看到:

1)  每個父類都有自己的虛表。

2)  子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)

這樣做就是為了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。

有虛函數覆蓋

下圖中,我們在子類中覆蓋了父類的f()函數。

下面是對於子類實例中的虛函數表的圖:

我們可以看見,三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,我們就可以任一靜態類型的父類來指向子類,並調用子類的f()了。比如

Derive d;

  Base1 *b1 = &d;

  Base2 *b2 = &d;

  Base3 *b3 = &d;

  b1->f(); //Derive::f()

  b2->f(); //Derive::f()

  b3->f(); //Derive::f()

  b1->g(); //Base1::g()

  b2->g(); //Base2::g()

  b3->g(); //Base3::g()

以上就是對多繼承情況的一種討論

那麼再看看如何在代碼的層面上來驗證原理呢?

首先在主函數內聲明了一個父類的指針,指向子類對象,那麼這個對這個父類指針解引用的話,就能得到一個vfptr指針,和父類,子類對象,但是現在我只需要指向虛函數的指針,那麼就可以定義指針只取前四個字節,利用強轉,*(int *)p 這個得到了vfptr的地址。

那麼繼續想要獲得虛函數表中的虛函數,就是再次解引用了,但是如何進行偏移呢?在整形,浮點型裡,指針的偏移量都是指針指向類型所覺得的,而這裡是個函數指針,函數指針不允許利用指針指向類型來進行偏移量的取值。

因為函數的類型大小是不確定的,但是我們知道,虛函數表裡都是函數指針,指針的大小是確定的都是四個字節,那還可以繼續利用強轉,控制指針每次偏移四個字節,那麼這個時候再進行解引用就是我們所取得函數的地址了,如果語言太贅述的話,可以看下面的例子

//
#include <iostream>
using namespace std;


class CFather
{
public:
    virtual void AA()
    {
        cout << "CFather::AA" << endl;
    }
    virtual void BB()
    {
        cout << "CFather::BB" << endl;
    }
    virtual void CC()
    {
        cout << "CFather::BB" << endl;
    }
    void DD()
    {
        cout << "CFather::DD" << endl;
    }
};

class CSon:public CFather
{
public:
    virtual void AA()
    {
        cout << "CSon::AA" << endl;
    }
    virtual void BB()
    {
        cout << "CSon::BB" << endl;
    }
    void DD()
    {
        cout << "CSon::DD" << endl;
    }
    virtual void EE()
    {
        cout << "CSon::EE" << endl;
    }
};


int main()
{

    typedef void (*PFUN)();

    cout << sizeof(CFather) << endl;
    CFather* p = new CSon;
    PFUN aa = (PFUN)*((int*)*(int*)p+0);
    PFUN bb = (PFUN)*((int*)*(int*)p+1);
    PFUN cc = (PFUN)*((int*)*(int*)p+2);
    PFUN dd = (PFUN)*((int*)*(int*)p+3);
    PFUN ee = (PFUN)*((int*)*(int*)p+4);



    system("pause");
    return 0;
}

通過監視就能直接看到(因為vs編譯器不允許,利用父類指針直接使用虛函數不覆蓋情況下的子類成員函數,利用這個方法也可以查看子類虛函數)

驗證了我上面敘述的原理,首先父類中先 生成了虛函數表,再繼承到子類當中,如果子類中有重載的函數,直接重寫,沒有的話直接添加

連結:
https://www.cnblogs.com/xgmzhna/p/10934562.html

推薦程式設計師必備微信號 

在這裡,我們分享程式設計師相關技術,職場生活,行業熱點資訊。不定期還會分享IT趣文和趣圖。這裡屬於我們程式設計師自己的生活,工作和娛樂空間。

相關焦點

  • c++是如何實現多繼承帶來的多態問題?
    多態是指同樣的消息被不同類型的對象接收時導致完全不同的的行為。有虛函數的類才能叫多態類型的類,可以從探索虛函數是如何實現動態綁定的來了解如何實現多繼承中的多態。單繼承時虛函數動態綁定的實現原理每個類各有一個虛表(虛函數表),虛表的內容是由編譯器安排的。
  • C++ 虛函數表及多態內部原理詳解
    (給CPP開發者加星標,提升C/C++技能)https://blog.csdn.net/songguangfan/article/details/87898915C++ 中的虛函數的作用主要是實現了多態的機制
  • 面試必知必會|理解C++虛函數
    C++語言一直在增加很多新特性來提高使用者的便利性,但是每種特性都有複雜的背後實現,充分理解實現原理和設計原因,才能更好地掌握這種新特性。只要出發總會達到,只有出發才會到達,焦慮沒用,學就完了,今天一起來學習C++的虛函數考點吧。
  • c++ 內存,虛函數,運算函數,三角函數
    :就是java中的抽象,純虛函數只有聲明沒有具體實現就是空方法,在子類中必須重新寫,虛函數就是在基類中寫了有實現。他們都得用關鍵字 virtual(虛擬的) 聲明的函數1. 虛函數和純虛函數可以定義在同一個類(class)中,含有純虛函數的類被稱為抽象類(abstract class),而只含有虛函數的類(class)不能被稱為抽象類(abstract class)。2.
  • C++ | 虛函數簡介
    本文將簡單探究一下 c++ 中的虛函數實現機制。本文主要基於 vs2013 生成的 32 位代碼進行研究,相信其它編譯器(比如,gcc)的實現大同小異。先從對象大小開始 假設我們有如下代碼,假設 int 佔 4 字節,指針佔 4 字節。
  • C++中的虛函數(virtual function)
    1.簡介 虛函數是C++中用於實現多態(polymorphism)的機制。核心理念就是通過基類訪問派生類定義的函數。
  • C++虛函數詳解
    有鑑於虛函數的重要性以及由此產生的眾多的問題,我覺得有必要把虛函數的實現機制梳理一遍,C++中的虛函數的作用主要是實現了多態的機制。
  • 軟體特攻隊|針對C++編譯期多態與運行期多態,我有話說
    時至今日的C++已經發展成為一種多次語言所組成的語言集合,而其中泛型編程與基於它的STL是C++發展歷程中最為精彩的那一部分。在C++的面向對象編程中,多態是OO三大特性之一,我們稱為運行期多態,也稱它為動態多態;但在泛型編程中,多態是基於模板的具現化與函數的重載解析,由於這種多態發生於編譯期,所以稱它為編譯期多態或靜態多態。
  • C++虛函數表剖析
    關鍵詞:虛函數,虛表,虛表指針,動態綁定,多態一、概述為了實現C++的多態,C++使用了一種動態綁定的技術
  • 每日一問(11) 什麼是虛函數
    別人都知道,我不知道 才是最尷尬的地方C++通過指針實現了多態,運行時函數重載決議,是他最有優秀地方,但是也是最讓人痛苦地方,內存模型假設存在讓對象生命周期管理更加複雜。我需要你必須重視起來,思想上重視就是口號,必須採取行動必須閱讀RocksDB是使用C++編寫的嵌入式kv存儲引擎 和看到別人是怎麼用的,從這裡開始,簡述C++虛函數作用及底層實現原理
  • C++語言中的「虛函數」就像C語言中的指針,必須要弄懂的
    上一節較為詳細的討論了C++語言中基類被派生類繼承過程中的內存模型,尤其較為詳細的分析了虛函數及其虛表、虛表指針在內存中是如何分布,如何存儲的,這對於理解C++語言中的「動態綁定」是極有幫助的。理解C++語言中的「動態綁定」正如之前兩篇文章所討論的,C++語言中虛函數的「動態綁定」能為多態的實現帶來極大的便利——「動態綁定」機制是在程序運行時根據指針所指向對象的類型(而不是指針本身類型
  • C++中虛函數和純虛函數的區別與總結
    定義他為虛函數是為了允許用基類的指針來調用子類的這個函數。定義一個函數為純虛函數,才代表函數沒有被實現。定義純虛函數是為了實現一個接口,起到一個規範的作用,規範繼承這個類的程式設計師必須實現這個函數。虛函數只能藉助於指針或者引用來達到多態的效果。C++純虛函數一、定義 純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。
  • C語言實現繼承和多態
    下面就用純C自己寫一個簡單的類的繼承、多態。繼承 繼承大體是用派生類的第一個地址放置基類的對象,這樣我們用強轉也不會出現問題。polymorphic 運行時多態一言蔽之:基類的指針或者引用指向子類的對象。
  • C++中多態的概念和意義
    ;而不是根據指針類型(編譯器默認的安全行為);父類指針(引用)指向:父類對象,則調用父類中定義的函數;子類對象,則調用子類中定義的重寫函數;      3、面向對象中的多態的概念:   4、C++ 語言直接支持多態的概念:
  • C語言實現面向對象的原理
    在性能不是很好、資源不是很多的MCU中使用C語言面向對象編程就顯得尤為重要。要想使用C語言實現面向對象,首先需要具備一些基礎知識。比如:(C語言中的)結構體、函數、指針,以及函數指針等,(C++中的)基類、派生、多態、繼承等。
  • 70道C語言與C++常見問答題
    ((void ()( ))0)( ):這就是上句的函數名所對應的函數的調用。19 C語言的指針和引用和c++的有什麼區別?36 說一說c++中四種cast轉換C++中四種類型轉換是:static_cast, dynamic_cast, const_cast, reinterpret_cast用於各種隱式轉換,比如非const轉const,void*轉指針等, static_cast能用於多態向上轉化,如果向下轉能成功但是不安全,結果未知;用於動態類型轉換。只能用於含有虛函數的類,用於類層次間的向上和向下轉化。
  • c++虛繼承,多繼承
    最後附上內存結構圖:例2: 為什麼虛函數效率低?因為虛函數需要一次間接的尋址,而普通的函數可以在編譯時定位到函數的地址,虛函數是要根據虛指針定位到函數的地址。多重繼承本身並不複雜,對象布局也不混亂,語言中都有明確的定義。真正複雜的是使用了運行時多態(virtual)的多重繼承(因為語言對於多態的實現沒有明確的定義)。要了解C++,就要明白有很多概念是C++ 試圖考慮但是最終放棄的設計。你會發現很多Java、C#中的東西都是C++考慮後放棄的。
  • C語言與C++面試知識總結
    重載多態(Ad-hoc Polymorphism,編譯期):函數重載、運算符重載子類型多態(Subtype Polymorphism,運行期):虛函數參數多態性(Parametric Polymorphism,編譯期):類模板、函數模板強制多態(Coercion Polymorphism,編譯期/運行期):基本類型轉換、自定義類型轉換靜態多態(編譯期/早綁定)
  • C++中值語義多態的一種實現方法
    C++中的運行時多態通常是採用虛函數實現的,譬如命令模式,通常定義如下接口:class ICommand{public: virtual ~ICommand() = default; virtual void execute() const
  • C語言是C++的母語,萬變不離指針,指針是C語言的一大法寶!
    眾所周知,類有三大特性:封裝、繼承、多態。我們來看看C語言如何借鑑類的三大特性來更好的組織代碼。 1、繼承 C語言沒有嚴格意義上的繼承,可以藉助結構體嵌套實現類似於繼承的形式,但始終不盡人意。