特に、なんらかの関数ポインタが配置されている必要はありませんか?
9 に答える
「仮想関数を持つクラスは vtables で実装される」というフレーズは誤解を招くと思います。
このフレーズは、仮想関数を持つクラスが「方法 A」で実装され、仮想関数を持たないクラスが「方法 B」で実装されているように聞こえます。
実際には、仮想関数を持つクラスは、クラスとして実装されるだけでなく、vtable も持っています。それを見る別の方法は、「「vtables」はクラスの「仮想関数」部分を実装する」ということです。
両方がどのように機能するかの詳細:
すべてのクラス (仮想メソッドまたは非仮想メソッドを含む) は構造体です。C++ の構造体とクラスの唯一の違いは、既定では、メンバーが構造体ではパブリックであり、クラスではプライベートであることです。そのため、ここではクラスという用語を使用して、構造体とクラスの両方を指します。これらはほぼ同義語であることを忘れないでください。
データ メンバー
クラスは (構造体と同様)、各メンバーが順番に格納される連続したメモリの単なるブロックです。CPU アーキテクチャ上の理由でメンバー間にギャップが生じる場合があるため、ブロックはその部分の合計よりも大きくなる可能性があることに注意してください。
メソッド
メソッドまたは「メンバー関数」は幻想です。実際には、「メンバー関数」のようなものはありません。関数は常に、メモリ内のどこかに格納されている一連のマシン コード命令です。呼び出しを行うために、プロセッサはメモリのその位置にジャンプして実行を開始します。すべてのメソッドと関数は「グローバル」であり、その反対の兆候は、コンパイラによって強制された便利な錯覚であると言えます。
明らかに、メソッドは特定のオブジェクトに属しているかのように動作するため、明らかにそれ以上のことが行われています。メソッド (関数) の特定の呼び出しを特定のオブジェクトに結び付けるために、すべてのメンバー メソッドには、問題のオブジェクトへのポインターである隠し引数があります。このメンバーは、自分で C++ コードに追加しないという点で隠されていますが、魔法のようなものは何もなく、非常にリアルです。あなたがこれを言うとき:
void CMyThingy::DoSomething(int arg);
{
// do something
}
コンパイラは実際にこれを行います:
void CMyThingy_DoSomething(CMyThingy* this, int arg)
{
/do something
}
最後に、これを書くと:
myObj.doSomething(aValue);
コンパイラは次のように述べています。
CMyThingy_DoSomething(&myObj, aValue);
関数ポインターはどこにも必要ありません。コンパイラは、呼び出しているメソッドをすでに認識しているため、直接呼び出します。
静的メソッドはさらに単純です。thisポインターがないため、記述したとおりに実装されます。
それです!残りは便利な構文シュガーリングです。コンパイラはメソッドが属するクラスを認識しているため、どのクラスを指定せずに関数を呼び出さないようにします。また、その知識を使用して、明確な場合に変換myItem
します。this->myItem
(ええ、そうです。メソッド内のメンバー アクセスは、ポインターが表示されていなくても、常にポインターを介して間接的に行われます)
(編集:個別に批判できるように、最後の文を削除して個別に投稿しました)
非仮想メンバー関数は、ほとんど通常の関数に似ていますが、アクセス チェックと暗黙的なオブジェクト パラメーターを使用するため、実際には単なる構文糖衣です。
struct A
{
void foo ();
void bar () const;
};
基本的には次と同じです:
struct A
{
};
void foo (A * this);
void bar (A const * this);
vtable は、特定のオブジェクト インスタンスに対して適切な関数を呼び出すために必要です。たとえば、次の場合:
struct A
{
virtual void foo ();
};
「foo」の実装は次のようになります。
void foo (A * this) {
void (*realFoo)(A *) = lookupVtable (this->vtable, "foo");
(realFoo)(this); // Make the call to the most derived version of 'foo'
}
ポリモーフィズムを使用する場合は、仮想メソッドが必要です。修飾子は、遅延バインディングのvirtual
ためにメソッドをVMTに配置し、実行時に、どのクラスからどのメソッドを実行するかを決定します。
メソッドが仮想でない場合、コンパイル時にどのクラスインスタンスから実行されるかが決定されます。
関数ポインタは主にコールバックに使用されます。
仮想関数を持つクラスがvtableを使用して実装されている場合、仮想関数を持たないクラスはvtableなしで実装されます。
vtableには、適切なメソッドへの呼び出しをディスパッチするために必要な関数ポインターが含まれています。メソッドが仮想でない場合、呼び出しはクラスの既知のタイプに送られ、間接参照は必要ありません。
非仮想メソッドの場合、コンパイラーは通常の関数呼び出しを生成するか(たとえば、このポインターをパラメーターとして渡して特定のアドレスにCALLする)、またはインライン化することもできます。仮想関数の場合、コンパイラは通常、コンパイル時にコードを呼び出すアドレスを認識しないため、実行時にvtableでアドレスを検索し、メソッドを呼び出すコードを生成します。確かに、仮想関数の場合でも、コンパイラーはコンパイル時に正しいコードを正しく解決できる場合があります(たとえば、ポインター/参照なしで呼び出されたローカル変数のメソッド)。
(このセクションを元の回答から引き出して、個別に批判できるようにしました。はるかに簡潔で、質問の要点に達しているため、ある意味でははるかに優れた回答です)
いいえ、関数ポインタはありません。代わりに、コンパイラは問題を裏返しにします。
コンパイラは、オブジェクト内のポイント先の関数を呼び出す代わりに、オブジェクトへのポインターを使用してグローバル関数を呼び出します
なんで?通常、その方がはるかに効率的だからです。間接呼び出しは高価な命令です。
コンパイラ/リンカは、呼び出されるメソッドを直接リンクします。vtableの間接参照は必要ありません。ところで、それは「スタックとヒープ」と何の関係があるのでしょうか。
ランタイム中に変更できないため、関数ポインターは必要ありません。
分岐は、メソッドのコンパイル済みコードに直接生成されます。クラスにまったく含まれていない関数がある場合と同様に、ブランチはそれらに直接生成されます。