仮想関数を持つクラスが vtable を使用して実装されている場合、仮想関数を持たないクラスはどのように実装されるのでしょうか?
-
01-07-2019 - |
質問
特に、とにかく何らかの関数ポインターを配置する必要があるのではないでしょうか?
解決
非仮想メンバー関数は、通常の関数とほとんど同じですが、アクセス チェックと暗黙的なオブジェクト パラメーターを備えているため、実際には単なる構文糖です。
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'
}
他のヒント
「」という言葉があったと思います。仮想関数を持つクラスは vtable で実装されます」という言葉はあなたを誤解させます。
このフレーズは、仮想関数を備えたクラスが実装されているように聞こえます。方法Aで" 仮想関数のないクラスが実装されています"方法Bで".
実際には、仮想関数を備えたクラス、 に加えて クラスとして実装されているため、vtable もあります。これを理解する別の方法は、「『vtable』がクラスの『仮想関数』部分を実装している」ということです。
両方がどのように機能するかについて詳しくは、次をご覧ください。
すべてのクラス (仮想メソッドまたは非仮想メソッドを含む) は構造体です。の のみ 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);
関数ポインターはどこにも必要ありません。コンパイラは、どのメソッドを呼び出しているかをすでに認識しているため、そのメソッドを直接呼び出します。
静的メソッドはさらに単純です。彼らは持っていない これ ポインターなので、作成したとおりに実装されます。
それはそうです!残りは単なる便利な構文シュガーリングです。コンパイラはメソッドがどのクラスに属しているかを知っているため、どのクラスを指定せずに関数を呼び出せないようにします。また、その知識を利用して翻訳します myItem
に this->myItem
そうすることが明確な場合。
(はい、そのとおり:メソッド内のメンバーアクセスは いつも ポインターが表示されない場合でも、ポインターを介して間接的に行われます)
(編集:個別に批判できるよう、最後の文を削除して個別に投稿しました)
仮想メソッドはポリモーフィズムを使用する場合に必要です。の virtual
修飾子は遅延バインディングのためにメソッドを VMT に配置し、実行時にどのクラスからどのメソッドが実行されるかを決定します。
メソッドが仮想でない場合、どのクラス インスタンスから実行されるかはコンパイル時に決定されます。
関数ポインタは主にコールバックに使用されます。
仮想関数を持つクラスが vtable を使用して実装される場合、仮想関数を持たないクラスは vtable なしで実装されます。
vtable には、適切なメソッドへの呼び出しをディスパッチするために必要な関数ポインターが含まれています。メソッドが仮想でない場合、呼び出しはクラスの既知の型に進み、間接参照は必要ありません。
非仮想メソッドの場合、コンパイラは通常の関数呼び出し (たとえば、パラメータとして渡されたこのポインタを使用して特定のアドレスへの CALL) を生成したり、それをインライン化することもできます。仮想関数の場合、コンパイラは通常、コンパイル時にコードを呼び出すアドレスを知りません。そのため、実行時に vtable 内のアドレスを検索してメソッドを呼び出すコードを生成します。確かに、仮想関数の場合でも、コンパイラはコンパイル時に正しいコードを正しく解決できる場合があります (たとえば、ポインタ/参照なしで呼び出されるローカル変数のメソッドなど)。
(このセクションは、個別に批判できるように、元の回答から抜粋しました。これははるかに簡潔で質問の要点を押さえているので、ある意味でははるかに優れた答えです)
いいえ、関数ポインターはありません。代わりに、コンパイラが問題を解決します インサイドアウト.
コンパイラはグローバル関数を次のように呼び出します。 オブジェクトへのポインタ 電話をかける代わりに オブジェクト内のポイント先関数
なぜ?なぜなら、その方が通常ははるかに効率的だからです。間接呼び出しはコストのかかる命令です。
関数ポインターは実行時に変更できないため、必要ありません。
分岐はメソッドのコンパイルされたコードに直接生成されます。クラスにまったく存在しない関数がある場合と同様に、それらの関数への分岐が直接生成されます。
コンパイラ/リンカーは、どのメソッドが呼び出されるかを直接リンクします。vtable 間接参照は必要ありません。ところで、それは「スタック vs.ヒープ"?