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

トップページModern C++編

Modern C++編は作りかけで、更新が停止しています。代わりに、C++14 をベースにして、その他の方針についても見直しを行った、新C++編を作成しています。
Modern C++編は削除される予定です。

この章の概要

この章の概要です。


new と delete

動的にメモリ領域を割り当てたうえで、その領域にオブジェクトをインスタンス化したい場合には、std::malloc関数などのC言語から引き継いだ関数を使用できません。std::malloc関数が行うことは、メモリ領域を割り当てることだけであって、オブジェクトは作られないからです。

代わりに、new演算子を使用します。new演算子は、オブジェクトをインスタンス化する場合であっても、クラス型でない型のための領域を確保する場合であっても使用できます。

new

new演算子を使う式(new式)の構文は次のようになっています。

new 型名;
new 型名(実引数の並び);

new演算子は、型名に応じた必要な大きさのメモリ領域を動的に確保し、そこにオブジェクトをインスタンス化します。そして、確保されたメモリ領域を指し示すポインタが返されます。

失敗したとしても、ヌルポインタが返されることはないため、そのようなエラーチェックは不要です。

new演算子の失敗は、(伝えられるとすれば)例外機構(第19章)によって実現されます。詳細は後で取り上げています

new演算子には、クラス型でも、そうでない型でも指定可能です。クラス型の場合には、コンストラクタが呼び出されますから、必要に応じて ( ) を補って、実引数を指定します。クラス型でない場合でも、単一の初期化子を与えて初期化できます。

MyClass* pm = new MyClass(10, 20);
int* pi = new int(10);

実引数を与えない形での呼び出しは注意が必要です。

MyClass* pm = new MyClass;
int* pi = new int;

この場合の初期化方法は、デフォルト初期化(第7章)です。生成する型がクラス型であればデフォルトコンストラクタが呼び出されますが、クラス型でない場合は未初期化なままです。

delete

new演算子によってインスタンス化されたオブジェクトの破棄と、確保されたメモリ領域の解放は、delete演算子で行います。free関数は使えません。

delete演算子を使う式(delete式)の構文は次のようになっています。

delete メモリアドレス;

delete演算子には、new演算子で確保されたメモリ領域のメモリアドレスか、ヌルポインタしか指定できません。ほかの指定は未定義の動作になります。ヌルポインタを指定した場合は、何も起こらないことが保証されています。

delete演算子に指定したメモリアドレスが、クラス型のオブジェクトがあるメモリアドレスの場合は、デストラクタが呼び出されます。

以下は、new と delete の使用例です。

#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++ の理解につながります。

まず、メモリを確保しようとします。このときに使われる領域をフリーストアと呼びます。

ヒープ領域や、動的メモリ領域といった言葉で表現されることもありますが、C++ の用語としてはフリーストアです。

指定した型の大きさ分の領域を確保することを要求しますが、実際に確保される大きさは、これよりも大きいかもしれません。これはたとえば、管理情報を置くための場所が必要になるからです。

【上級】メモリ領域を確保するために、operator new または operator new[] という関数を呼び出しています。これらの関数をプログラマーも定義できますが、そうしなかった場合はデフォルトの実装が使用されます。この辺りの詳細は、第41章で解説します。

この後の手順は、メモリ確保に成功したか、失敗したかによって異なります。

メモリ確保に成功した場合

メモリ確保に成功した場合は、続けて、オブジェクトをインスタンス化する作業が行われます。

ここでコンストラクタが呼び出されます。この過程の存在がC言語と C++ との大きな違いであり、malloc関数を使うことが適切でない理由です。malloc関数にはこの過程がありませんから、オブジェクトは生成されません。

コンストラクタの中でエラーが起きる可能性があります。コンストラクタの処理が正常に終了できなかった場合、前の過程で確保されたメモリ領域の解放が行われます。

また、コンストラクタの処理が最後まで正常に完了できなかった場合、オブジェクトは “作られなかった” とみなされます。これが意味することは、デストラクタは呼ばれないということです。コンストラクタの本体の処理が中途半端に実行されてしまっていると、デストラクタで行う予定だった後片付けがなされないということですから、注意深く実装されなければなりません。この話題は後で取り上げます

確保されたメモリ領域のメモリアドレスを、要求された型を指すポインタで返却して完了です。

メモリ確保に失敗した場合

メモリ確保に失敗してしまった場合には、newハンドラが呼び出されます。newハンドラは単なる関数です。プログラマーが自分で用意した関数を事前に登録しておけるので、メモリ確保に失敗したときに、それを知ることができます。

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

このような機構が設けられているのは、メモリ確保に失敗したときに、メモリ領域から重要でないデータを急きょ解放することで、領域を空けてやることができれば、メモリ確保を成功に導くことが可能であるかもしれないからです。実際、newハンドラとして登録した関数が、普通に関数の末尾まで実行されたり、return文で戻ってきたりした場合、再度、メモリ確保を試みることになっています。その結果、メモリ確保に成功すれば、何事もなかったかのように、メモリ確保成功時の処理が続行されます。

もし、再試行にも失敗したら、再び newハンドラが呼び出され、以降は同じことを無限に繰り返します。

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

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

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

namespace std {
    new_handler set_new_handler(new_handler p);
}

具体的には次のようなプログラムになります。

#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関数(C言語のリファレンス)を呼び出してプログラムを異常終了させます。

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

このように、new演算子は、newハンドラの実装次第で、メモリ確保失敗時に何が起きるかは変わります。無限ループになって、呼び出し元に帰ってこないかもしれませんし、プログラムが異常終了するかもしれません。そのため、new の失敗をチェックするために、ヌルポインタが返ってきたかどうかを調べることは間違っています。実際、ヌルポインタが返されることはあり得ません。

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

delete演算子がしていること

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

まず、オペランドがヌルポインタの場合には何もせずに終了します。

続いて、対象がクラス型であれば、デストラクタが呼び出されて、オブジェクトが破棄されます。オブジェクトを破棄するだけなので、メモリ領域は確保されたままです。つまり、領域は予約されていて、誰も使っていないという状態になります。

したがって、デストラクタ内でエラーが起こると、メモリ領域が未解放なままになってしまいます。デストラクタに限りませんが、何かを終了させる処理ではエラーが発生しないようにプログラムを書くべきです。それが無理ならば、ただちにプログラムを異常終了させるしかありません。

オブジェクトが破棄された後で、メモリ領域が解放されます。解放された領域がどのような状態になるかは不定です。

【上級】メモリ領域を解放するために、operator delete という関数を呼び出しています。この関数をプログラマーも定義できますが、そうしなかった場合はデフォルトの実装が使用されます。この辺りの詳細は、第42章で解説します。

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章第5章で解説していますので、そちらを参照してください。


練習問題

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

void f(const MyClass* mc);

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

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


解答ページはこちら

参考リンク


更新履歴

’2018/9/7 C++編【言語解説】第14章「動的なオブジェクトの生成」の修正に合わせて、内容更新。

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

’2017/9/9 新規作成。



前の章へ (第14章 右辺値参照とムーブ)

次の章へ (第16章 配列)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る