C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。
この章の概要です。
関連する話題が、以下のページにあります。
動的にメモリ領域を割り当てたうえで、その領域にオブジェクト📘をインスタンス化📘したい場合には、std::malloc関数などのC言語から引き継いだ関数を使用できません。std::malloc関数がおこなうことは、メモリ領域を割り当てることだけであって、オブジェクトは作られないからです。
代わりに、new演算子を使用します。new演算子は、オブジェクトをインスタンス化する場合であっても、クラス型でない型のための領域を確保する場合であっても使用できます。
new演算子を使う式(new式)の構文は次のようになっています。
new 型名;
new 型名(実引数の並び);
new演算子は、型名に応じた必要な大きさのメモリ領域を動的に確保し、そこにオブジェクトをインスタンス化します。そして、確保されたメモリ領域を指し示すポインタが返されます。
失敗したとしても、ヌルポインタが返されることはないため、そのようなエラーチェックは不要です。
new演算子の失敗は、(伝えられるとすれば)例外機構(第32章)によって実現されます。詳細は後で取り上げています。
new演算子には、クラス型でも、そうでない型でも指定可能です。クラス型の場合には、コンストラクタ📘が呼び出されますから、必要に応じて ( ) を補って、実引数を指定します。クラス型でない場合でも、単一の初期化子を与えて初期化できます。
* pm = new MyClass(10, 20);
MyClassint* pi = new int(10);
実引数を与えない形での呼び出しは注意が必要です。
* pm = new MyClass;
MyClassint* pi = new int;
この場合の初期化方法は、デフォルト初期化(第13章)です。生成する型がクラス型であればデフォルトコンストラクタが呼び出されますが、クラス型でない場合は未初期化なままです。
new演算子によってインスタンス化されたオブジェクトの破棄と、確保されたメモリ領域の解放📘は、delete演算子で行います。free関数は使えません。
delete演算子を使う式(delete式)の構文は次のようになっています。
delete メモリアドレス;
delete演算子には、new演算子で確保されたメモリ領域のメモリアドレスか、ヌルポインタしか指定できません。ほかの指定は未定義の動作になります。ヌルポインタを指定した場合は、何も起こらないことが保証されています。
delete演算子に指定したメモリアドレスが、クラス型のオブジェクトがあるメモリアドレスの場合は、デストラクタ📘が呼び出されます。
以下は、new と delete の使用例です。
#include <iostream>
class MyClass {
public:
(const char* s) : mStr(s)
MyClass{
std::cout << "MyClass(" << mStr << ")" << std::endl;
}
~MyClass()
{
std::cout << "~MyClass(" << mStr << ")" << std::endl;
}
private:
const char* mStr;
};
* func()
MyClass{
return new MyClass("func");
}
int main()
{
* c1 = new MyClass("main");
MyClass* c2 = func();
MyClass
delete c1;
delete c2;
}
実行結果:
MyClass(main)
MyClass(func)
~MyClass(main)
~MyClass(func)
動的に割り当てたいものが配列の場合には、異なる記法を使わなければなりません。これは解放の方も同様で、解放するものが配列であることをきちんと伝える必要があります。
new演算子で型名を指定するときに、要素数の指定を追加します。
new 型名[要素数];
今後、この記法を new[] と表記します。
確保されたメモリ領域の先頭(配列の先頭要素)を指すポインタが返されます。失敗したとしても、ヌルポインタが返されることはないため、そのようなエラーチェックは不要です。
クラス型の場合には、それぞれのデフォルトコンストラクタが呼び出されます。呼び出し可能なデフォルトコンストラクタがないとコンパイルエラーになります。
要素数は定数でなくても構いません。負数を指定した場合は未定義の動作📘です。
要素数に 0 を指定することは許されます。その場合でも有効なポインタが返されますが、そのポインタの先にあるものを参照すると未定義の動作になります。この場合でも delete[] は必要です。
new[] で確保された配列を解放するには、delete演算子の方に [] を付加します。
delete [] メモリアドレス;
今後、この記法を delete[] と表記します。
[] の内側には何もなく、要素数を指定する必要はありません。
delete[] に渡すメモリアドレスは、new[] で確保されたメモリ領域の先頭のメモリアドレスか、ヌルポインタでなければなりません。ほかの指定は未定義の動作になります。ヌルポインタを指定した場合は、何も起こらないことが保証されています。
クラス型の場合には、それぞれのデストラクタが呼び出されます。
new を使ったのなら delete を、new[] を使ったのなら delete[] を使うというように、使い分けなければならないことに注意してください。間違った対応関係にしてしまってもコンパイルエラーにはなりません。
以下は、使用例です。
#include <iostream>
class MyClass {
public:
()
MyClass{
std::cout << "MyClass()" << std::endl;
}
~MyClass()
{
std::cout << "~MyClass()" << std::endl;
}
};
int main()
{
* c = new MyClass[5];
MyClass
delete [] c;
}
実行結果:
MyClass()
MyClass()
MyClass()
MyClass()
MyClass()
~MyClass()
~MyClass()
~MyClass()
~MyClass()
~MyClass()
ところで、必要としているものが、動的な文字配列(文字列)なのであれば、std::string(【標準ライブラリ】第2章)を使いましょう。その方がずっと安全で便利です。
【上級】文字以外の配列の場合には、std::vector(【標準ライブラリ】第5章)を検討すると良いです。
ここで、new演算子がしていることの詳細を見ておきます。少し難しい部分も含まれますが、できるだけ正確に知っておくと、C++ の理解につながります。
まず、メモリを確保しようとします。このときに使われる領域をフリーストアと呼びます。
ヒープ領域📘や、動的メモリ領域といった言葉で表現されることもありますが、C++ の用語としてはフリーストアです。
指定した型の大きさ分の領域を確保することを要求しますが、実際に確保される大きさは、これよりも大きいかもしれません。これはたとえば、管理情報を置くための場所が必要になるからです。
【上級】メモリ領域を確保するために、operator new または operator new[] という関数を呼び出しています。これらの関数をプログラマーも定義できますが、そうしなかった場合はデフォルトの実装が使用されます。この辺りの詳細は、第36章で解説します。
この後の手順は、メモリ確保に成功したか、失敗したかによって異なります。
メモリ確保に成功した場合は、続けて、オブジェクトをインスタンス化する作業が行われます。
ここでコンストラクタが呼び出されます。この過程の存在がC言語と C++ との大きな違いであり、malloc関数を使うことが適切でない理由です。malloc関数にはこの過程がありませんから、オブジェクトは生成されません。
コンストラクタの中でエラーが起きる可能性があります。コンストラクタの処理が正常に終了できなかった場合、前の過程で確保されたメモリ領域の解放が行われます。
また、コンストラクタの処理が最後まで正常に完了できなかった場合、オブジェクトは “作られなかった” とみなされます。これが意味することは、デストラクタは呼ばれないということです。コンストラクタの本体の処理が中途半端に実行されてしまっていると、デストラクタでおこなう予定だった後片付けがなされないということですから、注意深く実装されなければなりません。この話題は、第32章であらためて取り上げます。
確保されたメモリ領域のメモリアドレスを、要求された型を指すポインタで返却して完了となります。
メモリ確保に失敗してしまった場合には、newハンドラが呼び出されます。newハンドラは単なる関数です。プログラマーが自分で用意した関数を事前に登録しておけるので、メモリ確保に失敗したことを検知できます。
newハンドラを登録していなかった場合は、例外が送出されます。例外については、第32章で説明しますが、特に気にしなければ、結果的にプログラムの実行が終了されます。
このような機構が設けられているのは、メモリ確保に失敗したときに、メモリ領域から重要でないデータを急きょ解放することで、領域を空けてやることができれば、メモリ確保を成功に導くことが可能であるかもしれないからです。実際、newハンドラとして登録した関数が、普通に関数の末尾まで実行されたり、return文で戻ってきたりした場合、再度、メモリ確保を試みることになっています。その結果、メモリ確保に成功すれば、何事もなかったかのように、メモリ確保成功時の処理が続行されます。
もし、再試行にも失敗したら、再び newハンドラが呼び出され、以降は同じことを無限に繰り返します。
少し分かり難いので、疑似的なコードで示すと次のようになります。
void* new関数 (std::size_t size)
{
for (;;) {
void* p = std::malloc(size);
if (p != NULL) {
return p;
}
if (newハンドラが登録されていない?) {
(new関数から抜け出す)
例外を送出 }
newハンドラを呼ぶ}
}
newハンドラを登録するには、std::set_new_handler関数を使用します。std::set_new_handler関数を使うには、<new> という妙な名前の標準ヘッダをインクルードします。
namespace std {
(new_handler p);
new_handler set_new_handler}
具体的には次のようなプログラムになります。
#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関数内では、std::abort関数(C言語のリファレンス)を呼び出しているので、メモリ確保に失敗した場合は、プログラムが異常終了します。
先ほどのサンプルプログラムで、my_new_handler関数の中にある std::abort関数の呼び出しをコメントアウトして試してみれば、標準出力に “my_new_handler” が繰り返し出力されるので、my_new_handler関数が無限に呼び出されている様子が分かります。
このように、new演算子は、newハンドラの実装次第で、メモリ確保失敗時に何が起きるかは変わります。無限ループ📘になって、呼び出し元に帰ってこないかもしれませんし、プログラムが異常終了するかもしれません。そのため、new の失敗をチェックするために、ヌルポインタが返ってきたかどうかを調べることは間違っています。実際、ヌルポインタが返されることはあり得ません。
【上級】new演算子を「new(std::nothrow) 型名(コンストラクタに渡す引数)」という形で使用した場合だけは、失敗するとヌルポインタを返します。ただしこの記法は、古いプログラムを維持するために用意されているものであり(C++ の規格化前はヌルポインタを返す実装もありました)、基本的に使わない方が良く、標準の使い方で統一した方が良いでしょう。
delete演算子がしていることは、new演算子の逆回しのような作業です。
まず、オペランドがヌルポインタの場合には何もせずに終了します。
続いて、対象がクラス型であれば、デストラクタが呼び出されて、オブジェクトが破棄されます。オブジェクトを破棄するだけなので、メモリ領域は確保されたままです。つまり、領域は予約されていて、誰も使っていないという状態になります。
したがって、デストラクタ内でエラーが起こると、メモリ領域が未解放なままになってしまいます。デストラクタに限りませんが、何かを終了させる処理ではエラーが発生しないようにプログラムを書くべきです。それが無理ならば、ただちにプログラムを異常終了させるしかありません。
オブジェクトが破棄された後で、メモリ領域が解放されます。解放された領域がどのような状態になるかは不定です。
【上級】メモリ領域を解放するために、operator delete という関数を呼び出しています。この関数をプログラマーも定義できますが、そうしなかった場合はデフォルトの実装が使用されます。この辺りの詳細は、第36章で解説します。
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) {
[i] = i;
array}
for (int i = 0; i < ARRAY_SIZE; ++i) {
std::cout << array[i] << std::endl;
}
delete array;
}
問題② デストラクタを利用すれば、delete演算子の呼び忘れを防げます。new演算子で確保された MyClass型のポインタを対象に、そのような呼び忘れを防ぐためのクラスを設計してください。
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
![]() |
管理者情報 | プライバシーポリシー |