C++入門|配列メモリの生成と解放

ヒープ領域に10人分の戦闘力を召喚せよ。C++の配列メモリは、newで生み出し、delete[]で正しく解放するのが鉄則です。

これまで、new 演算子と delete 演算子を使って、int 型1つ分の領域やクラスのインスタンスをヒープ領域に確保する流れを見てきました。
ここからはその応用として、複数の値をまとめて扱う配列のメモリを、動的に生成して解放する方法を見ていきます。

ドラゴンボール風にたとえるなら、戦士1人分の戦闘力だけを記録するのではなく、修行に参加した10人分の戦闘力データをまとめてスカウター保管庫に並べて管理するイメージです。
1つだけの値なら new int() でも十分ですが、複数人分を一度に管理したいなら、配列としてまとめて確保したほうが便利です。

ただし、ここで大事なのが生成方法と解放方法がセットで決まっていることです。
普通の変数1個を new で確保したなら delete を使いますが、配列を new[] で確保したなら delete[] を使わなければいけません。

この違いはとても大切です。
見た目は少しの差でも、意味は大きく違います。
とくに delete のあとに [] を付け忘れるミスは、とても起こりやすいので、ここでしっかり整理しておきましょう。

配列メモリとは何か

配列は、同じ型のデータを連続して並べて保存する仕組みです。
たとえば int 型の配列なら、整数をいくつもまとめて管理できます。

ドラゴンボールの世界観で考えるなら、これは修行生たちの戦闘力を順番に並べた記録棚のようなものです。

要素番号記録される値のイメージ
01人目の戦闘力
12人目の戦闘力
23人目の戦闘力
......
910人目の戦闘力

普通の配列は、たとえば次のように書けます。

int power[10];

これはスタック領域に10個分の int 型の領域を確保する書き方です。
一方で、プログラムの流れの途中で必要になったときに作りたい場合や、あとからサイズを決めたい場合は、new 演算子を使ってヒープ領域に確保する方法が使われます。

new 演算子で配列領域を確保する

配列を動的に確保するには、次のように書きます。

p = new int[10];

この書き方では、int 型10個分の領域がヒープ領域に確保されます。
ここで大事なのは、配列の場合は int のあとに [] を付け、その中に要素数を書くことです。

また、基本型1個分を確保するときのような new int() とは違い、配列では () を付けません

書き方意味
new int()int 型1個分を確保する
new int[10]int 型10個分の配列を確保する

ここは似ているようでかなり大事な違いです。
1人分の戦闘力メーターを出すのか、10人分のスカウター棚を一気に用意するのか、その違いだと思うとイメージしやすいです。

図1:配列メモリの生成イメージ

この図が示していること

この図では、new int[10] によって、ヒープ領域に int 型10個分の並んだ領域 が作られることを表しています。
そして、ポインタ変数 pPower は、その配列全体の先頭、つまり 0番目の要素の場所 を指しています。

ここで見てほしいのは、pPower 自体が10個の値を全部持っているわけではないことです。
pPower が持っているのは、あくまで 配列の先頭のアドレス です。
その先にある複数の要素へ、pPower[0]、pPower[1] のようにアクセスしていきます。

配列の各要素にアクセスする

配列を確保したあとには、それぞれの要素に値を代入したり、表示したりできます。
アクセス方法は通常の配列と同じで、添字 を使います。

p[i] = i;
cout << p[i] << " ";

この書き方では、i が 0 なら p[0]、1 なら p[1]、2 なら p[2] というように順番にアクセスできます。

ドラゴンボール風にいえば、

  • p[0] は 1人目の修行生の戦闘力記録
  • p[1] は 2人目の修行生の戦闘力記録
  • p[2] は 3人目の修行生の戦闘力記録

のようなイメージです。

つまり、配列を確保するというのは、同じ型のデータを順番に並べて扱える保管庫を出現させることなんですね。

サンプルプログラム

では、ドラゴンボール風の世界観にアレンジしたサンプルを見てみましょう。
今回は、修行場に集まった10人分の戦闘力ポイントをヒープ領域に確保し、順番に代入して表示するプログラムにしています。

プロジェクト/ファイル名: Chap4_04/main.cpp

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    // 戦闘力データを指すポインタ変数
    int* pPower = NULL;
    int i;

    // int型10個分の領域を動的確保
    pPower = new int[10];

    // 10人分の戦闘力を順番に記録して表示
    for (i = 0; i < 10; ++i) {
        // 修行生の戦闘力を設定
        pPower[i] = (i + 1) * 1000;
        cout << pPower[i] << " ";
    }

    // 改行
    cout << endl;

    // 動的に確保した配列領域を解放
    delete[] pPower;

    return 0;
}

実行結果

1000 2000 3000 4000 5000 6000 7000 8000 9000 10000

このプログラムの流れ

このプログラムは、大きく分けると次の流れで動いています。

手順処理内容
1int* pPower = NULL;配列を指すポインタ変数を用意する
2pPower = new int[10];int 型10個分の配列領域をヒープに確保する
3for 文で代入各要素に戦闘力を入れる
4for 文で表示各要素の値を順番に表示する
5delete[] pPower;配列領域を解放する

この流れは、配列の動的確保の基本そのものです。
特に、最後の delete[] がとても重要です。

for 文で順番に値を代入する

for 文の部分では、0番目から9番目までの各要素に値を代入しています。

for (i = 0; i < 10; ++i) {
    pPower[i] = (i + 1) * 1000;
    cout << pPower[i] << " ";
}

ここでの pPower[i] は、配列の i 番目の要素です。

