読者です 読者をやめる 読者になる 読者になる

ちぎっては投げるブログ

Programming, Android, RaspberryPi, Digital Devices, Kinkuma Hamster...

なんでこれで動くの?

#include <iostream>
class MyClass{
public:
    MyClass()
    {
        //コンストラクタ
        std::cout << "alloc=" << this << std::endl;
    }

    ~MyClass(){
        //デストラクタ
        std::cout << "delete=" << this << std::endl;
    }
    void func(){
        std::cout << this << std::endl;
        return;
    }
};

int main(){
    MyClass* myClassPtr = new MyClass();
    myClassPtr->func();
    delete myClassPtr;
    myClassPtr->func();
    getchar();
    return 0;
}

実行例

alloc=00625E00
00625E00
delete=00625E00
00625E00

上記のプログラムは明らかにおかしな領域にアクセスしているが、動いてしまう。 なぜなら、free(delete)してもそのアドレスはNULLにはならず、そのアドレスの中には値が入ったままなので、中身が他のデータに上書きされるまで動いてしまう。これは危険なコードである。 こうした危険を防ぐために、free(delete)したらNULLを代入するべきという主張がある。 するとエラーになるし、うっかりdeleteによる二重開放も防げるという主張である。

というつもりで以下のコードに書き換えてエラーを起こそうとしたのだが、結果が思っていたのと違う。

int main(){
    MyClass* myClassPtr = new MyClass();
    myClassPtr->func();
    delete myClassPtr;
    myClassPtr = NULL; //追加
    myClassPtr->func();
    getchar();
    return 0;
}

実行例

alloc=0088B3D8
0088B3D8
delete=0088B3D8
00000000

00000000ってなんだ?myClassPtrがNULLの時点で、その先のfuncは存在しないからエラーを吐くと思ったのに・・・。 ちなみに、VisualStudio 2013のデバッグビルドで動かしている。最適化類はオフになっているはずだ。なんで出力される?

そもそもNULL埋めじゃなくて、確保した領域は多重開放しないように設計し、かつvalgrind等でバッファオーバランやリークがないかをチェックするべき、と書こうと思っていたんだけど、検証用に書いたらなぜか動いたので不思議な気持ち。NULL->funcなんてないはずだと思うんだけど・・・。

考えてみると、メンバ関数であるfuncは、内部的には引数にそのメンバ関数の持ち主であるインスタンスを渡すはずなので、通常であればfunc(myClassPtr)のような形で呼び出されているはずだ。(細かいところは詳しくないので、本当にこのような渡し方かは知らないが) つまり今回はfunc(NULL)のような形で動いたのではないか。

そうなると、私はこれまでメンバ関数の呼び出しは、インスタンス内にメンバ関数への参照があって、それを経由して呼ばれるというイメージを勝手に持っていたが、実際には、メンバ関数のアドレスを直接呼んでいるのかもしれない。

今まで思っていた呼び出し方

1.インスタンスのアドレスにアクセス
2.インスタンス用に確保された領域の中に、メンバ関数へのポインタがある
3.ポインタの指す関数に自身のインスタンスのアドレスを渡して実行する

今予想している呼び出し方

1.メンバ関数に自身のインスタンスのアドレスを渡して実行する
*どうやってメンバ関数のアドレスを求めている?型情報から?

こんな動作をしているように見えるが、これを検証するにアセンブリを読まなきゃだめだろうか?