これは実際には面接の質問です、私は答えを理解することができません。誰もがこれについて知っていますか?スタックにプッシュされるデータなど、違いについて話すことができます。
5 に答える
仮想化/動的ディスパッチは厳密に実装定義されていますが、ほとんどの(すべての既知の)コンパイラはとを使用して実装しvptr
ますvtable
。
そうは言っても、非仮想関数と仮想関数の呼び出しの違いは次のとおりです。
非仮想関数はで解決さstatically
れますCompile-time
が、仮想関数はで解決さdynamically
れRun-time
ます。
実行時に呼び出す関数を決定できるというこの柔軟性を実現するために、仮想関数の場合は少しオーバーヘッドがあります。
fetch
実行する必要のある追加の呼び出しであり、動的ディスパッチを使用して支払うオーバーヘッド/価格です。
非仮想関数の場合、呼び出しのシーケンスは次のとおりです。
fetch-call
コンパイラfetch
は、関数のアドレスを指定してから、それをアドレス指定する必要がありますcall
。
仮想関数の場合、シーケンスは次のとおりです。
fetch-fetch-call
コンパイラはfetch
、vptr
からthis
、次にfetch
関数のアドレス、vptr
次に関数を実行する必要がcall
あります。
これは単純な説明であり、実際のシーケンスはこれよりもはるかに複雑かもしれませんが、これはあなたが本当に知る必要があることです。実装の本質を知る必要はありません。
良い読み物:
基本クラス「Base」と派生クラス「Derived」があり、関数「func()」が基本クラスで仮想として定義されている場合。この関数は、Derivedクラスによってオーバーライドされます。
あなたが定義するとします
Base obj = new Derived();
obj.func();
次に、Derivedクラスの「func」が呼び出されます。'func()'がBaseで仮想として定義されていない場合、'Base'クラスから呼び出されます。これは、仮想関数と非仮想関数の関数呼び出しの違いです。
非仮想メンバー関数は静的に解決されます。メンバー関数は、オブジェクトへのポインター(または参照)のタイプに基づいて、コンパイル時に静的にバインドされます。
対照的に、仮想メンバー関数は実行時に動的にバインドされます。クラスに少なくとも1つの仮想メンバー関数がある場合、コンパイラーは、オブジェクトの構築中にvptr(仮想テーブルアドレス)と呼ばれるオブジェクトに非表示のポインターを置きます。
コンパイラーは、少なくとも1つの仮想関数を持つクラスごとにvテーブルを作成します。仮想テーブルには、仮想関数のアドレスが含まれています。これは、仮想関数ポインタの配列またはリスト(コンパイラによって異なります)にすることができます。仮想関数
のディスパッチ中、ランタイムシステムはオブジェクトのvポインタ(クラスオブジェクトからアドレスをフェッチ)に従ってクラスのvテーブルに到達します。次に、オフセットがベースアドレス(vptr)に追加され、関数が呼び出されます。
上記の手法のスペースコストのオーバーヘッドはわずかです。オブジェクトごとに追加のポインター(ただし、動的バインディングを実行する必要があるオブジェクトの場合のみ)と、メソッドごとに追加のポインター(ただし、仮想メソッドの場合のみ)です。時間コストのオーバーヘッドもかなりわずかです。通常の関数呼び出しと比較して、仮想関数呼び出しには2つの追加フェッチが必要です(1つはvポインターの値を取得し、もう1つはメソッドのアドレスを取得します)。
コンパイラはポインタのタイプに基づいてコンパイル時に排他的に非仮想関数を解決するため、このランタイムアクティビティは非仮想関数では発生しません。
非仮想関数と仮想関数のバインディングがどのように発生したか、仮想関数メカニズムがどのように機能するかをよりよく理解するために、簡単な例を取り上げました。
#include<iostream>
using namespace std;
class Base
{
public:
virtual void fun()
{}
virtual void fun1()
{}
void get()
{
cout<<"Base::get"<<endl;
}
void get1()
{
cout<<"Base::get1"<<endl;
}
};
class Derived :public Base
{
public:
void fun()
{
}
virtual void fun3(){}
void get()
{
cout<<"Derived::get"<<endl;
}
void get1()
{
cout<<"Derived::get1"<<endl;
}
};
int main()
{
Base *obj = new Derived();
obj->fun();
obj->get();
}
基本クラスと派生クラス用にvtableがどのように作成されたか
理解を深めるために、アセンブリコードが生成されます。
$ g++ virtual.cpp -S -o virtual.s
BaseクラスとDerivedクラスのvirtual.sからそれぞれvtableの情報を取得しました。
_ZTV4Base:
.quad _ZN4Base3funEv
.quad _ZN4Base4fun1Ev
_ZTV7Derived:
.quad _ZN7Derived3funEv
.quad _ZN4Base4fun1Ev
.quad _ZN7Derived4fun3Ev
ご覧のとおり、funとfun1は、Baseクラスの2つの仮想関数にすぎません。基本クラス(_ZTV4Base)のVtableには、両方の仮想関数のエントリがあります。Vtableには非仮想関数のエントリがありません。fun(ZN4Base3funEv)&fun1(ZN4Base4fun1Ev)の名前と混同しないでください。名前が壊れています。
派生クラスのvtableにはツリーエントリがあります
- fun(_ZN7Derived3funEv)オーバーライド関数
- 基本クラスから継承されたfun1(_ZN4Base4fun1Ev)
- fun3(_ZN7Derived4fun3Ev)派生クラスの新しい関数
非仮想関数と仮想関数はどのように呼び出されますか?
非仮想機能の場合
Derived d1;
d1.get();
subq $16, %rsp
leaq -16(%rbp), %rax
movq %rax, %rdi
call _ZN7DerivedC1Ev //call constructor
leaq -16(%rbp), %rax
movq %rax, %rdi
call _ZN7Derived3getEv //call get function
単に伝え、フェッチし、getを呼び出します(バインドはコンパイル時に発生しました)
非仮想機能の場合
Base *obj = new Derived();
obj->fun();
pushq %rbx
subq $24, %rsp
movl $8, %edi
call _Znwm //call new to allocate memory
movq %rax, %rbx
movq $0, (%rbx)
movq %rbx, %rdi
call _ZN7DerivedC1Ev //call constructor
movq %rbx, -24(%rbp)
movq -24(%rbp), %rax
movq (%rax), %rax
movq (%rax), %rax
movq -24(%rbp), %rdx
movq %rdx, %rdi
call *%rax //call fun
vptrをフェッチし、関数オフセットを追加し、関数を呼び出します(バインディングは実行時に発生しました)
64のアセンブリは、C ++プログラマーのほとんどを混乱させますが、議論したい場合は、歓迎します
仮想メソッドを呼び出すときは、仮想関数テーブルで呼び出す関数を検索する必要があります。
仮想メソッドを呼び出すオーバーヘッドは重要です。