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

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

new演算子と delete演算子

オブジェクトを動的に生成するには、C言語のように malloc() を使うのではなく、new演算子を使用します。後の項で詳細を説明しますが、「オブジェクトを動的に生成する」という文章には、「メモリ領域を動的に確保する」ことと「オブジェクトを作る」ことの2つが含まれていることがポイントになります。
malloc() が無くなった訳ではありませんが、この関数はメモリ領域を確保することだけが仕事であり、オブジェクトを生成することまでは行いませんから、目的に合っていません。

new演算子は次のように使用します。

MyClass* a = new MyClass;
MyClass* b = new MyClass();

newキーワードに続けて、生成する型の名前と、コンストラクタに渡す引数のリストを指定します。デフォルトコンストラクタがある場合は () を省略することができます。
当然、選択されるコンストラクタが「非公開」であったり、削除されていたりすると、コンパイルエラーになります。new演算子の呼び出しの結果、生成されたオブジェクトを指すポインタが得られます。

new演算子は、クラス以外の型に対して使っても構いません。例えば、次のように使用できます。

int* a = new int(100);
int* b = new int();  // 未初期化
int* c = new int;    // 未初期化

実は int型のような組み込み型でも、コンストラクタを呼び出すような表記が可能なので、上記のようなコードは有効です。組み込み型の場合に、() の内側を空にしたり、() 自体を省略したりすると、未初期化な状態で生成されることになるので注意して下さい。原則として、未初期化な状態は避けるべきなので、このような使い方はしない方が良いです。

new演算子は失敗する可能性があります。malloc() のように、メモリ不足による失敗は当然のことながら、コンストラクタ内の処理が原因で失敗する可能性も考えられます。いずれにしても、new演算子の失敗は戻り値ではなく、例外(第18章)という仕組みを使って通知されます。new演算子の通常の使い方をしている限り、ヌルポインタが返されることはあり得ません。

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


new演算子によって生成されたオブジェクトを解放するには、delete演算子を使用します。new演算子の仕事に「メモリ領域を動的に確保する」ことまで含まれているように、delete演算子の仕事には「確保されたメモリ領域を解放する」ことも含まれています。

MyClass* a = new MyClass();
delete a;

deleteキーワードに続けて、new演算子で生成されたオブジェクトを指すポインタを指定します。free() と同様で、ヌルポインタを指定した場合には何も起こらないことが保証されていますが、二重解放や、new演算子で生成されたオブジェクト以外を指すポインタを渡したときの動作は未定義です。


使用例を挙げます。

#include <iostream>

class MyClass {
public:
    MyClass()
    {
        std::cout << "Constructor" << std::endl;
    }
    ~MyClass()
    {
        std::cout << "Destructor" << std::endl;
    }
};

MyClass* Create()
{
    return new MyClass();
}

int main()
{
    MyClass* obj1 = new MyClass();
    MyClass* obj2 = Create();

    delete obj1;
    delete obj2;
}

実行結果:

Constructor
Constructor
Destructor
Destructor

new演算子がしていること

ここで、new演算子がしていることの詳細を見ておきます。少し難しい部分も含まれますが、できるだけ正確に知っておくと、C++ の理解につながります。

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

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

メモリ確保に失敗してしまった場合には、newハンドラという関数が呼び出されます。newハンドラの正体は普通の関数ですが、プログラマが自分で用意した関数をあらかじめ登録しておくことができます。newハンドラを登録するには、std::set_new_handler() を使用します。std::set_new_handler() を使うには、「new」という名前の標準ヘッダをインクルードする必要があります。

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

struct BigData {
    char c[INT_MAX];
};

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

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

    BigData* data = new BigData();
    delete data;
}

実行結果:

call my_new_handler

このサンプルプログラムは、巨大なオブジェクトを動的に生成しようとして、メモリ確保を失敗させています。あらかじめ、std::set_new_handler() を呼び出して、my_new_handler() を newハンドラとして登録していますから、メモリ確保に失敗すると、このハンドラ関数が呼び出されます。my_new_handler() は、メッセージを出力した後、std::abort() を呼び出してプログラムを異常終了させます。

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

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

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

先ほどのサンプルプログラムで、my_new_handler() の中にある std::abort() の呼び出しをコメントアウトすると、標準出力に "call my_new_handler" が繰り返し出力されるようになり、my_new_handler() が無限に呼び出されている様子が分かります。

このように、new演算子は、newハンドラの実装次第で、メモリ確保が失敗した時に何が起きるかが変わってくるため、失敗してもヌルポインタが返されるようにはなっていません。

既に前述していますが、new演算子を「new(std::nothrow) 型名(コンストラクタに渡す引数)」という形で使用した場合には、失敗したときに nullptr を返します。この記法は、古いプログラムとの互換性維持のためにあるものなので、新しく書くプログラムでは使うべきではありません。

