C++虚関数テーブル、虚関数説明

17580 ワード

前言
最近多くの学生は私がC++虚表と虚関数について質問したことを私信して、そこでC++虚関数と虚表の原理の文章を書くつもりで、みんなのもっと良い理解と学習に役立ちます.
かそうかんすう
コンセプト
虚関数は、ベースクラスでvirtualキーワードで宣言される関数であり、1つ以上の派生クラスで再定義される関数です.虚関数の特徴は,ベースクラスのポインタを定義するだけで派生クラスのオブジェクトを指すことができることである.
[注意:虚関数がない場合は、C++の規定に従います.ベースクラスとして定義されたポインタは、派生クラスを指すポインタとしても使用できます.この派生クラスオブジェクトを指すポインタで継承されたベースクラスメンバーにアクセスできますが、派生クラスのメンバーにアクセスすることはできません.]
  • 虚関数を使用して実行時の多態性を実現する鍵は、これらの関数にベースクラスポインタでアクセスする必要があることです.
  • 関数が虚関数として定義されると、それが何層に伝わっても虚関数として維持されます.
  • 虚関数の再定義をオーバーロード(overriding)と呼び、オーバーロード(overloading)とは呼ばない.
  • 純虚関数:ベースクラスに定義された関数の1つであり、そのベースクラスに関連する定義がない関数のみが与えられる.純粋な虚関数により、派生クラスはすべて独自の関数バージョンを定義する必要があります.コンパイルエラーが発生しました.純虚関数定義の一般的な形式:
  •   virtual type func_name(args)=0;
    
  • 純虚関数を含むベースクラスを抽象ベースクラスと呼ぶ.抽象ベースクラスのもう一つの重要な特性:抽象クラスはオブジェクトを構築できません.しかし、抽象ベースクラスは、実行時のマルチステート性をサポートするために、独自のポインタを指すことができる.

  • ダミー関数サンプルコード
    #include"test.h"
    #include
    using namespace std;
    
    class Base{
    
    public:
    
        void printf()
        {
            cout << "Base printf()" << endl;
        }
    
        virtual void func()
        {
            cout << "Base func()" << endl;
        }
    
    };
    
    class Derived:public Base{
    
    public:
    
        void printf()
        {
            cout << "Derived printf()" << endl;
        }
    
        virtual void func()
        {
            cout << "Derived func()" << endl;
        }
    
    };
    

    例の説明
    上記のサンプルコードでは、printf()インスタンスメソッドが非虚関数であり、func()メソッドが虚関数として宣言される親クラスBaseとその派生クラスDeriveを宣言した.サブクラスではprintf()とfunc()法を再実現した.次に、各func()メソッドとprintf()メソッドにそれぞれサンプルオブジェクトでアクセスするDeriveインスタンスとBaseインスタンスをそれぞれ構築します.次に、新しいDerivedインスタンスを構築し、BaseポインタとDerivedポインタにそれぞれアドレスを割り当て、func()メソッドとprintf()メソッドにアクセスした結果をそれぞれ出力します.
    int main()
    {
        Base baseObj = Base();
        baseObj.func();
        baseObj.printf();
        Derived derivedObj = Derived();
        derivedObj.func();
        derivedObj.printf();
    
        Derived* pDerivedObj = new Derived();
        Base* pBaseObj = pDerivedObj;
        pDerivedObj->func();
        pBaseObj->func();
        pDerivedObj->printf();
        pBaseObj->printf();
        delete pDerivedObj;
        return 0;
    }
    

    実行結果
    Terminal output result:
    
    Base func()
    Base printf()
    Derived func()
    Derived printf()
    
    Derived func()
    Derived func()
    Derived printf()
    Base printf()
    

    結果の説明
    BaseインスタンスとDerivedインスタンスは、func()メソッドとprintf()メソッドにそれぞれアクセスします.実行結果は、それぞれ対応するfunc()メソッドとprintf()メソッドに出力されます.pDerivedObjとpBaseObjポインタはそれぞれDerivedインスタンスのアドレスを指し,pDerivedObjポインタの動作に対してそれ自体のメソッド出力を示しているが,同じオブジェクトのアドレスをpBaseObjポインタに割り当てると,その非虚関数printf()が親の挙動を表し,書き換えられていない様子がわかる.それはいったい何がこのような結果をもたらしたのだろうか.虚関数テーブルの説明を続けます.
    虚関数テーブルおよびメモリレイアウト
    虚関数(Virtual Function)は、虚関数テーブルを介して(Virtual Table)として実現される.V-Tableと略称する.この表では、主にクラスの虚関数のアドレス表が必要であり、この表は継承・上書きの問題を解決し、実際の関数をリアルに反応させることを保証する.このように、虚関数のあるクラスのインスタンスでは、このテーブルがこのインスタンスのメモリに割り当てられるので、親のポインタで1つの関数を操作するサブクラスの場合、この虚関数テーブルは地図のように、実際に呼び出すべき関数を示すことが重要になります.
    ここでは、この虚関数テーブルに重点を置いてみましょう.C++のコンパイラは,虚関数テーブルのポインタがオブジェクトインスタンスの先頭に存在することを保証する位置であるべきである.(これは、複数の継承または複数の継承がある場合に、虚関数テーブルのパフォーマンスが最も高いことを保証するためです.これは、オブジェクトインスタンスのアドレスからこの虚関数テーブルを取得し、関数ポインタを巡回し、対応する関数を呼び出すことができることを意味します.
    サンプルコード(以下、サンプルコードコンパイル環境はX 86であり、4 byteで位置合わせされている)
    非虚関数クラス
    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
    };
    

    メモリレイアウト
      class Base1   size(8):
        +---
       0    | a
       4    | c
            |  (size=3)
        +---
    +---
    

    ブロガーは今後、クラスメモリレイアウトでよく見られるバイト位置合わせの問題に注意するために、Base 1クラスにchar c変数を追加します.メモリ内のaおよびcメンバー変数は、宣言された順序に従って配列され(クラス内オフセットが0から始まる)、3バイトが整列に使用され、メンバー関数がメモリ領域を占めないことが明らかになった.
    単一継承派生クラスには非虚関数は含まれません
    class DerivedClass : public Base1
    {
        int c;
    public:
        void DerivedCommonFunction() {};
    };
    

    メモリレイアウト
      class DerivedClass    size(12):
        +---
       0    | +--- (base class Base1)
       0    | | a
       4    | | c
            | |  (size=3)
        | +---
       8    | c
        +---
    

    子クラスDerivedClassは親クラスBase 1のメンバー変数を継承し、メモリ配列では親クラスのメンバー変数を配列し、次に子クラスのメンバー変数を配列します.同様に、メンバー関数はバイトを占めません.
    虚関数クラスの存在
    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    

    メモリの分散
      class Base1   size(12):
        +---
       0    | {vfptr}
       4    | a
       8    | c
            |  (size=3)
        +---
    
      Base1::$vftable@:
        | &Base1_meta
        |  0
       0    | &Base1::VirtualFunction
    

    このメモリ構造図は2つの部分に分かれています.上はメモリ分布で、下は虚表です.一つ一つ見てみましょう.上の図から、虚表ポインタがメモリの先頭に置かれていることがわかります.(0アドレスオフセット)、次にメンバー変数;以下にダミーテーブルが生成され、&Base 1_metaのすぐ後ろに0が表示されます.このダミーテーブルに対応するダミーポインタはメモリに分布し、以下にダミー関数がリストされます.左側の0はこのダミー関数のシーケンス番号です.ブロガーはダミー関数を1つしか書いていないので、1つだけあります.複数のダミー関数があれば、シーケンス番号1、2のダミー関数があります.関数が表示されます.
    上の例を通して、虚表ポインタと虚表はいつ作成されたのかと聞いた学生がいました.コンストラクション関数が作成されたとき、すなわちクラスオブジェクトがインスタンス化されたときに作成されます.では、どのようにして虚表ポインタと虚表を利用して多態を実現するのでしょうか.虚関数を含む親クラスのオブジェクトを作成すると、コンパイラはオブジェクト構築時に虚表ポインタを親クラスの虚関数に指します.同様に、コンパイラは、サブクラスのオブジェクトを作成するときに、コンストラクション関数でダミーテーブルポインタ(サブクラスには1つのダミーテーブルポインタしかなく、親クラスから)をサブクラスのダミーテーブル(このダミーテーブル内のダミー関数エントリアドレスはサブクラス)に指し示すことで、マルチステートを実現することができます.
    単一継承派生クラスにも虚関数があり、上書き継承があります.
    class DerivedClass : public Base1
    {
        int d;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction() {};
    };
    

    メモリの分散
      class DerivedClass    size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | |  (size=3)
        | +---
      12    | d
        +---
    
      DerivedClass::$vftable@:
        | &DerivedClass_meta
        |  0
       0    | &DerivedClass::VirtualFunction
    

    上半分はメモリ分布であり、虚表ポインタが継承され、メモリ配列の先頭に位置していることがわかります.以下は親クラスのメンバー変数aとcであり、最後に子クラスのメンバー変数dであり、虚表ポインタは1つしかないことに注意してください.子クラスは虚表ポインタを生成していません.後半のダミーテーブルの場合、親と同じです.子が親のダミー関数メソッドを書き換えたため、生成されたダミーテーブル番号は1つしかありません.
    単一継承派生クラスにも虚関数があり、上書き継承は存在しません.
    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass : public Base1
    {
        int d;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction1() {};
    };
    

    メモリレイアウト
      class DerivedClass    size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | |  (size=3)
        | +---
      12    | d
        +---
    
      DerivedClass::$vftable@:
        | &DerivedClass_meta
        |  0
       0    | &Base1::VirtualFunction
       1    | &DerivedClass::VirtualFunction1
    

    この場合,メモリ分布の上半分にも1つのダミーポインタ変数のみがメモリ分布を順次並べているが,下のダミーテーブルの内容は変化しており,ダミーテーブルの0番は親のVirtualFunction,1番は子のVirtualFunction 2が置かれている.つまり、DerivedClassのオブジェクトが定義されている場合は、構築時に虚表ポインタがこの虚表を指し、後でVirtualFunctionが呼び出されている場合は親から対応する虚関数が検索され、VirtualFunction 1が呼び出されている場合は子から対応する虚関数が検索されます.
    単一継承派生クラスには上書き虚関数も非上書き虚関数も存在する
    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass : public Base1
    {
        int c;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction() {};
        void virtual VirtualFunction1() {};
    };
    

    メモリレイアウト
      class DerivedClass    size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | |  (size=3)
        | +---
      12    | c
        +---
    
      DerivedClass::$vftable@:
        | &DerivedClass_meta
        |  0
       0    | &DerivedClass::VirtualFunction
       1    | &DerivedClass::VirtualFunction1
    

    上のメモリレイアウトに基づいて,親クラスの虚関数も書き換え,新たに追加された虚関数もあり,最終的に虚関数テーブル0番と1番はサブクラスに対応する虚関数アドレスである.
    マルチ継承派生クラスに上書き虚関数継承が存在する
    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass1 : public Base1
    {
        int b;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass2 : public Base1
    {
        int d;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
    {
        int e;
    public:
        void DerivedDerivedCommonFunction() {};
        void virtual VirtualFunction() {};
    };
    

    メモリレイアウト
    class Base1 size(12):
    +---
    0   | {vfptr}
    4   | a
    8   | c
        |  (size=3)
    +---
    
    Base1::$vftable@:
    | &Base1_meta
    |  0
    0   | &Base1::VirtualFunction
    
    class DerivedClass1 size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | |  (size=3)
        | +---
      12    | b
        +---
    
      DerivedClass1::$vftable@:
        | &DerivedClass1_meta
        |  0
       0    | &DerivedClass1::VirtualFunction
    
      DerivedClass1::VirtualFunction this adjustor: 0
    
      class DerivedClass2   size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | |  (size=3)
        | +---
      12    | d
        +---
    
      DerivedClass2::$vftable@:
        | &DerivedClass2_meta
        |  0
       0    | &DerivedClass2::VirtualFunction
    
      DerivedClass2::VirtualFunction this adjustor: 0
    
      class DerivedDerivedClass size(36):
        +---
       0    | +--- (base class DerivedClass1)
       0    | | +--- (base class Base1)
       0    | | | {vfptr}
       4    | | | a
       8    | | | c
            | | |  (size=3)
        | | +---
      12    | | b
        | +---
      16    | +--- (base class DerivedClass2)
      16    | | +--- (base class Base1)
      16    | | | {vfptr}
      20    | | | a
      24    | | | c
            | | |  (size=3)
        | | +---
      28    | | d
        | +---
      32    | e
        +---
    
      DerivedDerivedClass::$vftable@DerivedClass1@:
        | &DerivedDerivedClass_meta
        |  0
       0    | &DerivedDerivedClass::VirtualFunction
    
      DerivedDerivedClass::$vftable@DerivedClass2@:
        | -16
       0    | &thunk: this-=16; goto DerivedDerivedClass::VirtualFunction
    

    上のメモリ分布の状況によって、この多継承オーバーライドの状況によって、私はそれぞれ各クラスのメモリ分布を打ち出しました.次に、このクラスDerivedDerivedClassを重点的に見てみましょう.外向的に見ると、継承された2つの親クラスDerivedClass 1とDerivedClass 2、そして自身のメンバー変数eが並んでいます.DerivedClass 1には、メンバー変数bと、0アドレスオフセットのダミーテーブルポインタと、メンバー変数aとcとが含まれています.DerivedClass 2のメモリ配列はDerivedClass 1に似ていて、DerivedClass 2にもBase 1が入っていることに気づきました.虚表の継承状況を見てみましょう.DerivedClass 1とDerivedClass 2の2つの虚表があります.&DerivedDericedClass_metaの下の数字はヘッダアドレスオフセット量0であり、DerivedClass 1の{vfptr}虚関数ポインタがDerivedDerivedClassのメモリオフセットでもあり、下の虚表の-16はこの虚表を指す虚ポインタのメモリオフセットを表し、これはDerivedClass 2の{vfptr}のDerivedDerivedClassのメモリオフセットである.
    DerivedDerivedClass()のダミーテーブルのVirtualFunction()ポインタ[局外画像アップロード中...(image-a 847 ce-1565342394654)]
    マルチ継承派生クラスに上書き虚関数継承は存在しません
    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass1 : public Base1
    {
        int b;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction1() {};
    };
    
    class DerivedClass2 : public Base1
    {
        int d;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction2() {};
    };
    
    class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
    {
        int e;
    public:
        void DerivedDerivedCommonFunction() {};
        void virtual VirtualFunction3() {};
    };
    
    

    メモリレイアウト
        class Base1 size(12):
        +---
        0   | {vfptr}
        4   | a
        8   | c
            |  (size=3)
        +---
    
        Base1::$vftable@:
        | &Base1_meta
        |  0
        0   | &Base1::VirtualFunction
    
      class DerivedClass1   size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | |  (size=3)
        | +---
      12    | b
        +---
    
      DerivedClass1::$vftable@:
        | &DerivedClass1_meta
        |  0
       0    | &Base1::VirtualFunction
       1    | &DerivedClass1::VirtualFunction1
    
      DerivedClass1::VirtualFunction1 this adjustor: 0
    
      class DerivedClass2   size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | |  (size=3)
        | +---
      12    | d
        +---
    
      DerivedClass2::$vftable@:
        | &DerivedClass2_meta
        |  0
       0    | &Base1::VirtualFunction
       1    | &DerivedClass2::VirtualFunction2
    
      DerivedClass2::VirtualFunction2 this adjustor: 0
    
      class DerivedDerivedClass size(36):
        +---
       0    | +--- (base class DerivedClass1)
       0    | | +--- (base class Base1)
       0    | | | {vfptr}
       4    | | | a
       8    | | | c
            | | |  (size=3)
        | | +---
      12    | | b
        | +---
      16    | +--- (base class DerivedClass2)
      16    | | +--- (base class Base1)
      16    | | | {vfptr}
      20    | | | a
      24    | | | c
            | | |  (size=3)
        | | +---
      28    | | d
        | +---
      32    | e
        +---
    
      DerivedDerivedClass::$vftable@DerivedClass1@:
        | &DerivedDerivedClass_meta
        |  0
       0    | &Base1::VirtualFunction
       1    | &DerivedClass1::VirtualFunction1
       2    | &DerivedDerivedClass::VirtualFunction3
    
      DerivedDerivedClass::$vftable@DerivedClass2@:
        | -16
       0    | &Base1::VirtualFunction
       1    | &DerivedClass2::VirtualFunction2
    

    この場合のメモリ分布は、マルチ継承を上書きするのと同様に、マルチ継承でメンバーの虚関数アドレスが最初の継承親の虚関数テーブルに保存されることに注意してください.
    マルチ継承の虚継承派生クラスに上書き虚関数継承が存在する
    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass1 : virtual public Base1
    {
        int b;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction1() {};
    };
    
    class DerivedClass2 : virtual public Base1
    {
        int d;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction2() {};
    };
    
    class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
    {
        int e;
    public:
        void DerivedDerivedCommonFunction() {};
        void virtual VirtualFunction3() {};
    };
    

    メモリの分散
        class Base1 size(12):
        +---
        0   | {vfptr}
        4   | a
        8   | c
            |  (size=3)
        +---
    
        Base1::$vftable@:
        | &Base1_meta
        |  0
        0   | &Base1::VirtualFunction
    
      class DerivedClass1   size(24):
        +---
       0    | {vfptr}
       4    | {vbptr}
       8    | b
        +---
        +--- (virtual base Base1)
      12    | {vfptr}
      16    | a
      20    | c
            |  (size=3)
        +---
    
      DerivedClass1::$vftable@DerivedClass1@:
        | &DerivedClass1_meta
        |  0
       0    | &DerivedClass1::VirtualFunction1
    
      DerivedClass1::$vbtable@:
       0    | -4
       1    | 8 (DerivedClass1d(DerivedClass1+4)Base1)
    
      DerivedClass1::$vftable@Base1@:
        | -12
       0    | &Base1::VirtualFunction
    
      DerivedClass1::VirtualFunction1 this adjustor: 0
      vbi:     class  offset o.vbptr  o.vbte fVtorDisp
                 Base1      12       4       4 0
    
      class DerivedClass2   size(24):
        +---
       0    | {vfptr}
       4    | {vbptr}
       8    | d
        +---
        +--- (virtual base Base1)
      12    | {vfptr}
      16    | a
      20    | c
            |  (size=3)
        +---
    
      DerivedClass2::$vftable@DerivedClass2@:
        | &DerivedClass2_meta
        |  0
       0    | &DerivedClass2::VirtualFunction2
    
      DerivedClass2::$vbtable@:
       0    | -4
       1    | 8 (DerivedClass2d(DerivedClass2+4)Base1)
    
      DerivedClass2::$vftable@Base1@:
        | -12
       0    | &Base1::VirtualFunction
    
      DerivedClass2::VirtualFunction2 this adjustor: 0
      vbi:     class  offset o.vbptr  o.vbte fVtorDisp
                 Base1      12       4       4 0
    
      class DerivedDerivedClass size(40):
        +---
       0    | +--- (base class DerivedClass1)
       0    | | {vfptr}
       4    | | {vbptr}
       8    | | b
        | +---
      12    | +--- (base class DerivedClass2)
      12    | | {vfptr}
      16    | | {vbptr}
      20    | | d
        | +---
      24    | e
        +---
        +--- (virtual base Base1)
      28    | {vfptr}
      32    | a
      36    | c
            |  (size=3)
        +---
    
      DerivedDerivedClass::$vftable@DerivedClass1@:
        | &DerivedDerivedClass_meta
        |  0
       0    | &DerivedClass1::VirtualFunction1
       1    | &DerivedDerivedClass::VirtualFunction3
    
      DerivedDerivedClass::$vftable@DerivedClass2@:
        | -12
       0    | &DerivedClass2::VirtualFunction2
    
      DerivedDerivedClass::$vbtable@DerivedClass1@:
       0    | -4
       1    | 24 (DerivedDerivedClassd(DerivedClass1+4)Base1)
    
      DerivedDerivedClass::$vbtable@DerivedClass2@:
       0    | -4
       1    | 12 (DerivedDerivedClassd(DerivedClass2+4)Base1)
    
      DerivedDerivedClass::$vftable@Base1@:
        | -28
       0    | &Base1::VirtualFunction
    

    上記のダミー継承のメモリ分布についてはあまり説明しないが、ダミー継承の役割はベースクラスへの重複を減らすことである(一般的なマルチ継承では二義性コンパイルのエラーが発生し、ダミー継承では二義性が解消される)が、対価はダミーポインタの負担(より多くのダミーポインタ)を増加させることである.以上の例によれば、ベースクラスにダミー関数がある場合:
  • 1各クラスには虚ポインタと虚表があります.
  • 2虚継承でなければ、子は親の虚ポインタを継承し、自身の虚テーブル(オブジェクト構造時に発生)を指します.虚関数の数がどれだけあるか、虚テーブル内の項目がどれだけあるか.多重継承の場合、複数のベースクラス虚テーブルと虚ポインタが存在する可能性があります.
  • 3ダミー継承の場合、サブクラスには2つのダミーポインタがあり、1つは自分のダミーテーブルを指し、もう1つはダミーベーステーブルを指し、多重継承時にダミーベーステーブルとダミーベーステーブルポインタが1つしかありません.

  • ブログの著作権は著者の所有であり、いかなる形式の転載も著者に連絡して許可を得て出典を明記してください.