閉じる

【C++】動的メモリの基本:new/deleteの使い方

C++で配列やオブジェクトを柔軟に扱うには、実行時にメモリを確保する「動的メモリ管理」が重要です。

本記事では、C++の基本的な動的メモリ確保手段であるnew/deleteについて、配列や構造体、クラスを例にしながら詳しく解説します。

図解とサンプルコードを交えて、ミスしやすいポイントや注意点も丁寧に説明します。

new/deleteとは何か

静的メモリと動的メモリの違い

C++のプログラムでは、メモリは大きく分けて「静的メモリ」と「動的メモリ」に分かれます。

関数内のローカル変数は、通常スタック領域に確保され、関数の終了とともに自動的に解放されます。

一方、newを使って確保されたメモリはヒープ領域に確保され、deleteを呼ぶまで残り続けます。

この違いにより、次のような場面で動的メモリが必要になります。

  • 配列のサイズが実行時まで分からない場合
  • ライフタイム(生存期間)を関数のスコープより長くしたい場合
  • 大きなデータを扱う場合

new/deleteは、ヒープ領域を手動で管理するためのC++の基本機能です。

newとdeleteの基本構文

newとdeleteの基本的な構文は次のようになります。

  • 単一オブジェクトの確保と解放
    • 確保: ポインタ型 変数 = new 型名;
    • 解放: delete ポインタ変数;
  • 配列の確保と解放
    • 確保: ポインタ型 変数 = new 型名[要素数];
    • 解放: delete[] ポインタ変数;

単一オブジェクトにはdelete、配列にはdelete[]を必ず対応させることが重要です。

単一オブジェクトに対するnew/delete

整数1つを動的に確保する

まずは最も基本的な例として、int型の変数を1つだけnewで確保し、deleteで解放するプログラムを見てみます。

C++
#include <iostream>
using namespace std;

int main() {
    // int型の領域をヒープに1つ確保し、そのアドレスをpに格納
    int* p = new int;  // ここで動的メモリ確保

    // 確保したメモリに値を書き込む
    *p = 42;

    cout << "pが指す値は: " << *p << endl;

    // メモリを解放する
    delete p;  // ここでメモリを返却

    // 解放後のポインタは安全のためnullptrを代入しておくと良い
    p = nullptr;

    return 0;
}
実行結果
pが指す値は: 42

この例では、new intで確保した領域に*p = 42;と代入しています。

stack上の変数と違い、明示的にdeleteを呼ばない限り、このメモリは解放されません

コンストラクタ付きオブジェクトのnew

クラスのオブジェクトをnewする場合、コンストラクタが自動的に呼ばれる点も重要です。

C++
#include <iostream>
#include <string>
using namespace std;

class Person {
public:
    string name;
    int age;

    // コンストラクタ
    Person(const string& n, int a) : name(n), age(a) {
        cout << "コンストラクタ実行: " << name << endl;
    }

    // デストラクタ
    ~Person() {
        cout << "デストラクタ実行: " << name << endl;
    }
};

int main() {
    // Personオブジェクトを動的に生成
    Person* p = new Person("Taro", 20);

    cout << p->name << " は " << p->age << " 歳です" << endl;

    // 解放時にデストラクタが呼ばれる
    delete p;
    p = nullptr;

    return 0;
}
実行結果
コンストラクタ実行: Taro
Taro は 20 歳です
デストラクタ実行: Taro

ここでのポイントは、newでコンストラクタが、deleteでデストラクタが自動的に呼ばれるということです。

クラス設計とnew/deleteは密接に関係します。

配列に対するnew[]/delete[]

実行時にサイズが決まる配列

コンパイル時にサイズが決まっている配列はint a[10];のように書けますが、サイズが実行時まで分からない場合にはnew[]を使います。

C++
#include <iostream>
using namespace std;

