125

C++ の仮想関数については誰もが知っていますが、深いレベルではどのように実装されているのでしょうか?

実行時に vtable を変更したり、直接アクセスしたりできますか?

vtable はすべてのクラスに存在しますか、それとも少なくとも 1 つの仮想関数を持つクラスにのみ存在しますか?

抽象クラスは、少なくとも 1 つのエントリの関数ポインタに対して単に NULL を持っていますか?

単一の仮想関数を使用すると、クラス全体が遅くなりますか? それとも、仮想の関数への呼び出しのみですか? また、仮想関数が実際に上書きされているかどうかにかかわらず、速度は影響を受けますか、それとも仮想である限り効果はありませんか。

4

12 に答える 12

133

仮想機能はどのように深いレベルで実装されていますか?

「C++ の仮想関数」から:

プログラムで仮想関数が宣言されている場合は常に、クラスの av テーブルが構築されます。v-table は、1 つ以上の仮想関数を含むクラスの仮想関数へのアドレスで構成されます。仮想関数を含むクラスのオブジェクトには、メモリ内の仮想テーブルのベース アドレスを指す仮想ポインターが含まれます。仮想関数呼び出しがある場合は常に、v-table を使用して関数アドレスに解決されます。1 つ以上の仮想関数を含むクラスのオブジェクトには、メモリ内のオブジェクトの先頭に vptr と呼ばれる仮想ポインターが含まれます。したがって、この場合のオブジェクトのサイズは、ポインターのサイズだけ増加します。この vptr には、メモリ内の仮想テーブルのベース アドレスが含まれています。仮想テーブルはクラス固有であることに注意してください。クラスに含まれる仮想関数の数に関係なく、クラスの仮想テーブルは 1 つだけです。この仮想テーブルには、クラスの 1 つ以上の仮想関数のベース アドレスが含まれています。オブジェクトで仮想関数が呼び出されると、そのオブジェクトの vptr はメモリ内のそのクラスの仮想テーブルのベース アドレスを提供します。このテーブルには、そのクラスのすべての仮想関数のアドレスが含まれているため、関数呼び出しを解決するために使用されます。これは、仮想関数呼び出し中に動的バインディングが解決される方法です。そのオブジェクトの vptr は、メモリ内のそのクラスの仮想テーブルのベース アドレスを提供します。このテーブルには、そのクラスのすべての仮想関数のアドレスが含まれているため、関数呼び出しを解決するために使用されます。これは、仮想関数呼び出し中に動的バインディングが解決される方法です。そのオブジェクトの vptr は、メモリ内のそのクラスの仮想テーブルのベース アドレスを提供します。このテーブルには、そのクラスのすべての仮想関数のアドレスが含まれているため、関数呼び出しを解決するために使用されます。これは、仮想関数呼び出し中に動的バインディングが解決される方法です。

実行時に vtable を変更したり、直接アクセスしたりできますか?

普遍的に、答えは「いいえ」だと思います。vtable を見つけるためにメモリ マングリングを行うこともできますが、それを呼び出す関数シグネチャがどのように見えるかはまだわかりません。この機能 (言語がサポートするもの) で達成したいことはすべて、vtable に直接アクセスしたり、実行時に変更したりしなくても可能になるはずです。また、C++ 言語仕様では、vtables が必要であるとは指定されていませんが、ほとんどのコンパイラが仮想関数を実装する方法です。

vtable はすべてのオブジェクトに対して存在するのか、それとも少なくとも 1 つの仮想関数を持つオブジェクトに対してのみ存在するのか?

そもそも仕様では vtables が必要ないため、ここでの答えは「実装に依存する」であると思いますただし、実際には、最新のコンパイラはすべて、クラスに少なくとも 1 つの仮想関数がある場合にのみ vtable を作成すると思います。vtable に関連するスペースのオーバーヘッドと、仮想関数と非仮想関数の呼び出しに関連する時間のオーバーヘッドがあります。

抽象クラスは、少なくとも 1 つのエントリの関数ポインタに対して単に NULL を持っていますか?

答えは、言語仕様で指定されていないため、実装に依存します。純粋仮想関数を呼び出すと、定義されていない場合 (通常は定義されていません)、未定義の動作が発生します (ISO/IEC 14882:2003 10.4-2)。実際には、関数の vtable にスロットを割り当てますが、アドレスは割り当てません。これにより、vtable が不完全なままになり、派生クラスが関数を実装して vtable を完成させる必要があります。一部の実装では、vtable エントリに NULL ポインタを配置するだけです。他の実装では、アサーションと同様の処理を行うダミー メソッドへのポインターを配置します。

抽象クラスは純粋仮想関数の実装を定義できますが、その関数は修飾 ID 構文 (つまり、メソッド名でクラスを完全に指定すること) でのみ呼び出すことができることに注意してください。派生クラス)。これは、派生クラスがオーバーライドを提供することを引き続き要求しながら、使いやすい既定の実装を提供するために行われます。

単一の仮想関数を使用すると、クラス全体が遅くなりますか、それとも仮想関数の呼び出しのみが遅くなりますか?

これは私の知識の限界に達しているので、誰かが間違っている場合はここで助けてください!

クラス内の仮想関数のみが、仮想関数と非仮想関数の呼び出しに関連する時間のパフォーマンス ヒットを経験すると思います。クラスのスペースのオーバーヘッドはいずれにせよあります。vtable がある場合、objectごとに 1 つではなく、classごとに 1 つだけあることに注意してください。

