閉じる

C++のnew配列とdelete[]の使い方|メモリ解放の注意点まで解説

C++プログラミングにおいて、メモリ管理はパフォーマンスと安定性を左右する極めて重要な要素です。

特に実行時にデータの個数が決まる動的配列を扱う際、new[]演算子とdelete[]演算子の正しい理解は避けて通れません。

もしこれらを誤って使用すれば、メモリリークや未定義動作といった深刻なバグを引き起こし、システムのクラッシュを招く恐れがあります。

本記事では、動的配列の基本から、多次元配列の扱い、そして現代的なC++(Modern C++)におけるベストプラクティスまで、プロフェッショナルな視点で徹底的に解説します。

動的配列が必要な理由とメモリ領域の仕組み

プログラミングを行う際、配列の要素数がコンパイル時に確定している場合は静的な配列(スタック領域)を使用できます。

しかし、ユーザーの入力やファイルの内容に応じて要素数を変更したい場合には、実行中にメモリを確保する「動的メモリ確保」が必要です。

スタックとヒープの違い

C++のメモリ管理を理解する上で、スタック領域ヒープ領域の違いを知ることは不可欠です。

スタック領域

関数のローカル変数などが格納される領域です。

管理はコンパイラが自動で行い、スコープを抜ければ自動的に解放されます。

しかし、サイズに制限があり、非常に大きな配列を確保しようとするとスタックオーバーフローを発生させます。

ヒープ領域

プログラムの実行中に自由に使用できる広大なメモリ領域です。

new演算子を使って確保したメモリはこのヒープ領域に配置されます。

解放のタイミングをプログラマが制御できる反面、明示的に解放しない限りメモリが残り続けるというリスクがあります。

new[] 演算子による動的配列の確保

動的な配列を作成するには、new演算子の後ろに型名と角括弧[]を記述します。

基本的な構文と使い方

まずは、最もシンプルな単一の型を持つ動的配列の確保方法を見てみましょう。

C++
#include <iostream>

int main() {
    int size;
    std::cout << "配列のサイズを入力してください: ";
    std::cin >> size;

    // ヒープ領域にsize個のint型配列を確保
    // pointerは配列の先頭アドレスを指す
    int* pArray = new int[size];

    // 配列の要素に値を代入
    for (int i = 0; i < size; ++i) {
        pArray[i] = i * 10;
    }

    // 値を表示
    for (int i = 0; i < size; ++i) {
        std::cout << "pArray[" << i << "] = " << pArray[i] << std::endl;
    }

    // 後で解説するが、確保したメモリは必ず解放する
    delete[] pArray;

    return 0;
}
実行結果
配列のサイズを入力してください: 3
pArray[0] = 0
pArray[1] = 10
pArray[2] = 20

上記のコードでは、ユーザーが入力したsizeに基づいて、実行時にメモリが確保されています。

ポインタ変数pArrayは、確保された連続したメモリ領域の先頭アドレスを保持しています。

動的配列の初期化

new[]で確保した直後のメモリの内容は不定(ゴミデータ)であることが一般的です。

初期化を行いたい場合は、以下のような構文が利用可能です。

  • ゼロ初期化: new int[size]() のように空の括弧を付けると、すべての要素が0で初期化されます。
  • リスト初期化: new int[3]{1, 2, 3} のように波括弧を使用すると、特定の値で初期化できます。

delete[] 演算子によるメモリの解放

確保したメモリを使い終わったら、必ずdelete[]を用いてOSにメモリを返却しなければなりません。

これを怠ると、アプリケーションが実行を続けるうちにメモリを食いつぶすメモリリークが発生します。

なぜ delete ではなく delete[] なのか

C++において、newnew[]は明確に区別されます。

同様に、deletedelete[]も厳密に使い分ける必要があります。

  • new で確保した単一のオブジェクト → delete で解放
  • new[] で確保した配列オブジェクト → delete[] で解放

もし、new[]で確保した配列をdelete(角括弧なし)で解放しようとすると、未定義の動作を引き起こします。

具体的には、配列の先頭要素のデストラクタしか呼ばれなかったり、メモリ管理情報が破損してプログラムが即座にクラッシュしたりすることがあります。

配列サイズとデストラクタの関係

なぜdelete[]が必要なのか。

それは、コンパイラが「このポインタが指しているのは1つの要素なのか、それとも複数の要素の塊なのか」を判別するためです。

クラスのオブジェクトを配列として確保した場合、delete[]配列のすべての要素に対してデストラクタを呼び出します

C++
#include <iostream>

class Sample {
public:
    Sample() { std::cout << "コンストラクタ呼び出し" << std::endl; }
    ~Sample() { std::cout << "デストラクタ呼び出し" << std::endl; }
};

int main() {
    // 3つのSampleオブジェクトを動的に作成
    Sample* pSamples = new Sample[3];

    // 解放時にすべてのデストラクタが呼ばれる
    delete[] pSamples; 

    return 0;
}
実行結果
コンストラクタ呼び出し
コンストラクタ呼び出し
コンストラクタ呼び出し
デストラクタ呼び出し
デストラクタ呼び出し
デストラクタ呼び出し

もしここでdelete pSamples;としてしまうと、デストラクタが1回しか呼ばれない(あるいは不正終了する)ため、各オブジェクトが保持していたリソースが適切に処理されません。

多次元の動的配列の作成方法