たとえば、

  • i = 0 のとき pPower[0] = 1000
  • i = 1 のとき pPower[1] = 2000
  • i = 2 のとき pPower[2] = 3000

というように、順番に値が入っていきます。

これは、修行生名簿に沿って、1人ずつ戦闘力を書き込んでいる感じです。

図2:配列要素へのアクセス

この図が示していること

この図は、pPower[i] の i が変化することで、配列の各要素へ順番にアクセスできることを示しています。
配列は連続した領域なので、0番目、1番目、2番目…というように、添字を使ってきれいに管理できます。

また、値を代入したあと、そのまま pPower[i] を表示できるので、配列の中に何が入っているのかも確認しやすいです。

配列の解放には delete[] を使う

配列を new[] で確保した場合、解放するときは次のように書きます。

delete[] pPower;

ここで一番大事なのは、delete のあとに [] を付けることです。

生成時の書き方解放時の書き方
new int()delete p;
new int[10]delete[] p;

つまり、

  • 1個分なら delete
  • 配列なら delete[]

です。

この対応は必ずセットで覚えてください。

なぜ delete[] が必要なのか

配列は、1個の変数ではなく、複数の要素がまとまった領域です。
そのため、解放するときにも「これは配列です」という情報を伝える必要があります。
その役目を持っているのが delete[] です。

ドラゴンボール風にいえば、修行場に戦闘力メーターを1台だけ出したなら delete で片付ければ十分です。
でも、10台まとめて並べたスカウター棚なら、棚全体を片付ける命令が必要です。
それが delete[] なんですね。

[] を付けずに delete するとどういう意味になるのか

ここはとても大切です。
配列を new int[10] で確保したのに、次のように書いてしまうミスがあります。

delete pPower;

これは文法エラーにはなりません。
そのため、うっかり見逃しやすいです。
でも意味としては、配列として確保した領域に対して、1個のオブジェクトを消す delete を使っている状態です。

つまり delete pPower; は、

  • pPower が指しているものを
  • 配列ではなく
  • 1つの単独のオブジェクトとして扱って解放しようとする

という意味になります。

これは、生成方法と解放方法が対応していない状態です。

配列に対して delete を使うと何が問題なのか

配列に対して delete を使ってしまうと、正しく解放される保証がありません
C++ではこのような不一致は 未定義動作 になります。

未定義動作というのは、

  • たまたま動くように見える
  • 何も起こらないように見える
  • 一部だけ壊れる
  • 突然落ちる
  • 別の場所でおかしな不具合になる

といった、非常に危ない状態のことです。

特に、要素がクラス型だった場合はもっと深刻です。
delete[] なら配列要素すべてに対して適切な後始末が行われますが、delete だとその扱いが壊れてしまい、デストラクタが正しく呼ばれないことがあります。

今回はデストラクタがありませんが、それでも
new[] で確保したものを delete で解放するのは間違い です。

delete と delete[] の違いを整理する

書き方想定している対象意味
delete p;単一オブジェクト1個分の領域を解放する
delete[] p;配列複数要素の配列領域を解放する

この違いは、見た目以上に重要です。
たった [] の有無ですが、プログラムにとっては「1人だけ片付ける」のか「全員分まとめて片付ける」のかくらい違います。

図3:delete と delete[] の違い

この図が示していること

この図では、new int[10] で確保した配列領域は、delete[] で解放する必要があることを示しています。
左側は正しいケースで、配列全体が正しく解放されます。
右側は delete を使ってしまったケースで、配列として正しく扱われず、不安定な状態になることを表しています。

ここから分かるのは、配列の生成と解放は必ず new[] と delete[] を対応させる必要があるということです。

配列の動的確保で押さえたいポイント

ここまでの内容を整理すると、配列メモリの生成と解放で押さえたいポイントは次のとおりです。

ポイント内容
配列の動的確保new 型名[要素数] で行う
配列要素へのアクセスp[i] の形で行う
解放方法delete[] p; を使う
delete の [] 省略文法エラーにはならないが危険
不一致の結果未定義動作になり、正しく動く保証がない

配列を動的に確保するメリット

では、なぜわざわざ配列を動的に確保するのでしょうか。
理由はいくつかあります。

必要なときに確保できる

プログラムの途中で、急に大量のデータを扱いたくなる場面があります。
そのとき、ヒープ領域に配列を確保すれば、必要なタイミングで使い始められます。

関数の外に依存しにくい

スタック領域の配列は、その関数の実行中だけ有効です。
一方で、ヒープ領域に確保した配列は、delete[] するまで残せます。
そのため、扱い方の自由度が上がります。

大きな配列を扱いやすい

大量のデータを扱うとき、スタック領域よりヒープ領域のほうが向いている場合があります。
ドラゴンボール風にいえば、大勢の戦士の戦闘力記録を一時的に管理したいとき、動的配列が便利です。

実務的に気を付けたいこと

入門段階では、new[] と delete[] の対応をしっかり覚えることが何より大切です。
そのうえで、実際のC++では、手動でのメモリ管理ミスを減らすために、vector などの便利な仕組みがよく使われます。

ただ、new[] と delete[] の基礎を理解しておくと、

  • C++のメモリ管理の考え方
  • ポインタと配列の関係
  • 配列がどのようにメモリ上に作られるか

がぐっと分かりやすくなります。

今回のテーマは、その基礎の中でもとても大事なところです。
配列を生成したら delete[] で解放する
このルールは、修行の基本動作のように、しっかり体に覚え込ませておくと安心です。