int main() {
    int n;
    cout << "要素数を入力してください: ";
    cin >> n;

    // ヒープにint型n個分の配列を確保
    int* arr = new int[n];

    // 要素に値を代入
    for (int i = 0; i < n; ++i) {
        arr[i] = i * 10;
    }

    // 値を表示
    for (int i = 0; i < n; ++i) {
        cout << "arr[" << i << "] = " << arr[i] << endl;
    }

    // 配列の解放はdelete[]で行う
    delete[] arr;
    arr = nullptr;

    return 0;
}
実行結果
要素数を入力してください: 3
arr[0] = 0
arr[1] = 10
arr[2] = 20

ここではint* arr = new int[n];として、整数をn個分まとめて確保しています。

確保したときと同じ形(new[]で確保したならdelete[])で解放することが重要です。

クラスオブジェクトの配列new

クラスの配列をnew[]で確保すると、全要素分のコンストラクタとデストラクタが呼ばれる点にも注意します。

C++
#include <iostream>
using namespace std;

class Node {
public:
    int value;
    Node() : value(0) {
        cout << "Nodeコンストラクタ value=" << value << endl;
    }
    ~Node() {
        cout << "Nodeデストラクタ value=" << value << endl;
    }
};

int main() {
    int n = 3;

    // Nodeオブジェクトの配列を動的に生成
    Node* nodes = new Node[n];

    // 各要素に値を設定
    for (int i = 0; i < n; ++i) {
        nodes[i].value = (i + 1) * 100;
    }

    // 値を表示
    for (int i = 0; i < n; ++i) {
        cout << "nodes[" << i << "].value = " << nodes[i].value << endl;
    }

    // 配列の解放
    delete[] nodes;
    nodes = nullptr;

    return 0;
}
実行結果
Nodeコンストラクタ value=0
Nodeコンストラクタ value=0
Nodeコンストラクタ value=0
nodes[0].value = 100
nodes[1].value = 200
nodes[2].value = 300
Nodeデストラクタ value=100
Nodeデストラクタ value=200
Nodeデストラクタ value=300

new[]で3つのNodeを確保すると、コンストラクタが3回実行され、delete[]で解放するとデストラクタも3回呼ばれます。

オブジェクトのライフサイクル管理が自動で行われることが分かります。

new/deleteの注意点と典型的なミス

deleteとdelete[]の使い分け

最もよくあるミスの1つが、配列をnew[]で確保したのにdeleteで解放してしまうパターンです。

これは未定義動作となり、プログラムがクラッシュしたり、メモリ破壊を引き起こす可能性があります。

  • 正しい対応
    • int* p = new int;delete p;
    • int* a = new int[10];delete[] a;

どのnewに対してどのdeleteを使うか、常にセットで意識することが大切です。

メモリリークとダングリングポインタ

もう1つの代表的な問題がメモリリークです。

これは「newしたのにdeleteを呼ばなかった」ことにより、再利用できないメモリが増え続けてしまう現象です。

C++
#include <iostream>
using namespace std;

void leak() {
    int* p = new int;   // 確保
    *p = 100;

    // deleteをしないで関数を終了 → メモリリーク
}

int main() {
    for (int i = 0; i < 100000; ++i) {
        leak();  // leak内で確保したメモリが解放されない
    }
    cout << "終了" << endl;
    return 0;
}
実行結果
終了

このプログラムは表面的には正常終了しますが、内部ではメモリが大量に消費され続けている可能性があります。

また、ダングリングポインタ(ぶら下がりポインタ)にも注意します。

これはdeleteされたメモリを指し続けているポインタのことです。

解放後のポインタを使うと、やはり未定義動作になります。

C++
#include <iostream>
using namespace std;

int main() {
    int* p = new int(10);

    delete p;  // ここでメモリは解放される

    // pはまだ同じアドレスを保持しているが、そのアドレスは無効
    // *p = 20;  // 未定義動作。コメントアウトしておく

    // 安全のためにnullptrを代入しておく
    p = nullptr;

    return 0;
}

deleteしたポインタには必ずnullptrを代入するという習慣をつけると、バグを減らしやすくなります。

配列・構造体・クラスを組み合わせた応用例

構造体の動的配列

少し実用的な例として、構造体の配列を動的に確保し、データを格納していくサンプルを示します。