2次元以上の配列を動的に確保する場合、少し複雑な手順が必要になります。

C++では「ポインタの配列」を作成し、その各要素に対してさらに配列を割り当てるという手法が一般的です。

2次元配列の確保と解放

以下のコードは、行数rows、列数colsの2次元配列を動的に確保する例です。

C++
#include <iostream>

int main() {
    int rows = 3;
    int cols = 4;

    // 1. 行の数だけ「int型へのポインタ」の配列を確保
    int** matrix = new int*[rows];

    // 2. 各行に対して「int型」の配列(列)を確保
    for (int i = 0; i < rows; ++i) {
        matrix[i] = new int[cols];
    }

    // データの利用
    matrix[1][2] = 100;
    std::cout << "matrix[1][2] = " << matrix[1][2] << std::endl;

    // 3. 解放は確保の逆順で行う
    // まず各行を解放
    for (int i = 0; i < rows; ++i) {
        delete[] matrix[i];
    }
    // 最後にポインタの配列自体を解放
    delete[] matrix;

    return 0;
}
実行結果
matrix[1][2] = 100

この方法の注意点は、確保した回数分だけ、正しい順序で解放を行わなければならない点です。

外側のmatrixを先にdelete[]してしまうと、各行のアドレス情報が失われ、内側のメモリを解放できなくなります。

new[] と malloc() の決定的な違い

C言語の経験がある方は、malloc()関数を使用してメモリを確保することに慣れているかもしれません。

しかし、C++においてnew[]を使用することには明確なメリットがあります。

機能new[] / delete[]malloc() / free()
コンストラクタ呼び出される呼び出されない
デストラクタ呼び出される呼び出されない
型安全性型を正しく認識するvoid* を返すためキャストが必要
サイズ計算自動で行われるsizeof(T) * n の計算が必要
オーバーロード可能不可

C++のクラスを扱う場合、malloc()は絶対に使用してはいけません

なぜなら、malloc()はメモリ領域を確保するだけで、オブジェクトの初期化(コンストラクタの実行)を行わないからです。

メモリ管理における注意点とトラブルシューティング

動的配列を扱う際、初心者が陥りやすいミスや、注意すべきポイントを整理します。

二重解放(Double Free)の防止

すでにdelete[]したポインタに対して、再度delete[]を実行するとプログラムはクラッシュします。

これを防ぐための最も確実な方法は、解放した直後にポインタに nullptr を代入することです。

C++
int* p = new int[10];
delete[] p;
p = nullptr; // 安全策

// C++では nullptr に対する delete[] は安全(何もしない)
delete[] p;

ダングリングポインタ(野良ポインタ)

メモリを解放した後、そのアドレスを指し続けたままのポインタをダングリングポインタと呼びます。

解放済みのメモリにアクセスしようとすると、他のデータが書き込まれていたり、メモリアクセス違反が発生したりします。

これもnullptr代入で防ぐべき重要なポイントです。

例外発生時のリーク

new[]でメモリを確保した後、delete[]を呼び出す前にプログラムがエラー(例外)を投げて中断した場合、解放処理がスキップされてメモリリークが発生します。

C++
void riskyFunction() {
    int* data = new int[1000];
    
    // ここで何らかのエラーが発生して関数を抜けると...
    throw std::runtime_error("エラー発生");

    // ここに到達しないため、dataはリークする
    delete[] data;
}

Modern C++ におけるベストプラクティス:new を使わない

2020年代以降のC++開発において、生ポインタとnew[]を直接扱う機会は大幅に減っています

より安全で、管理が容易な代替手段が標準ライブラリで提供されているからです。

std::vector の活用

動的配列が必要な場合、まず第一の選択肢はstd::vectorです。

  • サイズを自由に変更可能。
  • メモリ管理(確保と解放)を完全に自動で行う(RAII原則)。
  • 範囲外アクセスチェックなどの安全機能。
C++
#include <vector>

void modernStyle() {
    // new[] は不要
    std::vector<int> vec(10); 

    vec[0] = 5;
    // 関数を抜けるときに自動的にメモリが解放される
}

std::unique_ptr<T[]> による管理

どうしてもポインタのセマンティクスが必要な場合は、スマートポインタであるstd::unique_ptrを使用します。

C++
#include <memory>

void smartPointerStyle() {
    // 配列専用の unique_ptr
    std::unique_ptr<int[]> pData(new int[10]);

    pData[0] = 100;
    // delete[] を書かなくても、スコープを抜ければ自動解放される
}

現代のC++では、「生ポインタで new を書いたら負け」と言われるほど、これらの標準コンテナやスマートポインタの利用が推奨されています。

これにより、メモリリークのリスクをほぼゼロに抑えることができます。

まとめ

C++におけるnew[]delete[]は、自由なメモリ管理を可能にする強力なツールですが、その分大きな責任が伴います。

  • new[]で確保したものは、必ずdelete[]で解放する。
  • 単一オブジェクト用のdeleteと混同しない。
  • 多次元配列は確保の逆順で丁寧に解放する。
  • 解放後はポインタをnullptrにして安全性を高める。
  • 実務ではstd::vectorstd::unique_ptrを活用し、生のnew[]を避ける。

これらの原則を守ることで、堅牢で効率的なC++プログラムを記述できるようになります。

基本をしっかり押さえつつ、現代的な手法を取り入れて、安全なコード設計を心がけましょう。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!