仮想テーブルに関する情報を探していましたが、わかりやすいものが見つかりませんでした。
誰かが私に説明付きの良い例を教えてもらえますか?
仮想テーブルがないと、関数へのすべての参照がコンパイル時にバインドされるため、ランタイムポリモーフィズムを機能させることができません。簡単な例
struct Base {
virtual void f() { }
};
struct Derived : public Base {
virtual void f() { }
};
void callF( Base *o ) {
o->f();
}
int main() {
Derived d;
callF( &d );
}
関数内では、それがオブジェクトを指しているcallF
ことだけがわかります。ただし、実行時に、コードは呼び出す必要があります(仮想であるため)。コンパイル時に、コンパイラは、何を指しているのかわからないため、呼び出しによって実行されるコードを知ることができません。o
Base
Derived::f
Base::f
o->f()
o
したがって、基本的に関数ポインタのテーブルである「仮想テーブル」と呼ばれるものが必要です。仮想関数を持つ各オブジェクトには、そのタイプのオブジェクトの仮想テーブルを指す「vテーブルポインタ」があります。
callF
上記の関数のコードはBase::f
、仮想テーブル(オブジェクトのv-tableポインターに基づいて検出されます)でエントリを検索するだけでよく、テーブルエントリが指す関数を呼び出します。それはそうかもしれませんが、たとえば、他のBase::f
何かを指している可能性もあります。Derived::f
つまり、仮想テーブルにより、呼び出される実際の関数は、仮想テーブルで関数ポインターを検索し、そのポインターを介して関数を呼び出すことによって実行時に決定されるため、実行時にポリモーフィズムを持つことができます。関数を直接(非仮想関数の場合のように)。
仮想関数テーブルは実装の詳細です。これは、コンパイラがクラスにポリモーフィックメソッドを実装する方法です。
検討
class Animal
{
virtual void talk()=0;
}
class Dog : Animal
{
virtual void talk() {
cout << "Woof!";
}
}
class Cat : Animal
{
virtual void talk() {
cout << "Meow!";
}
}
そして今、私たちは
A* animal = loadFromFile("somefile.txt"); // from somewhere
animal->talk();
どのバージョンtalk()
が呼び出されているかをどうやって知ることができますか?動物オブジェクトには、その動物で使用される仮想関数を指すテーブルがあります。たとえば、talk
他に2つの仮想メソッドがある場合、3番目のオフセットにある可能性があります。
dog
[function ptr for some method 1]
[function ptr for some method 2]
[function ptr for talk -> Dog::Talk]
cat
[function ptr for some method 1]
[function ptr for some method 2]
[function ptr for talk -> Cat::Talk]
のインスタンスがある場合、どのメソッドを呼び出すAnimnal
かわかりません。talk()
コンパイラはポインタに対応するものを知っているので、仮想テーブルを調べて3番目のエントリをフェッチすることでそれを見つけtalk
ます(コンパイラはAnimalの仮想メソッドを知っているので、vtable内のポインタの順序を知っています)。
動物が与えられた場合、適切なtalk()メソッドを呼び出すために、コンパイラーは3番目の関数ポインターをフェッチしてそれを使用するコードを追加します。次に、これは適切な実装に向けられます。
非仮想メソッドでは、呼び出される実際の関数はコンパイル時に決定できるため、これは必要ありません。非仮想呼び出しで呼び出すことができる関数は1つだけです。
あなたの見出しの質問に答えるために-あなたはそうしません、そしてC++標準はあなたがそれを提供されなければならないことを指定していません。あなたがしたいのは、次のように言えることです。
struct A {
virtual ~A() {}
virtual void f() {}
};
struct B : public A {
void f() {}
};
A * p = new B;
p->f();
A::fではなくB::fを呼び出します。仮想関数テーブルはこれを実装する1つの方法ですが、率直に言って、平均的なC++プログラマーには興味がありません。私はこのような質問に答えるときにのみ考えます。
簡単な答え:仮想関数呼び出しbasePointer-> f()は、basePointerの履歴に応じて異なる意味を持ちます。Reallyが派生クラスであるものを指している場合は、別の関数が呼び出されます。
このために、コンパイラは関数ポインタの単純なゲームを実行します。さまざまなタイプに対して呼び出される関数のアドレスは、仮想テーブルに格納されます。
仮想テーブルは、関数ポインタだけに使用されるわけではありません。RTTI機構は、実行時型情報(基本型の1つのアドレスによって参照されるオブジェクトの実際の型を取得する)にこれを使用します。
一部の新規/削除実装では、オブジェクトサイズが仮想テーブルに格納されます。
Windows COMプログラミングでは、仮想テーブルを使用して仮想テーブルに侵入し、インターフェイスとしてプッシュします。
仮想操作を定義する抽象基本クラスを想定して継承しますPlayer
。さらに、俳優に名前を尋ねる関数があるとします。Monster
Actor
name()
void print_information(const Actor& actor)
{
std::cout << "the actor is called " << actor.name() << std::endl;
}
コンパイル時に、アクターが実際にプレーヤーであるかモンスターであるかを推測することはできません。メソッドが異なるname()
ため、呼び出すメソッドの決定は実行時まで延期する必要があります。コンパイラーは、実行時にこの決定を行うことができるように、各アクター・オブジェクトに追加情報を追加します。
私が知っているすべてのコンパイラでは、この追加情報は、具象クラスに固有の関数ポインタ(多くの場合vtbl )のテーブルへのポインタ(多くの場合vptrと呼ばれます)です。つまり、すべてのプレーヤーオブジェクトは、すべてのプレーヤーメソッドへのポインターを含む同じ仮想テーブルを共有します(モンスターにも同じことが言えます)。実行時に、メソッドが呼び出されるオブジェクトのvptrが指すvtblからメソッドを選択することにより、正しいメソッドが見つかります。