web-dev-qa-db-ja.com

NULLポインターでクラスメンバーにアクセスする

私は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ルックアップが必要であり、有効なオブジェクトでのみ機能するため、クラッシュすることを知っています。

次の質問があります

  1. 非仮想メソッドはどのようにsay_hi NULLポインタで動作しますか?
  2. オブジェクトfooはどこに割り当てられますか?

何かご意見は?

46
Navaneeth K N

オブジェクト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に依存している場合があります。ただし、コンパイラベンダーには、定義されていない動作にさらに定義を追加できるという利点があります。自分でやらないでください。

83
Rob Kennedy

say_hi()メンバー関数は通常、コンパイラによって次のように実装されます。

void say_hi(Foo *this);

メンバーにアクセスしないため、呼び出しは成功します(標準に従って未定義の動作を入力している場合でも)。

Fooはまったく割り当てられません。

17
Pontus Gagge

NULLポインターを逆参照すると、「未定義の動作」が発生します。これは、何かが発生する可能性があることを意味します。コードが正しく機能しているように見える場合もあります。ただし、これに依存してはなりません。同じコードを別のプラットフォームで(または同じプラットフォームで)実行すると、クラッシュする可能性があります。

コードにはFooオブジェクトはなく、値NULLで初期化されたポインターのみがあります。

7
anon

それは未定義の振る舞いです。しかし、ほとんどのコンパイラーは、メンバー変数と仮想テーブルにアクセスしない場合にこの状況を正しく処理する命令を作成しました。

何が起こるかを理解するために、ビジュアルスタジオで分解を見てみましょう

   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アドレスが原因で、コアを取得します。

5
bayda

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の場合に未定義の動作が有効になるため、コンパイラはfoonot nullであると自由に想定できるようになりました。これにより、if (foo != 0)が冗長になり、コンパイラーはそれを最適化できます。これは非常に無意味な最適化だと思われるかもしれませんが、コンパイラの作成者は非常に積極的になっており、実際のコードではこのようなことが起こっています。

2
Mark Ransom

a)暗黙の「this」ポインタを介して何も逆参照しないため、機能します。あなたがそれをするやいなや、ブーム。 100%確信はありませんが、nullポインタの逆参照はRWがメモリスペースの最初の1Kを保護することによって行われると思います。したがって、1K行を超えて逆参照するだけではnull参照がキャッチされない可能性がわずかにあります(つまり、インスタンス変数これは、次のように非常に遠くに割り当てられます。

 class A {
     char foo[2048];
     int i;
 }

その場合、Aがnullの場合、a-> iはおそらく捕捉されません。

b)どこにも、main():sスタックに割り当てられたポインターのみを宣言しました。

2
Pasi Savolainen

Say_hiの呼び出しは静的にバインドされています。したがって、コンピュータは実際には関数への標準的な呼び出しを行うだけです。この関数はフィールドを使用しないため、問題はありません。

Virtual_say_hiの呼び出しは動的にバインドされるため、プロセッサは仮想テーブルに移動します。仮想テーブルがないため、ランダムな場所にジャンプしてプログラムをクラッシュさせます。

2
Uri

C++の元の時代には、C++コードはCに変換されていました。オブジェクトメソッドは、次のような非オブジェクトメソッドに変換されます(あなたの場合)。

foo_say_hi(Foo* thisPtr, /* other args */) 
{
}

もちろん、foo_say_hiという名前は単純化されています。詳細については、C++の名前マングリングを調べてください。

ご覧のとおり、thisPtrが逆参照されない場合、コードは正常で成功します。あなたの場合、インスタンス変数やthisPtrに依存するものは使用されていません。

ただし、仮想機能は異なります。正しいオブジェクトポインタがパラメータとして関数に渡されることを確認するために、多くのオブジェクトルックアップがあります。これにより、thisPtrが逆参照され、例外が発生します。

1
Tommy Hui