動的なオブジェクトの生成 | Programming Place Plus C++編【言語解説】 第14章

C++編【言語解説】 第14章 動的なオブジェクトの生成

先頭へ戻る

この章と同じ(または似た)情報を扱うページが、Modern C++編 (C++11/14/17 対応) の以下の章にあります。

この章の概要

この章の概要です。


関連する話題が、以下のページにあります。

new と delete

オブジェクトを、動的に確保したメモリ領域に作成したい場合に、malloc関数などのC言語から引き継いだ関数を使用して、以下のように記述することはできません。

MyClass* obj = static_cast<MyClass*>(malloc(sizeof(MyClass)));

このように書いた場合、これはオブジェクトを置くためのメモリ領域を確保しただけであって、実際にオブジェクトは作成されていません。

そこで代わりに、new演算子を使用します。C++ ではオブジェクトだけでなく、組み込み型などの動的なメモリ確保でも、new演算子を使うようにして構いません。なお、解放時には、delete演算子を使用します。

#include <iostream>

class MyClass {
public:
    MyClass(const char* s) : mStr(s)
    {
        std::cout << "MyClass(" << mStr << ")" << std::endl;
    }

    ~MyClass()
    {
        std::cout << "~MyClass(" << mStr << ")" << std::endl;
    }

private:
    const char* mStr;
};


MyClass* func()
{
    return new MyClass("func");
}

int main()
{
    MyClass* c1 = new MyClass("main");
    MyClass* c2 = func();

    delete c1;
    delete c2;
}

実行結果:

MyClass(main)
MyClass(func)
~MyClass(main)
~MyClass(func)

new演算子は「new 型名(コンストラクタに渡す引数)」という形で使用します。もし、コンストラクタに引数が無いのなら「new 型名()」と書きます。結果、作られたオブジェクトへのポインタが返されます。

delete演算子は、「delete メモリアドレス」という形で使用します。指定するメモリアドレスは、new演算子で確保された領域のメモリアドレスか、ヌルポインタでなければなりません。ヌルポインタを指定した場合は、何も起こらないことが保証されています

なお、new演算子はコンストラクタを呼び出しますし、delete演算子はデストラクタを呼び出しますから、それぞれにアクセスできるようなアクセス指定が行われていなければ、new演算子、delete演算子を使うこともできなくなります(コンパイルエラーになります)。

new[] と delete[]

オブジェクトの配列を必要としている場合は、これまでに挙げたような new/delete演算子の使い方ではいけません。配列であることを示すために、「new 型名[要素数]」および「delete [] メモリアドレス」という形式で使用するようにします。特に、delete演算子の方に [] が必要であることに注意して下さい。これを忘れても、コンパイルは出来てしまいますが、うまく解放できません。

#include <iostream>

class MyClass {
public:
    MyClass(const char* s) : mStr(s)
    {
        std::cout << "MyClass(" << mStr << ")" << std::endl;
    }

    ~MyClass()
    {
        std::cout << "~MyClass(" << mStr << ")" << std::endl;
    }

private:
    const char* mStr;
};


int main()
{
    MyClass* c = new MyClass[5];  // デフォルトコンストラクタが無いので、コンパイルエラー

    delete [] c;
}

これはコンパイルエラーになります。配列版の new演算子を使うと、コンストラクタに引数が渡せないため、デフォルトコンストラクタが必要になります。MyClassクラスには、デフォルトコンストラクタが無いので、エラーになってしまいます。試しに、デフォルトコンストラクタを追加してみると、次のようにコンパイルも通り、実行できるようになります。

#include <iostream>

class MyClass {
public:
    MyClass() : mStr("")
    {
    }

    MyClass(const char* s) : mStr(s)
    {
        std::cout << "MyClass(" << mStr << ")" << std::endl;
    }

    ~MyClass()
    {
        std::cout << "~MyClass(" << mStr << ")" << std::endl;
    }

private:
    const char* mStr;
};


int main()
{
    MyClass* c = new MyClass[5];  // デフォルトコンストラクタが無いので、コンパイルエラー

    delete [] c;
}

実行結果:

~MyClass()
~MyClass()
~MyClass()
~MyClass()
~MyClass()

new演算子がしていること

ここで、new演算子がしていることの詳細を見ておきます。少し難しい部分も含まれますが、できるだけ正確に知っておくと、C++ の理解につながります。なお、以下の説明は、new でも new [] でも同様であると考えて構いません。

まず、ヒープ領域にメモリを確保しようとします。このとき、確保される大きさは、「sizeof(指定した型)」が返す大きさになります。「new double(5.0)」のように書いたとすれば、「sizeof(double)」の大きさで確保されるという訳です。

メモリ領域を確保するために、operator new または operator new[] という関数を呼び出しています。これらの関数をプログラマが定義することもできますが、そうしなかった場合は標準の実装が使用されます。標準の実装では、malloc関数を呼び出したときと同じ動作になります。この辺りの詳細は、第36章で解説します。

メモリ確保に失敗してしまった場合には、newハンドラという関数が呼び出されます。newハンドラの正体は単なる関数で、プログラマが自分で用意した関数を登録しておけば、メモリ確保に失敗したときに、自動的に呼び出してくれるようになっています。

newハンドラを登録するには、set_new_handler関数を使用します。set_new_handler関数を使うには、<new> という妙な名前の標準ヘッダをインクルードする必要があります。

#include <iostream>
#include <new>
#include <climits>

void my_new_handler()
{
    std::cout << "my_new_handler" << std::endl;
    std::abort();
}

int main()
{
    std::set_new_handler(my_new_handler);

    char* p = new char[INT_MAX];
    delete[] p;
}

実行結果:

my_new_handler