話をメモリ確保に成功した場合の流れに戻します。
メモリ確保に成功した場合は、続けて、オブジェクトの生成作業が行われます。これはつまり、コンストラクタを呼び出すということです。この過程が存在することが、C言語と C++ との大きな違いであり、malloc() では対応できない理由になっています。malloc() は、前述の「メモリ確保」の部分しか行いません。
コンストラクタが正常に実行されたら、オブジェクトの生成作業は成功となり、そのオブジェクトを指すポインタが返されます。この段階をもって、new演算子のすべての仕事が完了したことになります。

ところで、コンストラクタの中でもエラーが起りうることを忘れてはなりません。これはつまり、コンストラクタの実装コードの中で例外が発生して、コンストラクタの中だけで解決できなかった場合です。この場合、コンストラクタは最後まで実行できていないので、オブジェクトは生成できていないとみなされます。
重要な点として、オブジェクトが生成できていないのだから、デストラクタも呼び出されることが無いということを理解しておかねばなりません。コンストラクタが中途半端に行ってしまった処理がそのままにならないように注意を払う必要があります。

理解には例外の知識が必要なので、ここは飛ばして構いませんが、例えば以下のコードには問題があります。

MyClass::MyClass()
{
    mValue1 = new int(10);
    mValue2 = new int(20);
}

MyClass::~MyClass()
{
    delete mValue2;
    delete mValue1;
}

2つ目の new演算子のところでエラーが起きてしまうと、コンストラクタは中途半端に実行された状態になります。そのため、デストラクタが呼ばれる機会を失ってしまい、既に正常に確保されていた mValue1 を delete する機会も失われてしまいます。
この問題を解決するためには、後で解説する RAII の考え方を導入すると良いです。

delete演算子がしていること

delete演算子がしていることは、new演算子の逆回しのような作業です。

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

デストラクタがエラーを起こした場合は、最早どうしようもなく、どうなるかも分かりません。一般論として、デストラクタのような終了に関する処理は、絶対にエラーを発生させないように実装するべきものです。

デストラクタの中で例外が起こること自体は問題は無く、それがデストラクタ内で完結すれば良いです。

次に、メモリ領域が解放されます。

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

この2つの過程をセットで行うことが、delete演算子の仕事になっています。free() はメモリ領域を解放することだけが仕事なので、デストラクタを呼び出す過程が飛ばされてしまいます。

RAII(資源獲得時初期化)

new演算子を使ったら、最後には delete演算子を使って解放を行わなければなりません。C言語の頃の常識は、「忘れないように気を付けよう」でしたが、C++ にはデストラクタがあるので、解放を忘れないようにコーディングすることができます。つまり、解放が必要なオブジェクトをクラスのメンバ変数として持つようにして、デストラクタ内で解放するようにすれば良いのです。

#include <iostream>

class MyClass {
public:
    MyClass()
    {
        std::cout << "Constructor" << std::endl;
    }
    ~MyClass()
    {
        std::cout << "Destructor" << std::endl;
    }
};

class MyClassWrapper {
public:
    MyClassWrapper() :
        mObj(new MyClass())
    {}

    ~MyClassWrapper()
    {
        delete mObj;
    }

private:
    MyClass* const  mObj;
};


int main()
{
    MyClassWrapper obj;
}

実行結果:

Constructor
Destructor

コードをこの形にするにあたっては、確保を行う部分についても、同一のクラスに任せるようにします。つまり、確保から解放までの過程のすべてを1つのクラスに一任します。このような考え方は RAII (Resource Acquisition Is Initialization。資源獲得時初期化) と呼ばれており、C++ の重要なテクニックになっています。

RAII の考え方は、動的なメモリ確保と解放だけでなく、ファイルオープンとクローズとか、通信の接続と切断といったように、対応関係があって、最後に必ず何かしなければならないという形のときには常に適用できます。

RAII の考え方を使って、delete演算子の適用を確実に行えるようにするのと同時に、あたかも通常のポインタであるかのように振る舞うクラスを定義することができます。これは通常のポインタよりもスマート(賢い)ということで、スマートポインタと呼ばれています。スマートポインタを自作することは勿論可能ですが、標準ライブラリに既に用意されたものがあるので、まずはこれを理解して、積極的に使っていくようにしましょう。C++ においては、動的確保された領域を指す生のポインタをそのまま使うことは避けるべきです
標準ライブラリのスマートポインタについては、【標準ライブラリ】第3章第4章で解説していますので、そちらを参照して下さい。


練習問題

問題① 「RAII」の項のサンプルプログラムで、MyClassWrapperクラスは MyClass のオブジェクトを保持していますが「公開」されていないので、事実上使用できません。例えば、次のような関数があると、MyClassWrapperクラスが保持している MyClass のオブジェクトが必要になります。

void f(const MyClass* mc);

MyClassWrapperクラスのオブジェクトを使いつつも、上記のような関数が利用できるように、MyClassWrapperクラスにメンバを補って下さい。

問題② RAII の考え方を使って、std::fopen() に対する std::fclose() を確実に行うクラスを設計して下さい。


解答ページはこちら

参考リンク

更新履歴

'2017/9/9 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