C++におけるオブジェクトの生存期間とか未定義の動作とか

前回の記事を書いたあと、「この書き方でもイケるのでは」と思って動かしたら全然予想と違う結果になったので、プログラムの動きを追ってみました。


#include 
#include 
#include 

#include "vehicle.h"
#include "Truck.h"
#include "Car.h"

using namespace std;

int main()
{
    int vhc_num;
    string brd;
    string clr;
    int st;
    string lug;
    int vhc_type;

    cout << "How many vehicles do you have?: ";
    cin >> vhc_num;

    Vehicle* vhc_array[vhc_num];

    /* store vehicles into array */
    for (int i = 0; i < vhc_num; i++) {
        cout << "Which brand?: ";
        cin >> brd;
        cout << "What color?: ";
        cin >> clr;
        cout << "Is it car or truck? (Type 1 for car, type 2 for truck): ";
        cin >> vhc_type;

        if (vhc_type == 1) {
            cout << "How many seats?: ";
            cin >> st;
            Car car(brd, clr, st);
            cout << "Object type: " << typeid(car).name() << endl;
            cout << "Address of car: " << &car << endl;
            vhc_array[i] = &car;
         }
        else if (vhc_type == 2) {
            cout << "What luggage do you have?: ";
            cin >> lug;
            Truck trk(brd, clr, lug);
            cout << "Object type: " << typeid(trk).name() << endl;
            cout << "Address of trk: " << &trk << endl;
            vhc_array[i] = &trk;
        }

    }
    /* print vehicle information */
    for (int i = 0; i < vhc_num; i++) {
        vhc_array[i]->print();
        cout << "\n";
    }

    return 0;

}


Vehicle* vhc_array[vhc_num];

前回と違ってnewを使わずにVehicle型のポインタ配列を作成。プログラムを実行すると。。

3.png
Car型のオブジェクトを2つ、Truck型のオブジェクトを2つで計4つのVehicleオブジェクトをVehicle型のポインタ配列に格納しようとしたのですが、上記の出力のとおり、Car、Truckともにオブジェクトがダブって格納されています。

何故こんなことが起きたのか?デバッグしてみると。。。
4.png
car1のオブジェクトがアドレス 0x7ffee269d858に作成されています。で、car2のオブジェクトも全く同じアドレス 0x7ffee269d858に作成されていました。同様にTruck型オブジェクトも、まずtruck1がアドレス 0x7ffee269d7d0に作成されたあと、全く同じアドレスにtruck2が作成されました。


Car car(brd, clr, st);
Truck trk(brd, clr, lug);

プログラム中では、Car型オブジェクトは"car"の名前で、Truck型オブジェクトは"trk"の名前で毎回インスタンスを作成しているのですが、それが良くなかった模様。どうやら、例えば、一度 "hoge" という名前でメモリ上にインスタンスが作成されたあとに、全く同じ"hoge"という名前でインスタンスを作ろうとすると、新たにメモリが確保されるのではなく、すでにある"hoge"のメモリ上で内容が上書きされるようなのです。
そして、ポインタ配列には常に同じアドレス(今回の場合、carなら0x7ffee269d858、trkなら0x7ffee269d7d0) が格納されるので、出力の内容がダブってしまうと。。。

オブジェクトがダブって格納された理由は分かりました。でもCar固有メンバのseatsやTruck固有メンバのluggageが表示されないのは何故?
"Object type"のところを見てみると、"3Car" および "5Truck" となっているので、CarとTruckのオブジェクトはちゃんと作成されているように見えます。

色々調べてみると、どうやらオブジェクトの生存期間とデストラクタが関係していそうです。少しコードをいじって、デストラクタがどのタイミングで呼び出されているか確認してみました。
5.png
上記の出力を見ると、オブジェクトの作成後、ただちにデストラクタが呼ばれているのがわかります。デストラクタとはオブジェクトがメモリから破棄されるときに自動的に呼び出される特殊なメンバ関数です。

オブジェクトの生存期間には以下の3種類があります。(柴田望洋著 新・明解C++入門より抜粋)

静的記憶域期間
関数の外あるいはstaticをつけて関数の中で宣言されたオブジェクトは、プログラム実行時、具体的にはmain関数を実行する前の準備段階で生成され、プログラムの終了時に破棄される。

自動記憶域期間
関数の中でstaticをつけずに宣言されたオブジェクトは、プログラムの流れが宣言を通過する際に生成される。宣言を囲むブロック終点 "}" を通過するときに、そのオブジェクトは役目を終えて破棄される。

動的記憶域期間
プログラム上から自由に制御できるオブジェクトの生存期間。new演算子で生成し、delete演算子で破棄する。

今回のcarおよびtrkの生存期間は自動記憶域期間にあたります。


if (vhc_type == 1) {
    cout << "How many seats?: ";
    cin >> st;
    Car car(brd, clr, st);
    cout << "Object type: " << typeid(car).name() << endl;
    cout << "Address of car: " << &car << endl;
    vhc_array[i] = &car;
}
else if (vhc_type == 2) {
    cout << "What luggage do you have?: ";
    cin >> lug;
    Truck trk(brd, clr, lug);
    cout << "Object type: " << typeid(trk).name() << endl;
    cout << "Address of trk: " << &trk << endl;
    vhc_array[i] = &trk;
}

上記の if else文の中でcarおよびtrkオブジェクトが生成されて破棄されるので、あとからポインタ経由でアクセスしようとしても、そこには何も無いと。出力の結果からもオブジェクトが生成されて程なくしてCarのデストラクタ(またはTruckのデストラクタ)が呼び出された後にVehicleのデストラクタが呼び出されて、オブジェクトが破棄されているのがわかります。
なるほど、なるほど。

ん。。?でも、ちょっと待って。
6.png
破棄されたはずのcar2とtruck2の一部のデータにポインタ経由でアクセスできているように見えるんですけど。。。

調べてみると、これは「未定義の動作」に関わるものらしいです。未定義の動作とは規格上定義されていない異常なプログラムの動作のこと。
下記のページに代表的な未定義の動作が列挙されているのですが。。
http://www.c-lang.org/detail/undefined_behavior.html

その中に「生存期間が終了したオブジェクトを指すポインタの値が使われる場合。」というものがあり、今回は正にそれにあたります。


for (int i = 0; i < vhc_num; i++) {
    vhc_array[i]->print();
    cout << "\n";
}

プログラムの最後のほうの上記のfor文で破棄されたオブジェクトにポインタ経由でアクセスしようとしていますが、これはプログラム的には正しい動作ではありません。ただし、「動作」が「未定義」なのでコンパイル時にエラーになることもあれば、今回のようにプログラムの実行自体は問題なく行える場合もあります。(見た目上、問題なく動いているように見えてもプログラム的に間違った動作をしていることには変わりないので、このようなコーディングは避けるべきです。)

ん〜、奥が深い。

以上

Leave a Reply

Your email address will not be published. Required fields are marked *