私はC++を試していましたが、以下のコードは非常に奇妙であることがわかりました。
class Foo{
public:
virtual void say_virtual_hi(){
std::cout << "Virtual Hi";
}
void say_hi()
{
std::cout << "Hi";
}
};
int main(int argc, char** argv)
{
Foo* foo = 0;
foo->say_hi(); // works well
foo->say_virtual_hi(); // will crash the app
return 0;
}
仮想メソッド呼び出しは、vtableルックアップが必要であり、有効なオブジェクトでのみ機能するため、クラッシュすることを知っています。
次の質問があります
say_hi
NULLポインタで動作しますか?foo
はどこに割り当てられますか?何かご意見は?
オブジェクトfoo
は、タイプFoo*
のローカル変数です。その変数は、他のローカル変数と同じように、スタックにmain
関数に割り当てられる可能性があります。ただし、foo
に格納されているvalueはnullポインタです。それはどこも指さない。タイプFoo
のインスタンスはどこにも表されていません。
仮想関数を呼び出すには、呼び出し元は関数が呼び出されているオブジェクトを知る必要があります。これは、オブジェクト自体が、実際に呼び出す必要のある関数を指示するものだからです。 (これは、オブジェクトにvtableへのポインター、関数ポインターのリストを与えることによって頻繁に実装され、呼び出し元は、そのポインターがどこを指しているかを事前に知らなくても、リストの最初の関数を呼び出すことになっていることを知っています。)
ただし、非仮想関数を呼び出すために、呼び出し元はそのすべてを知る必要はありません。コンパイラは、呼び出される関数を正確に認識しているため、CALL
マシンコード命令を生成して、目的の関数に直接移動できます。関数が呼び出されたオブジェクトへのポインタを、関数の非表示パラメータとして渡すだけです。言い換えると、コンパイラは関数呼び出しを次のように変換します。
void Foo_say_hi(Foo* this);
Foo_say_hi(foo);
これで、その関数の実装は、そのthis
引数が指すオブジェクトのメンバーを参照しないため、nullポインターを逆参照することはないため、nullポインターを逆参照するという弾丸を効果的に回避できます。
正式には、nullポインタでany関数(非仮想関数であっても)を呼び出すことは未定義の動作です。未定義の動作の許容される結果の1つは、コードが意図したとおりに実行されているように見えることです。 Yoはそれに依存すべきではありませんが、コンパイラベンダーのライブラリがdoに依存している場合があります。ただし、コンパイラベンダーには、定義されていない動作にさらに定義を追加できるという利点があります。自分でやらないでください。
say_hi()
メンバー関数は通常、コンパイラによって次のように実装されます。
void say_hi(Foo *this);
メンバーにアクセスしないため、呼び出しは成功します(標準に従って未定義の動作を入力している場合でも)。
Foo
はまったく割り当てられません。
NULLポインターを逆参照すると、「未定義の動作」が発生します。これは、何かが発生する可能性があることを意味します。コードが正しく機能しているように見える場合もあります。ただし、これに依存してはなりません。同じコードを別のプラットフォームで(または同じプラットフォームで)実行すると、クラッシュする可能性があります。
コードにはFooオブジェクトはなく、値NULLで初期化されたポインターのみがあります。
それは未定義の振る舞いです。しかし、ほとんどのコンパイラーは、メンバー変数と仮想テーブルにアクセスしない場合にこの状況を正しく処理する命令を作成しました。
何が起こるかを理解するために、ビジュアルスタジオで分解を見てみましょう
Foo* foo = 0;
004114BE mov dword ptr [foo],0
foo->say_hi(); // works well
004114C5 mov ecx,dword ptr [foo]
004114C8 call Foo::say_hi (411091h)
foo->say_virtual_hi(); // will crash the app
004114CD mov eax,dword ptr [foo]
004114D0 mov edx,dword ptr [eax]
004114D2 mov esi,esp
004114D4 mov ecx,dword ptr [foo]
004114D7 mov eax,dword ptr [edx]
004114D9 call eax
ご覧のとおり、Foo:say_hiは通常の関数として呼び出されますが、ecxレジスタにthisがあります。簡単にするために、thisは、例では決して使用しない暗黙のパラメーターとして渡されたと想定できます。
しかし、2番目のケースでは、仮想テーブルによる関数のアドレスを計算します-fooアドレスが原因で、コアを取得します。
both呼び出しは未定義の動作を生成し、その動作は予期しない形で現れる可能性があることを理解することが重要です。 appearsの呼び出しが機能したとしても、地雷原を敷設している可能性があります。
あなたの例へのこの小さな変更を考えてみましょう:
_Foo* foo = 0;
foo->say_hi(); // appears to work
if (foo != 0)
foo->say_virtual_hi(); // why does it still crash?
_
foo
を最初に呼び出すと、foo
がnullの場合に未定義の動作が有効になるため、コンパイラはfoo
がnot nullであると自由に想定できるようになりました。これにより、if (foo != 0)
が冗長になり、コンパイラーはそれを最適化できます。これは非常に無意味な最適化だと思われるかもしれませんが、コンパイラの作成者は非常に積極的になっており、実際のコードではこのようなことが起こっています。
a)暗黙の「this」ポインタを介して何も逆参照しないため、機能します。あなたがそれをするやいなや、ブーム。 100%確信はありませんが、nullポインタの逆参照はRWがメモリスペースの最初の1Kを保護することによって行われると思います。したがって、1K行を超えて逆参照するだけではnull参照がキャッチされない可能性がわずかにあります(つまり、インスタンス変数これは、次のように非常に遠くに割り当てられます。
class A {
char foo[2048];
int i;
}
その場合、Aがnullの場合、a-> iはおそらく捕捉されません。
b)どこにも、main():sスタックに割り当てられたポインターのみを宣言しました。
Say_hiの呼び出しは静的にバインドされています。したがって、コンピュータは実際には関数への標準的な呼び出しを行うだけです。この関数はフィールドを使用しないため、問題はありません。
Virtual_say_hiの呼び出しは動的にバインドされるため、プロセッサは仮想テーブルに移動します。仮想テーブルがないため、ランダムな場所にジャンプしてプログラムをクラッシュさせます。
C++の元の時代には、C++コードはCに変換されていました。オブジェクトメソッドは、次のような非オブジェクトメソッドに変換されます(あなたの場合)。
foo_say_hi(Foo* thisPtr, /* other args */)
{
}
もちろん、foo_say_hiという名前は単純化されています。詳細については、C++の名前マングリングを調べてください。
ご覧のとおり、thisPtrが逆参照されない場合、コードは正常で成功します。あなたの場合、インスタンス変数やthisPtrに依存するものは使用されていません。
ただし、仮想機能は異なります。正しいオブジェクトポインタがパラメータとして関数に渡されることを確認するために、多くのオブジェクトルックアップがあります。これにより、thisPtrが逆参照され、例外が発生します。