2

私は仮想関数に関していくつかの疑問を持っています。それよりも、ランタイム ポリモーフィズムと言えます。私によると、私はそれが以下のように機能する方法を想定しました、

  1. 仮想テーブル (V-Table) は、少なくとも 1 つの仮想メンバー関数を持つすべてのクラスに対して作成されます。これは静的テーブルだと思うので、すべてのオブジェクトではなく、すべてのクラスに対して作成されます。ここで間違っている場合は、これを修正してください。

  2. この V-Table には、仮想機能のアドレスがあります。クラスに 4 つの仮想関数がある場合、このテーブルには、対応する 4 つの関数を指す 4 つのエントリがあります。

  3. コンパイラは、クラスの隠しメンバーとして仮想ポインター (V-Ptr) を追加します。この仮想ポインタは、仮想テーブルの開始アドレスを指します。

このようなプログラムがあるとします。

class Base
{
    virtual void F1();
    virtual void F2();
    virtual void F3();
    virtual void F4();
}
class Der1 : public Base  //Overrides only first 2 functions of Base class
{
    void F1(); //Overrides Base::F1()
    void F2(); //Overrides Base::F2()
}
class Der2 : public Base  //Overrides remaining functions of Base class
{
    void F3(); //Overrides Base::F3()
    void F4(); //Overrides Base::F4()
}
int main()
{
    Base* p1 = new Der1; //Believe Vtable will populated in compile time itself
    Base* p2 = new Der2;
    p1->F1(); //how does it call Der1::F1()
    p2->F3(); //how does it call Base::F3();
}

V-Table がコンパイル時に読み込まれる場合、なぜそれをランタイム ポリモーフィズムと呼ぶのでしょうか? 上記の例を使用して、vtables と vptr の数と、それがどのように機能するかを説明してください。私によると、Base、Der1、および Der2 クラス用に 3 つの Vtables があります。Der1 Vtable では、独自の F1() および F2() のアドレスを持ちますが、F3() および F4() のアドレスは Base クラスを指します。また、3 つの Vptr が Base、Der1、および Der2 クラスの隠しメンバーとして追加されます。コンパイル時にすべてが決定されている場合、実行時に正確に何が起こるのでしょうか?. 概念が間違っている場合は修正してください。

4

4 に答える 4

5

明らかに実装で定義されていますが、ほとんどの実装はかなり似ており、多かれ少なかれあなたが説明した線に沿っています。

  1. 正解です。

  2. vtable には、関数へのポインタ以上のものが含まれています。通常、RTTI 情報を指すエントリがあり、多くの場合、関数を呼び出すときに this ポインターを修正する方法に関する情報があります (これはトランポリンを使用して行うこともできます)。仮想ベースの場合、仮想ベースへのオフセットも存在する可能性があります。

  3. これも正しいです。構築と破棄の間vptr、オブジェクトの動的な型が変更されると、コンパイラは を変更することに注意してください。また、複数の継承 (仮想ベースの有無にかかわらず) の場合は、複数の が存在することに注意してくださいvptr。(vptrは、クラスのベース アドレスに対して固定オフセットにあり、多重継承の場合、すべてのクラスが同じベース アドレスを持つことはできません。)

最後の発言については、vtables はコンパイル時に設定され、静的です。ただし、vptr は動的な型に従って実行時に設定され、関数呼び出しはそれを使用して vtable を検索し、呼び出しをディスパッチします。

あなたの(非常に単純な)例では、各クラスに1つずつ、3つのvtableがあります。単純な継承のみが含まれるため、インスタンスごとに 1 つの vptr のみが存在し、Baseと派生クラスの間で共有されます。の vtable には、 、、およびBaseを指す 4 つのスロットが含まれます。の vtable には、 、、およびを指す 4 つのスロットも含まれます 。の vtable は、、および を指します。のコンストラクターは vptr をのテーブルに設定しますBase::f1Base::f2Base::f3Base::f4Der1Der1::f1Der1::f2Base::f3Base::f4Der2Base::f1Base::f2Der2::f3Der2::f4BaseBase; 派生クラスのコンストラクターは、最初に基本クラスのコンストラクターを呼び出し、次に vptr をその型に対応する vtable に設定します。(実際には、このような単純なケースでは、コンパイラーはおそらく vptr がコンストラクター内で使用されていないことを判断できるためBase、設定をスキップします。より複雑なケースでは、コンパイラーはベースのすべての動作を確認できません。ただし、クラス コンストラクターの場合はこの限りではありません)。

実行時ポリモーフィズムと呼ばれる理由については、次の関数を考えてみてください。

void f(Base* p)
{
    p->f1();
}

実際に呼び出される関数は、 aまたは aのどちらpを指すかによって異なります。つまり、実行時に決定されます。Der1Der2

于 2013-02-08T15:17:32.623 に答える
4

C++ 標準では、仮想関数呼び出しをどのように実装する必要があるかは指定されていませんが、広く受け入れられているアプローチの簡単な例を次に示します。

大まかに見ると、v テーブルは次のようになります。

ベース:

Index |  Function Address
------|------------------
    0 |  Base::F1
    1 |  Base::F2
    2 |  Base::F3
    3 |  Base::F4

Der1 :

Index |  Function Address
------|------------------
    0 |  Der1::F1
    1 |  Der1::F2
    2 |  Base::F3
    3 |  Base::F4

Der2 :

Index |  Function Address
------|------------------
    0 |  Base::F1
    1 |  Base::F2
    2 |  Der2::F3
    3 |  Der2::F4

と を作成するp1と、の vtable との vtable をそれぞれp2指すポインタが取得されます。Der1Der2

への呼び出しは、p1->F1基本的にp1「の仮想テーブルで関数 0 を呼び出す」ことを意味します。 vptr[0]であるDer1::F1ため、呼び出されます。

特定のオブジェクトに対して呼び出される関数は、実行時に (オブジェクトの vtable でルックアップを作成することによって) 決定されるため、実行時ポリモーフィズムと呼ばれます。

于 2013-02-08T15:16:28.290 に答える
2

これは実装定義です。virtualC++ でプログラミングする場合、メソッドを宣言すると、ポインターまたは参照の背後にあるオブジェクトの実行時の内容によって、どのコードが呼び出されるかが決まるということだけを考慮する必要があります。

おそらく、最初にそのトピックについて読む必要があります。ここにC++固有のものがあります。

于 2013-02-08T14:50:29.783 に答える