C++ 再入門 その12 ヒープからのメモリ割り当てと返却(2) - new と delete
C++のヒープからのメモリ割り当てに先立って、C言語の malloc と free について説明しました。
C++ 再入門 その11 ヒープからのメモリ割り当てと返却(1)
他にも malloc の仲間たちや malloc が行っているヒープ領域の管理、良くあるトラブルや注意点など触れておきたいネタはたくさんあるのですが、それらはC言語の話ではあるので、別のマガジンで取り上げよう思います。
というところで、C++での動的メモリの割り当てについてですが、C++はもちろんC言語のライブラリを呼ぶことが出来るので malloc と free を使うことも出来ます。とはいえC++では言語として動的メモリ割り当ての機構を持っており、より安全でわかりやすくヒープを使う方法が用意されています。そして、これらを混ぜて使うことはトラブルの元なので、特別な理由がない限り malloc を使うことはありません。
C++にはヒープから領域を確保してオブジェクトを作成する new 演算子とオブジェクトを破壊して領域をヒープに返却する delete 演算子を持っています。
まず、一番シンプルな整数の配列を動的に確保する場合、
int *array = new int[10];
delete[] array;;
これは対象がクラスではなく単純な型なので、単に領域の確保と解放を行うだけで意味としては
int *array = (int * )malloc(sizeof(int)*10);
free(array);
と同じです。mallocと異なるのは領域を確保できなかった場合に、戻り値としてNULLが戻るのではなく、new 演算子の処理の中で例外が発生し適切な例外ハンドラに処理が渡るという点です。ですからポインタとして戻って来る値は必ず有効なのでNULLであるかのチェックをする必要もなく安心して使うことが出来ます(この例外処理にも、もちろん例外はあります^^;)。
New演算子
さて new 演算子では単純な型であれば初期値の設定もC言語っぽい書き方が可能です。
int *array = new int[10] {0,1,2,3,4,5,6,7,8,9};
この演算子でクラスのインスタンス(実体)をヒープに作成することも出来ます。
classA *pi = new classA;
delete pi;
これがmallocと異なるのは、newによってclassAの領域を確保するとともに、classAのコンストラクタを呼び出し、適切な初期化を行ったうえでポインタを返すというところです。そして delete によって、もしデストラクタが用意されていれば、それを呼び出して必要な後始末をすることができます。コンストラクタにパラメタを渡す場合にはクラス名に引き数を追加すればOKです。
classA *pi = new classA(1, 2);
これで整数を2つ持つコンストラクタが呼び出されインスタンスが初期化されます。
ここで先の整数配列とちょっとだけ書き方が違うのに気が付かれた方もいるかもしれません。整数は配列なので型名の後ろに[]で囲んだ要素数が書かれていますが、クラスの場合は配列ではないのでクラス名のみで、配列をdeleteする時には delete[] で、そうでない場合には[]を付けずに delete と書きます。これを間違って配列を解放する時にdeleteに[]を付け忘れるとメモリリークが発生します。
ところでオブジェクトの配列を new で作成する時には、それぞれの要素はデフォルトコンストラクタ(引き数が無い時に呼ばれるコンストラクタ)が呼ばれ、個別に引き数を与えたコンストラクタを呼び出す方法はありません。
classA * pi = new classA[10]; // OK
classA * pi = new classA[10](1); // ERROR 配列に初期化子を指定することは出来ません。
new と delete の配列形式
new 演算子と delete 演算子
ところで、new は演算子です。ですからクラスで、この演算子をオーバーロードすることができます(もちろん delete も)。まだオーバーロードについては大雑把な説明しかしてきていませんが、
void *operator new(size_t);
void operator delete(void *);
というクラス関数を定義すれば、そのクラスの new 演算子を自分でカスタマイズすることができます。もっとも他にも初期値を持つ new であるとか、配列用の new も用意しないといろいろ使いにくいです。演算子のオーバーロード関数はクラスのstaticな関数となりthisポインタが無いので、あれこれする時には注意してメンバを設計する必要があります。そのあたりは追々ということで。そういえば new 演算子で例外が発生した時に、どのように catch するのか、例外の内容をカスタマイズしたい時にはどのようにするか(set_new_handler関数)なんていう書き方もあるのですが、こちらも例外処理のところで触れましょう。
ところで、この配列の初期化に関して新しい処理系(C++20)では、初期化子によって要素の数が推定できる場合、宣言の部分で要素数の指定を省略できる変更があるようです。
new式での配列要素数の推論
C++では同じオブジェクトや型をいくつもまとめて扱う時には、配列ではなくてテンプレートのvectorなどを使うことが多いので、配列の時にどう書くんだっけ?というのは忘れがちです。オブジェクトの寿命は関数の単位とは独立したスコープを持つことが多いので、スタックではなくヒープに置くことが普通です。そして new で確保したポインタをいろいろな関数で使い回すことが一般的で、このような使い方をする時に、いったい誰がどのタイミングでオブジェクトの寿命が尽きたことを判断し delete を呼ぶべきなのかを決めるのが難しいことになります。そこで autoptr が導入されたり、リファレンスカウンタを組み込んだり、ガベージコレクタによって使われていない領域を回収するようになりつつあります。次は簡単にそんなことを調べてみたいと思います。
ヘッダ画像は、以下のものを使わせていただきました。
https://commons.wikimedia.org/wiki/File:ISO_C%2B%2B_Logo.svg
Jeremy Kratz - https://github.com/isocpp/logos , パブリック・ドメイン,
https://commons.wikimedia.org/w/index.php?curid=62851110による
#プログラミング #プログラミング言語 #プログラミング講座 #cpp #ヒープ #new #delete #演算子 #配列