C++
#include <iostream>
#include <string>
using namespace std;

struct Student {
    int id;
    string name;
    int score;
};

int main() {
    int n;
    cout << "学生数を入力してください: ";
    cin >> n;

    // 学生情報をn人分確保
    Student* students = new Student[n];

    // データ入力
    for (int i = 0; i < n; ++i) {
        cout << "学生" << i << "のID: ";
        cin >> students[i].id;
        cout << "学生" << i << "の名前: ";
        cin >> students[i].name;
        cout << "学生" << i << "の点数: ";
        cin >> students[i].score;
    }

    // 入力結果を表示
    cout << "=== 入力された学生情報 ===" << endl;
    for (int i = 0; i < n; ++i) {
        cout << "ID: " << students[i].id
             << ", 名前: " << students[i].name
             << ", 点数: " << students[i].score << endl;
    }

    // メモリ解放
    delete[] students;
    students = nullptr;

    return 0;
}
実行結果
学生数を入力してください: 2
学生0のID: 1
学生0の名前: Taro
学生0の点数: 80
学生1のID: 2
学生1の名前: Hanako
学生1の点数: 90
=== 入力された学生情報 ===
ID: 1, 名前: Taro, 点数: 80
ID: 2, 名前: Hanako, 点数: 90

このように、構造体やクラスも配列として動的に扱えるため、柔軟なデータ管理が可能になります。

2次元配列風の確保(基本形)

C++では、2次元配列風の構造もnewを使って表現できます。

C++
#include <iostream>
using namespace std;

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

    // 行ポインタの配列を確保
    int** matrix = new int*[rows];

    // 各行ごとに列分を確保
    for (int i = 0; i < rows; ++i) {
        matrix[i] = new int[cols];
    }

    // 値の設定
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            matrix[i][j] = (i + 1) * 10 + j;
        }
    }

    // 表示
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            cout << matrix[i][j] << " ";
        }
        cout << endl;
    }

    // 解放(内側の配列から順に)
    for (int i = 0; i < rows; ++i) {
        delete[] matrix[i];
    }
    delete[] matrix;
    matrix = nullptr;

    return 0;
}
実行結果
10 11 12
20 21 22

ここでは、new[]をネストして使うことで2次元配列風の構造を実現しています。

その分、解放も階層ごとに行う必要があり、ミスが増えやすい点には注意が必要です。

現代C++との関係と使いどころ

なるべくスマートポインタやコンテナを使う

C++11以降では、生のnew/deleteを直接使う場面はかなり減っています

代わりに次のような手段が推奨されます。

  • std::vectorstd::string などの標準コンテナ
  • std::unique_ptrstd::shared_ptr などのスマートポインタ

これらは内部でnew/deleteを使っていますが、自動的にメモリを解放してくれるため、安全性が高いです。

とはいえ、次のような場面では、new/deleteの理解が今でも重要になります。

  • レガシーコードの読み書き
  • 自分でコンテナやスマートポインタを実装する場合
  • OSや組み込み向けの低レベルコードを書く場合

new/deleteを学ぶ意義

new/deleteは「C++がどのようにメモリを扱っているか」を理解するための基礎です。

たとえ実務で頻繁に生のnew/deleteを書かなくても、これを理解しておくと次のようなメリットがあります。

  • メモリリークやクラッシュの原因を追いやすくなる
  • スマートポインタやコンテナの挙動を正しく理解できる
  • クラス設計時のコンストラクタ/デストラクタの意味が明確になる

まとめ

本記事では、C++のnew/deleteについて、単一オブジェクトと配列、構造体やクラスの例を通して解説しました。

newで確保したメモリは必ずdeleteで、new[]で確保した配列は必ずdelete[]で解放することが基本です。

また、解放し忘れによるメモリリークや、解放後ポインタを使うダングリングポインタにも注意が必要です。

実務ではstd::vectorやスマートポインタの利用が推奨されますが、new/deleteの仕組みを理解しておくことは、C++プログラマにとって大きな武器になります。

ポインタと参照の基礎

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

URLをコピーしました!