C言語からC++にステップアップすると、動的メモリ確保でつまずきやすいのが配列用のnew[]
とdelete[]
です。
ここでは両者の正しい対応関係と未定義動作を避けるための具体的な書き方を、初心者の方にもわかりやすい順序で丁寧に解説します。
最後に実践的な運用ポイントもまとめます。
C++のnew[]とdelete[]の基本
newとnew[]の違い
new
は単一オブジェクトの生成、new[]
は配列(複数個の連続した要素)の生成に使います。
生成される個数と初期化の方法が異なるため、文法だけでなく意味の違いを理解することが重要です。
要点の整理
new T
は1個のT
を生成します。new T[n]
はn
個のT
を連続領域に生成します。- オブジェクト型の場合、
new
は1回、new[]
は要素数ぶんコンストラクタが呼ばれます。
以下の表で特徴を並べて確認します。
項目 | new | new[] |
---|---|---|
生成個数 | 1 | n個(連続領域) |
典型例 | int* p = new int(42); | int* a = new int[10]; |
コンストラクタ | 1回 | n回 |
初期化 | 引数可 | 要素ごとにデフォルト/値初期化のみ(引数は個別指定不可) |
対応する解放 | delete | delete[] |
初心者の方が特に誤解しやすいのは、new[]
で生成した配列に対しては必ずdelete[]
が対応するという点です。
次節でさらに深掘りします。
deleteとdelete[]の違い
delete
は単一オブジェクトの破棄、delete[]
は配列全体の破棄に使います。
配列の場合は要素ごとにデストラクタが必要なため、ランタイムは配列の個数情報を内部的に保持していることが多いです。
破棄時の挙動
delete p;
は1回だけデストラクタを呼びます。delete[] a;
は要素数回デストラクタを呼びます(一般に逆順)。
この「何回デストラクタを呼ぶか」を実現するため、new[]
とdelete[]
はペアで設計されています。
したがって混在させると未定義動作になります。
new[]とdelete[]は対応させる
対応関係は次の通りです。
これ以外の組み合わせは未定義動作です。
確保 | 正しい解放 | 備考 |
---|---|---|
p = new T; | delete p; | 単一オブジェクト |
a = new T[n]; | delete[] a; | 配列オブジェクト |
ここでの「未定義動作」とは、クラッシュする場合もあれば表面上は動いて見える場合もあるということです。
テストでたまたま動いても安全ではありません。
混在は未定義動作になる
new[]
で確保したものをdelete
で解放したり、new
で確保したものをdelete[]
で解放するのは未定義動作です。
内部実装では配列の要素数やアラインメント情報を埋め込むことがあり、それを誤ったdelete
系で処理すると破綻します。
悪い例(絶対にしないこと)
// NG例: new[] と delete の混在は未定義動作
int main() {
int* a = new int[10];
// ... 何らかの処理 ...
delete a; // ← 必ず delete[] にしなければならない
return 0;
}
なお、new
/delete
とmalloc
/free
を混在させることも未定義動作です。
この解説ではC++のnew[]
/delete[]
に絞りますが、Cの関数と混在させないことも強く意識してください。
コンストラクタとデストラクタの挙動
配列のnew[]
では、各要素に対してコンストラクタが順に呼ばれ、delete[]
では各要素のデストラクタが逆順に呼ばれます。
動作を見える化するためのサンプルを示します。
#include <iostream>
class Tracer {
public:
Tracer() : id_(next_id_++) {
std::cout << "Ctor: id=" << id_ << "\n";
}
~Tracer() {
std::cout << "Dtor: id=" << id_ << "\n";
}
private:
int id_;
static int next_id_;
};
int Tracer::next_id_ = 0;
int main() {
std::cout << "Allocate array\n";
Tracer* arr = new Tracer[3]; // デフォルトコンストラクタが3回呼ばれる
std::cout << "Delete array\n";
delete[] arr; // デストラクタが逆順(2,1,0)で3回呼ばれる
return 0;
}
Allocate array
Ctor: id=0
Ctor: id=1
Ctor: id=2
Delete array
Dtor: id=2
Dtor: id=1
Dtor: id=0
配列の動的確保と解放のコード例
int配列をnew[]で確保しdelete[]で解放
整数配列は最も基本的な例です。
確保から利用、解放までを一気通貫で示します。
#include <iostream>
int main() {
const int n = 5; // 要素数
int* a = new int[n]; // new[] で確保
// 配列に値を入れる
for (int i = 0; i < n; ++i) {
a[i] = i + 1; // 1,2,3,4,5
}
// 合計を計算
int sum = 0;
for (int i = 0; i < n; ++i) {
sum += a[i];
}
std::cout << "sum = " << sum << "\n";
// 忘れずに解放。配列は delete[] を使う
delete[] a;
a = nullptr; // ダングリング防止のために明示的に無効化
return 0;
}
sum = 15
クラス配列の生成と破棄の手順
ユーザ定義型の配列でも、基本は同じです。
ただし各要素のコンストラクタとデストラクタが正しく呼ばれることが重要です。
#include <iostream>
#include <string>
class Person {
public:
Person() : name_("unknown") {
std::cout << "Ctor: " << name_ << "\n";
}
~Person() {
std::cout << "Dtor: " << name_ << "\n";
}
void set_name(const std::string& n) { name_ = n; }
const std::string& name() const { return name_; }
private:
std::string name_;
};
int main() {
const int n = 3;
Person* people = new Person[n]; // 3人分のPersonがデフォルト構築される
// 各要素を設定
people[0].set_name("Alice");
people[1].set_name("Bob");
people[2].set_name("Carol");
// 利用
for (int i = 0; i < n; ++i) {
std::cout << "Hello, " << people[i].name() << "!\n";
}
// 破棄。各要素のデストラクタが呼ばれる
delete[] people;
people = nullptr;
return 0;
}
Ctor: unknown
Ctor: unknown
Ctor: unknown
Hello, Alice!
Hello, Bob!
Hello, Carol!
Dtor: Carol
Dtor: Bob
Dtor: Alice
二次元配列でのnew[]の注意
C++のnew[]
で多次元配列を扱うときは2通りあります。
行ごとに確保する方法と、1塊で確保する方法です。
それぞれ解放手順が異なります。
行ごとに確保する方法(ジャグ配列)
#include <iostream>
int main() {
const int rows = 2;
const int cols = 3;
// 行ポインタの配列を確保
int** mat = new int*[rows];
// 各行を個別に確保(行ごとに delete[] が必要)
for (int r = 0; r < rows; ++r) {
mat[r] = new int[cols];
}
// 値を設定と表示
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < cols; ++c) {
mat[r][c] = r * 10 + c;
std::cout << mat[r][c] << (c + 1 == cols ? '\n' : ' ');
}
}
// 解放は「各行をdelete[]」→「行ポインタ配列をdelete[]」の順
for (int r = 0; r < rows; ++r) {
delete[] mat[r];
}
delete[] mat;
mat = nullptr;
return 0;
}
0 1 2
10 11 12
この方法は行ごとにサイズが異なる配列も表現できますが、解放の手順を間違えやすい点に注意します。
1塊で確保する方法(連続領域)
#include <iostream>
int main() {
const int rows = 2;
const int cols = 3;
// 要素数 rows*cols の1次元配列として連続確保
int* mat = new int[rows * cols];
auto idx = [cols](int r, int c) { return r * cols + c; };
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < cols; ++c) {
mat[idx(r, c)] = r * 10 + c;
std::cout << mat[idx(r, c)] << (c + 1 == cols ? '\n' : ' ');
}
}
// 解放は1回だけ
delete[] mat;
mat = nullptr;
return 0;
}
0 1 2
10 11 12
この方法はキャッシュ効率が良く、解放も1回で済みます。
多くの場合はこちらが扱いやすいです。
よくある間違いとメモリリーク
new[]をdeleteで解放してしまう
配列にdelete
を呼ぶと、要素数分のデストラクタが呼ばれないため未定義動作です。
POD型(intなど)でも、ランタイムが内部ヘッダを参照する実装ではクラッシュやヒープ破壊を招きます。
// NG: new[] に delete は対応しない
int* a = new int[10];
// ...
delete a; // 必ず delete[] a; にする
newをdelete[]で解放してしまう
単一オブジェクトにdelete[]
を呼ぶのも未定義動作です。
実装依存のヘッダを誤って読み書きし、ヒープ状態が壊れる可能性があります。
// NG: new に delete[] は対応しない
int* p = new int(42);
// ...
delete[] p; // 必ず delete p; にする
delete[]を忘れてリークになる
確保した配列を解放し忘れるとメモリリークになります。
早期return
や例外で解放コードがスキップされるパターンが典型です。
#include <string>
// NG例: 途中で return して delete[] が実行されない
char* read_line_or_null(bool ok) {
char* buf = new char[1024]; // new[]
if (!ok) {
return nullptr; // ← ここでリーク
}
// ... bufに読み込む処理 ...
// 正常経路では解放していないのも問題
return buf; // 呼び手が必ず delete[] する契約が必要
}
このような問題を避けるため、C++では所有権を表すスマートポインタを推奨します。
配列にはstd::unique_ptr<T[]>
が使えます。
確保したスコープを抜けると自動でdelete[]
が呼ばれます。
#include <memory>
#include <iostream>
int main() {
std::unique_ptr<int[]> a(new int[5]); // 例外や早期returnでも自動解放
for (int i = 0; i < 5; ++i) a[i] = i;
std::cout << a[4] << "\n";
// スコープ末尾で自動的に delete[] が呼ばれる
return 0;
}
4
ただし本記事のテーマはnew[]
/delete[]
の対応関係です。
実務では配列はまずstd::vector
やstd::array
の利用を検討してください。
メモリリークを防ぐ運用ポイント
確保と解放を一対で書く
new[]
を書いた直後に、その解放位置と方法を決めておくと漏れにくくなります。
最低でも同じ関数内の対称位置にdelete[]
を書く、あるいはgoto
やbreak
などの制御フローでスキップされないようブロック構造を工夫します。
もっとも効果的なのは、所有権をオブジェクトに閉じ込めるRAII(リソースの獲得は初期化)の活用です。
#include <memory>
void process() {
// RAIIで自動解放。配列は unique_ptr<T[]> を使う
std::unique_ptr<char[]> buf(new char[1024]);
// ... bufを使った処理 ...
} // スコープを抜けると自動で delete[] buf
関数の早期returnに注意する
早期return
のパスごとにdelete[]
が確実に実行されるかを点検します。
手動で管理する場合は「1出口方式」にするか、スコープ終了時に必ず解放される仕組みを使います。
// 手動管理の例: 失敗時でも必ず解放
int do_work_manual(bool ok) {
int* data = new int[100];
int result = -1;
if (!ok) {
result = 0;
goto cleanup; // 例外がない環境での単純な対処
}
// ... 処理 ...
result = 1;
cleanup:
delete[] data;
return result;
}
実務では手動管理より、std::unique_ptr<T[]>
やstd::vector<T>
の採用が堅実です。
ポインタの所有範囲を明確にする
「誰が解放するのか」をあらかじめ決め、コードやコメントで明記します。
生ポインタを関数に渡すときは、所有権の移動があるのか、借用なのかを区別してください。
所有権を移動するならスマートポインタで表現し、借用ならconst T*
またはT*
と長さをペアで渡し、呼び出し側のライフタイム内でのみ使用するルールを守ります。
目的 | 推奨の受け渡し方 | 解放の責任 |
---|---|---|
借用(所有権なし) | T* ptr, std::size_t size | 呼び出し元 |
所有権の移動 | std::unique_ptr<T[]> | 受け取り側(スコープ終端で自動解放) |
共有所有 | std::shared_ptr<T> (配列は非推奨) | 参照カウントが0で自動解放 |
まとめ
new[]
で確保した配列は必ずdelete[]
で解放し、new
で確保した単一オブジェクトはdelete
で解放する、この1対1の対応関係が最重要ポイントです。
混在させると未定義動作になり、テストでは見逃されがちな潜在バグやメモリ破壊、クラッシュを引き起こします。
配列では要素数分のコンストラクタとデストラクタが呼ばれること、二次元配列の確保と解放順序の違いなども合わせて理解しておくと安全です。
実務では漏れやすい場面(早期return、例外)を避けるために、可能ならstd::vector
やstd::unique_ptr<T[]>
などRAIIを活用し、所有権の所在を明確に保つ運用を徹底してください。
これらの基本を守ることで、C++におけるメモリリークや未定義動作のリスクを大きく減らすことができます。