C++で動的に配列を扱う際は、確保と解放の対応関係を正しく理解しておくことが重要です。
特にnew[]で確保したメモリは必ずdelete[]で解放するという基本は、初心者がつまずきやすいポイントです。
本記事では、配列確保と解放の正しい組み合わせ、間違いやすい例、注意点、そして実践的なヒントまで丁寧に解説します。
C++のnew[]とdelete[]の基本
配列はnew[]で確保する
動的配列はnew[]
を用いて確保します。
戻り値は配列の先頭要素を指すポインタです。
組み込み型の場合、初期化をしないと未定義の値が入るため、必要に応じて{}
で値初期化を行います。
例えばnew int[n]{}
とすると0で初期化されます。
例:int配列を確保して利用する
#include <iostream>
int main() {
std::size_t n = 5;
// int配列を動的に確保。{} を付けると 0 初期化されます。
int* arr = new int[n]{}; // すべて 0 で初期化
// 値を代入して利用
for (std::size_t i = 0; i < n; ++i) {
arr[i] = static_cast<int>(i * 10);
}
// 内容を表示
for (std::size_t i = 0; i < n; ++i) {
std::cout << "arr[" << i << "] = " << arr[i] << '\n';
}
// 配列は delete[] で解放
delete[] arr;
arr = nullptr; // ダングリングポインタ防止のために nullptr を代入
return 0;
}
arr[0] = 0
arr[1] = 10
arr[2] = 20
arr[3] = 30
arr[4] = 40
配列はdelete[]でメモリ解放する
配列を解放するときは必ずdelete[]を使います。
クラス型の配列では、各要素のデストラクタが順番に呼ばれます。
これがdelete
との大きな違いです。
例:コンストラクタとデストラクタの呼び出しを確認する
#include <iostream>
struct Tracer {
int id;
Tracer(int i) : id(i) {
std::cout << "Tracer(" << id << ") constructed\n";
}
~Tracer() {
std::cout << "Tracer(" << id << ") destructed\n";
}
};
int main() {
std::size_t n = 3;
// Tracer の配列を new[] で確保
Tracer* arr = new Tracer[n]{ Tracer(0), Tracer(1), Tracer(2) };
// 何らかの処理
std::cout << "Working...\n";
// 配列は delete[] で解放し、各要素のデストラクタが呼ばれます
delete[] arr;
arr = nullptr;
return 0;
}
Tracer(0) constructed
Tracer(1) constructed
Tracer(2) constructed
Working...
Tracer(2) destructed
Tracer(1) destructed
Tracer(0) destructed
new/new[]とdelete/delete[]の違い
new
とdelete
は単一オブジェクト用、new[]
とdelete[]
は配列用です。
配列の場合は要素数分のコンストラクタとデストラクタが呼ばれます。
内部的には、配列の要素数を管理するための情報が確保領域の前方などに付加される実装もあり、対応を間違えると未定義動作になります。
組み込み型では一見動いてしまうことがあっても正しくありません。
以下に対応関係を簡単にまとめます。
用途 | 確保 | 解放 | 備考 |
---|---|---|---|
単一オブジェクト | T* p = new T; | delete p; | コンストラクタ1回、デストラクタ1回 |
配列 | T* p = new T[n]; | delete[] p; | 要素数分のコンストラクタとデストラクタ |
new[]とdelete[]の正しい対応関係
T* p = new T[n] と delete[] p の組み合わせ
配列を確保したら解放はdelete[]
で行います。
クラス型であれば要素ごとにデストラクタが呼ばれ、リソースが確実に片付けられます。
例:std::stringの配列を確保して解放する
#include <iostream>
#include <string>
int main() {
std::size_t n = 3;
// std::string の配列を new[] で確保
std::string* names = new std::string[n];
// 代入して利用
names[0] = "Alice";
names[1] = "Bob";
names[2] = "Charlie";
for (std::size_t i = 0; i < n; ++i) {
std::cout << names[i] << '\n';
}
// 配列は delete[] で解放
delete[] names;
names = nullptr;
return 0;
}
Alice
Bob
Charlie
T* p = new T と delete p の組み合わせ
単一のオブジェクトを確保した場合はdelete
で解放します。
ここでdelete[]
を使ってはいけません。
例:単一オブジェクトの確保と解放
#include <iostream>
#include <string>
int main() {
// 単体の std::string を new で確保
std::string* ps = new std::string("hello");
std::cout << *ps << '\n';
// 単体は delete で解放
delete ps;
ps = nullptr;
return 0;
}
hello
混在は不可(間違い例)
new と delete[] または new[] と delete の組み合わせは未定義動作です。
プログラムがクラッシュしたり、静かにメモリ破壊が起きたりします。
以下は悪い例で、実行してはいけません。
// 悪い例1: 配列を delete している
int* a = new int[10];
/* ... */
delete a; // NG: 配列なのに delete
// 正しくは: delete[] a;
// 悪い例2: 単体を delete[] している
double* p = new double;
/* ... */
delete[] p; // NG: 単体なのに delete[]
// 正しくは: delete p;
未定義動作の症状の例:
- まれに動いてしまうが、別の箇所で突然クラッシュする
- 解放されないメモリが溜まり続ける
- デストラクタが呼ばれずリソースリークが発生する
配列解放の注意点
new[]にdeleteは使わない
配列は必ずdelete[]で解放します。
delete
では要素数が分からないため、必要なデストラクタ呼び出しが行われず、未定義動作になります。
組み込み型でも同様に危険です。
// NG: 未定義動作の例
std::string* ss = new std::string[2];
/* ... */
delete ss; // だめ。必ず delete[] ss; を使う
delete[]の書き忘れはメモリリーク
解放し忘れるとメモリリークになります。
関数の途中で早期リターンがあるコードは特に漏れがちです。
#include <iostream>
bool process(bool ok) {
int* buf = new int[100]; // 要確保
// 何らかの処理...
if (!ok) {
// ここで return すると解放されない → メモリリーク
return false;
}
// 正常系の最後で解放
delete[] buf;
return true;
}
対策としては、早期リターンの前にきちんとdelete[]
する、またはスマートポインタやコンテナで自動解放に任せる方法があります(ヒントは後述)。
配列サイズは自分で管理する
delete[]
自体はサイズを必要としませんが、配列を使う処理では自分でサイズを保持しておく必要があります。
関数に渡す際も、ポインタとサイズをセットで扱うのが基本です。
#include <iostream>
void printArray(const int* arr, std::size_t n) {
for (std::size_t i = 0; i < n; ++i) {
std::cout << arr[i] << (i + 1 == n ? '\n' : ' ');
}
}
int main() {
std::size_t n = 4;
int* arr = new int[n]{1, 2, 3, 4};
printArray(arr, n); // サイズとセットで渡す
delete[] arr;
arr = nullptr;
}
同じポインタを二重にdelete[]しない
二重解放は未定義動作です。
解放後はnullptrを代入し、必要ならチェックしてから解放するパターンを徹底します。
#include <iostream>
int main() {
int* data = new int[3]{1,2,3};
delete[] data; // 1回目
data = nullptr;
// nullptr への delete[] は安全に何もしません
delete[] data; // 2回目だが安全
return 0;
}
初心者向けの実践ヒント
確保と解放を近い場所に書く
確保と解放が離れていると漏れやすく、可読性も落ちます。
できるだけ同じ関数やスコープ内で完結させると安全です。
早期リターンが多い場合は、例外安全も考慮してスマートポインタを使うのも有効です。
例:std::unique_ptrで配列を自動解放する
#include <iostream>
#include <memory>
int main() {
// unique_ptr<T[]> はスコープを抜けると自動で delete[] されます
std::unique_ptr<int[]> arr(new int[5]{1,2,3,4,5});
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << (i == 4 ? '\n' : ' ');
}
// 明示的な delete[] は不要。スコープ終了時に自動解放。
return 0;
}
1 2 3 4 5
変数名で配列か単体かを区別しやすくする
読みやすさとミス防止のため、命名で単体か配列かを示すのも一手です。
例えば、配列ポインタはarr
やlist
、単体ポインタはpObj
やptr
など、チームで統一するとdelete[]とdeleteの取り違えを減らせます。
まとめ
new[]で確保したらdelete[]で解放、newで確保したらdeleteで解放という対応関係は、配列のデストラクタ呼び出しや内部管理情報の整合性に直結する基本ルールです。
これを破ると未定義動作に陥ります。
配列のサイズは自分で管理し、二重解放や解放漏れを避けるためにnullptr代入や確保と解放の近接配置を徹底してください。
可能であればstd::unique_ptr<T[]>やstd::vectorなど自動解放の仕組みも活用すると、安全で保守しやすいコードになります。