仮想関数が実際にオーバーライドされているかどうかにかかわらず、速度は影響を受けますか?それとも仮想である限り、影響はありませんか?

オーバーライドされた仮想関数の実行時間が、基本仮想関数の呼び出しに比べて減少するとは思いません。ただし、派生クラスと基本クラスの別の vtable を定義することに関連するクラスには、追加のスペース オーバーヘッドがあります。

その他のリソース:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (帰りのマシン経由)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi/abi.html#vtable

于 2008-09-19T03:36:24.610 に答える
41
  • 実行時に vtable を変更したり、直接アクセスしたりできますか?

移植性はありませんが、汚いトリックを気にしないのであれば、もちろんです!

警告: このテクニックは、子供、 969歳未満の大人、アルファ ケンタウリの小さな毛むくじゃらの生き物にはお勧めできません。副作用として、悪魔が鼻から飛び出したり、その後のすべてのコード レビューで必要な承認者としてYog-Sothothが突然出現したり、既存のすべてのインスタンスに が遡及的に追加されたりすることが含まれる場合がありIHuman::PlayPiano()ます]

私が見たほとんどのコンパイラでは、vtbl * はオブジェクトの最初の 4 バイトであり、vtbl の内容は単にそこにあるメンバー ポインターの配列です (通常、宣言された順序で、基本クラスが最初になります)。もちろん、他の可能なレイアウトもありますが、それは私が一般的に観察したものです.

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

今、いくつかの悪ふざけを引っ張るために...

実行時のクラスの変更:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

すべてのインスタンスのメソッドを置き換える (クラスのモンキーパッチ)

vtbl 自体はおそらく読み取り専用メモリにあるため、これは少しトリッキーです。

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

後者は、mprotect の操作により、ウイルス チェッカーとリンクを目覚めさせ、注意を喚起する可能性が高くなります。NX ビットを使用するプロセスでは、失敗する可能性があります。

于 2008-09-19T13:39:41.653 に答える
3

通常、関数へのポインターの配列である VTable を使用します。

于 2008-09-19T03:31:21.110 に答える
2

この回答は、コミュニティ Wiki の回答に組み込まれています。

  • 抽象クラスは、少なくとも 1 つのエントリの関数ポインタに対して単に NULL を持っていますか?

その答えは、それが指定されていないということです。純粋仮想関数を呼び出すと、定義されていない場合 (通常は定義されていません)、未定義の動作が発生します (ISO/IEC 14882:2003 10.4-2)。一部の実装では、vtable エントリに NULL ポインタを配置するだけです。他の実装では、アサーションと同様の処理を行うダミー メソッドへのポインターを配置します。

抽象クラスは純粋仮想関数の実装を定義できますが、その関数は修飾 ID 構文 (つまり、メソッド名でクラスを完全に指定すること) でのみ呼び出すことができることに注意してください。派生クラス)。これは、派生クラスがオーバーライドを提供することを引き続き要求しながら、使いやすい既定の実装を提供するために行われます。

于 2008-09-19T04:01:47.637 に答える
2

関数ポインターをクラスのメンバーとして使用し、静的関数を実装として使用するか、メンバー関数へのポインターと実装のメンバー関数を使用して、C++ で仮想関数の機能を再作成できます。2 つの方法の間には、表記上の利点しかありません。実際、仮想関数呼び出し自体は表記上の利便性にすぎません。実際、継承は表記上の便宜にすぎません...継承用の言語機能を使用せずにすべて実装できます。:)

以下はテストされていないがらくたで、おそらくバグのあるコードですが、うまくいけばアイデアを示しています。

例えば

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};
于 2008-09-19T14:41:32.037 に答える
1

これらすべての回答でここで言及されていないのは、基本クラスがすべて仮想メソッドを持つ多重継承の場合です。継承クラスには、vmt への複数のポインターがあります。その結果、そのようなオブジェクトの各インスタンスのサイズが大きくなります。仮想メソッドを持つクラスでは、vmt に 4 バイト余分にあることは誰もが知っていますが、多重継承の場合は、仮想メソッドを持つ各基本クラスに 4 を掛けたものになります。4 はポインターのサイズです。

于 2015-04-12T19:42:16.710 に答える
1

各オブジェクトには、メンバー関数の配列を指す vtable ポインターがあります。

于 2008-09-19T03:33:03.573 に答える
0

Burlyの答えは、質問を除いてここで正しいです:

抽象クラスは、少なくとも1つのエントリの関数ポインタに対して単にNULLを持っていますか?

答えは、抽象クラスの仮想テーブルはまったく作成されないということです。これらのクラスのオブジェクトは作成できないため、必要ありません。

言い換えれば、私たちが持っている場合:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

pBを介してアクセスされるvtblポインターは、クラスDのvtblになります。これは、まさにポリモーフィズムの実装方法です。つまり、pBを介してDメソッドにアクセスする方法です。クラスBのvtblは必要ありません。

以下のマイクのコメントに応えて...

私の説明のBクラスにDによってオーバーライドされない仮想メソッドfoo()とオーバーライドされる仮想メソッドbar()がある場合、DのvtblはBのfoo()とそれ自体のbar()へのポインターを持ちます。B用に作成されたvtblはまだありません。

于 2008-09-19T04:55:19.117 に答える