このサンプルプログラムの場合、my_new_handler関数を newハンドラとして登録しています。my_new_handler関数内では、abort関数を呼び出しているので、メモリ確保に失敗した場合は、プログラムが異常終了することになります。

newハンドラを登録していなかった場合は、例外が送出されます。例外については、第32章で説明しますが、特に気にしなければ、結果的にプログラムが終了するという結果になります。

このような機構が存在しているのは、メモリ確保に失敗した場合、メモリ領域から重要でないデータを急きょ解放することで、領域を空けてやることができれば、メモリ確保を成功に導くことが可能であるかも知れないからです。実際、newハンドラとして登録した関数が、普通に関数の末尾まで実行されたり、return文で戻ってきたりした場合、再度、メモリ確保を試みることになっています。その結果、メモリ確保に成功すれば、何事もなかったかのように、new演算子の呼び出し元に適切なポインタが返却されてプログラムは続行できます。もし、再試行にも失敗したら、再び newハンドラが呼び出され、以下同じことを無限に繰り返します。

少し分かり難いので、疑似的なコードで示すと次のようになります。

void* new関数 (std::size_t size)
{
    for (;;) {
        void* p = std::malloc(size);
        if (p != NULL) {
            return p;
        }
        if (newハンドラが登録されていない?) {
            例外を送出 (new関数から抜け出す)
        }
        newハンドラを呼ぶ
    }
}

先ほどのサンプルプログラムで、my_new_handler関数の中にある abort関数の呼び出しをコメントアウトして試してみれば、標準出力に "my_new_handler" が繰り返し出力されるので、my_new_handler関数が無限に呼び出されている様子が分かります。

このように、new演算子は、newハンドラの実装次第で、メモリ確保失敗時に何が起きるかは変わってくるため、エラーチェックのために、返された値がヌルポインタかどうかを調べるのは間違っています。実際、ヌルポインタが返されることはあり得ません。

new演算子を「new(std::nothrow) 型名(コンストラクタに渡す引数)」という形で使用した場合だけは、失敗時に、NULL (C++11 では nullptr) を返します。ただしこの記法は、古いプログラムの移植性を維持するために用意されているものであり(規格化前は失敗時に NULL を返す実装もありました)、基本的に使わない方が良く、標準の使い方で統一した方が良いでしょう。

話をメモリ確保に成功した場合の流れに戻します。メモリ確保に成功した場合は、続けて、オブジェクトの生成作業が行われます。これはつまり、コンストラクタを呼び出すということです。この過程が存在することがC言語と C++ との大きな違いであり、malloc関数を使うのではダメな理由です。malloc関数では、前述の「メモリ確保」の部分だけで終わってしまいます。

コンストラクタの中でエラーが起きてしまったらどうなるのか、という部分の理解も、C++ の深い理解のためには必要です。このようなことが起こる可能性は、例外が起きたときなのですが、その場合、先に確保したメモリ領域は解放されることになっています。ですから、このような状況でメモリリークが起こるということはありません。なお、この場合、コンストラクタの処理は正常に完結していないため、オブジェクトは生成できていないとみなされます。従って、破棄すべきものが無いので、デストラクタが呼び出されることもありません。

delete演算子がしていること

delete演算子がしていることは、new演算子の逆回しのような作業です。なお、以下の説明は、delete でも delete [] でも同様であると考えて構いません。

まず、デストラクタが呼び出されて、オブジェクトが破棄されます。ここで破棄されても、ヒープ領域に確保された領域は、確保されたままであることを理解しておいて下さい。領域は予約されているが、誰も使っていないという状態になります。

次に、メモリ領域が解放されます。これは free関数と同じことをしています。

この2つの過程をセットで行うのが、delete演算子の仕事になっています。free関数だけでダメなのは、デストラクタを呼び出す過程が飛ばされてしまうからです。

注意事項のまとめ

C言語編第35章にまとめている、malloc関数や free関数の注意事項は、ほぼそのまま、new演算子や delete演算子にも当てはまりますが、少し事情が違うところもあります。C++ の視点でまとめると次のようになります。

new演算子に関しては、以下の通りです。

delete演算子に関しては、以下の通りです。

まとめると非常に複雑な感じがしますが、malloc関数と free関数の注意事項がきちんと理解できていれば、配列版と非配列版の使い分けにさえ気を付ければ、後はほぼ同じです。そのため、SAFE_FREEマクロ(C言語編第35章)のような安全策は、C++ でも効果があります。配列版と非配列版を使い分けなければならないため、マクロも2種類必要になります。

#define SAFE_DELETE(ptr)       if (ptr != NULL) { delete ptr; ptr = NULL; }
#define SAFE_DELETE_ARRAY(ptr) if (ptr != NULL) { delete [] ptr; ptr = NULL; }

SAFE_DELETE、SAFE_DELETE_ARRAYマクロにはそれなりの価値がありますが、C++ には、スマートポインタと呼ばれる、更に良い機構があるので、これを使うのが良いです。第32章で解説します。


練習問題

問題① 次のプログラムの誤りを指摘して下さい。

#include <iostream>

int main()
{
    static const int ARRAY_SIZE = 10;

    int* array = new int[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; ++i) {
        array[i] = i;
    }

    for (int i = 0; i < ARRAY_SIZE; ++i) {
        std::cout << array[i] << std::endl;
    }

    delete array;
}

問題② デストラクタを利用すれば、delete演算子の呼び忘れを防げます。new演算子で確保された MyClass型のポインタを対象に、そのような呼び忘れを防ぐためのクラスを設計して下さい。


解答ページはこちら

参考リンク



更新履歴

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

'2014/5/10 新規作成。



前の章へ(第13章 コンストラクタとデストラクタ)

次の章へ(第15章 static (静的))

C++編のトップページへ

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS