C++ での仮想関数の呼び出しは、コンパイル時ではなく実行時に決定されると言います。そのため、コンパイル時と実行時の違いについては明確ではないと思います。私の大まかな考えでは、すべてがコンパイル時に決定されるべきです...誰がこの質問を助けることができますか? ありがとう!</p>
8 に答える
この単純化された例を見てください。
struct Base
{
virtual void func()
{ std::cout << "Base::func()" << std::endl; }
};
struct Derived : Base
{
virtual void func()
{ std::cout << "Derived::func()" << std::endl; }
};
仮想関数を持つ基本クラスと、それをオーバーライドする派生クラスがあります。メインプログラムは次のとおりです。
int main()
{
Base *bp = 0;
std::string input;
std::cin >> input;
if (input == "base")
bp = new Base;
else
bp = new Derived;
/* The compiler cannot decide which
function is called here: */
bp->func();
return 0;
}
コンパイラはbp->func()
、ユーザーからの入力に依存するため、基本クラスの関数で呼び出されるか、派生クラスの関数で呼び出されるかを判断できません。
これは、コンパイル時と実行時の違いを示しています。コンパイラはコンパイル時にコードをマシン コードに変換しますが、ユーザー入力は実行時にしか利用できません。
(私のコード例は完全なコードではありません。たとえば、仮想デストラクタを宣言せずに仮想関数を使用してクラスを宣言しています。他にも問題があります。これは、コンパイル時と実行時の違いを説明し、何を示すかを示すことのみを目的としています。可能であり、毎回不可能なことです。)
class C
{
public:
virtual void f() {}
};
class D : public C
{
public:
void f() {}
};
void fn(C * c)
{
// Is C::f or D::f called here?
c->f();
}
呼び出しがあるという事実は、コンパイル時に決定されます。呼び出されるメンバー関数は、オブジェクトが既知である場合にのみ知ることができます。たとえば、
Base* ptr = (rnd() % 2 ? new D1() : new D2());
ptr->vf();
が仮想関数のvf()
場合、コンパイル時にどちらが呼び出されるか (D1 と D2 がそれぞれ独自のものを持っていると仮定) を知ることはできません。vf()
任意の関数内で、コンパイル時に、入力パラメーターの値に関係なく、関数の可能な実行で true になるものだけを推測できます。関数へのデータ入力がその動作を変更する可能性がある場合は常に、コンパイル時に結果を推測することはできません。
例えば:
class A
{
virtual void virt() = 0;
};
class B : public A
{
virtual void virt() { /*some computation */};
};
class C : public A
{
virtual void virt() { /*some other computation */};
};
void f(A* a)
{
a->virt();
}
ここで、 をコンパイルするときf
、 が指すオブジェクトがまたはa
の型になるかどうB
かC
、または (このコンパイル単位では) わからない他の派生型になるかどうかを知る方法はありません。実際、これはユーザー入力に依存する可能性があり、実行ごとに異なる場合があります。
したがって、コンパイル時には、 を入力したときに実際にどの関数が呼び出されるかわかりませんa->virt()
。
ただし、ランタイムでは、実際に何a
が指されているかを知っているため、どの関数が呼び出されるかを判断できます。
実際には、仮想関数呼び出しは、クラスのすべての仮想関数へのポインターの配列であるvtableを使用して解決されます。
#include <iostream>
struct Base { virtual void foo() { std::cout << "base\n"; } };
struct Derived : Base { void foo() { std::cout << "derived\n"; } };
int main() {
Base b;
Derived d;
bool flag;
if (std::cin >> flag) {
Base *ptr = flag ? &b : &d;
ptr->foo();
} else {
std::cout << "error\n";
}
}
あなたが現在何を「コンパイル時」と考えているかはわかりませんが、プログラムのコンパイルとリンクはどちらも、プログラムが実行される前、つまりユーザーが入力を提供する前に行われます。したがって、 への呼び出しの宛先は、foo
実行時のユーザー入力に依存するため、コンパイル時に決定できない可能性があります。
foo
が非仮想関数である場合Base::foo()
、 の値に関係なく呼び出されるflag
ため、その場合、宛先はコンパイル時に認識されます。
もう少し技術的なレベルでは(私の事実がまっすぐになっていることを願っています:S)、ポリモーフィズムを実現するために構築されたvtableと呼ばれるものがあります。
基本的に、クラスごとに1つのvtableしか存在できないため、クラスのインスタンスはすべて同じvtableを共有し、vtableはプログラマーには見えず、仮想関数の実装へのポインターが含まれています。
vtableをビルドするのはコンパイラであり、必要な場合にのみビルドされます(つまり、クラスまたはその基本クラスにが含まれている場合。したがって、すべてのクラスがvtableをビルドvirtual function
するわけではないことに注意してください。
時間の例:
class Base {
public:
virtual void helloWorld();
}
class Derived : public Base {
public:
void helloWorld();
}
int main(void) {
Derived d;
Base *b = &d;
b->helloWorld(); // here is the magic...
/* This call is actually translated to something like the line below,
lets assume we know that the virtual pointer pointing to the viable
for Derived is called Derived_vpointer (but it's only a name and
probably not what it would be called):
*(b -> Derived_vpointer -> helloWorld() )
*/
つまり、b->helloWorld()
が呼び出されると、実際にはvpointerを使用してvtableを検索します。これは、仮想関数の正しいバージョンへの呼び出しをガイドするために置き換えられます。したがって、ここのクラス Derived
には、vtableとテーブルを指す仮想ポインターがあります。したがって、bがDerivedインスタンスを指している場合、 Derivedのvpointerを使用して、最終的に正しい実装を呼び出します。
これは実行時に実行されます。つまり、別のクラスでBaseを拡張し、この(これを)クラスにポイントさせることができるため、ルックアップは実行時に実行されます。再び使用すると、のvpointerがへの呼び出しを評価するために使用されます。b
AnotherDerived
b->helloWorld()
AnotherDerived
helloWorld()
それでは、コードで取得しましょう。
...
int main(void) {
Derived derived;
AnotherDerived anotherDerived;
Base *base;
base->helloWorld();
/* base points to a Base object, i.e. helloWorld() will be called for base. */
*base = &derived; // base's vpointer will point at the vtable of Derived!
base->helloWorld();
/* calling:
base->Derived_vpointer->helloWorld();
*/
*base = &anotherDerived;
base->helloWorld(); // base's vpointer will point at the vtable of AnotherDerivedClass
/* calling:
base->AnotherDerived_vpointer->helloWorld();
*/
一般に、仮想関数を呼び出すときにどのコードが実行されるかはわかりません。
struct Base
{
virtual void method() = 0;
};
void foo(Base* p)
{
p->method(); // What code will be execute here?
}
から派生したクラスが複数ある場合Base
、どのコードが実行されますか?
上記に加えて、短い コンパイル時間はソースコードがコンパイルされる時間であり、実行時間はコンパイルされたコードが実行される時間であり、プログラムへの入力に依存する可能性があります....したがって、どのオブジェクト参照が仮想関数にアクセスするかは、実行時に決定されます。ここ