C++入門|4章の練習問題

修行メーターは、片付ける前に読み取ろう。C++の動的配列は、new[]で生成し、使い終わってからdelete[]で解放する順番が大切です。

4章では、C++のメモリ管理について学んできました。

new演算子を使うと、ヒープ領域にメモリを動的に確保できます。
delete演算子を使うと、newで確保したメモリを解放できます。

また、配列を動的に確保する場合は、new[]とdelete[]をセットで使います。

double* pEnergy = new double[5];

// 配列を使う処理

delete[] pEnergy;

この対応はとても大切です。

ただし、delete[]を書く場所にも注意が必要です。
delete[]で解放したあと、その配列にアクセスしてはいけません。

ドラゴンボール風にたとえるなら、修行場に5個の気力メーターを出して、そこに修行の安定率を記録したとします。
メーターを読み取る前に片付けてしまったら、もうそのメーターの値を安全に読むことはできません。

C++でも同じです。

new[]で確保した配列は、使い終わるまでは保持しておきます。
値の代入や表示が終わり、本当に不要になったタイミングでdelete[]します。

今回の練習問題では、delete[]したあとに配列へアクセスしてしまう危険なプログラムを、正しく動くように修正します。

学習する内容確認するポイント
new[]配列メモリをヒープ領域に生成する
配列アクセスpEnergy[i]の形で各要素を使う
delete[]配列メモリを解放する
解放後アクセスdelete[]後に配列を読むのは危険
修正方法表示を終えてからdelete[]する

この問題を通して、メモリは生成 → 利用 → 解放の順番で扱うことを確認していきましょう。

図:delete[]したあとに読もうとすると危険

この図が示していること

この図では、new double[5]で5個分の配列メモリを生成し、値を入れたあと、表示する前にdelete[]してしまう危険な流れを表しています。

delete[]を実行すると、その配列領域はもう使ってはいけない状態になります。
そのあとでpEnergy[i]を読もうとすると、解放済みのメモリへアクセスすることになり、異常終了や予期しない動作につながります。

問題4-1:解放後に配列へアクセスしているプログラムを修正する

問題文

次のプログラムは、ヒープ領域にdouble型5個分の配列を確保し、修行の安定率を記録するプログラムです。

しかし、このプログラムは、気力メーターの値を表示する前にdelete[]で配列を解放してしまっています。

そのため、解放済みの配列にアクセスする危険な状態になっています。

期待される実行結果を得られるように、プログラムを修正しなさい。

修正前のプログラム

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

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    // 気力安定率を保存する配列を指すポインタ変数
    double* pEnergyRate = NULL;

    // double型5個分の領域を動的確保する
    pEnergyRate = new double[5];

    // 5段階の気力安定率を設定する
    for (int i = 0; i < 5; i++) {
        pEnergyRate[i] = i / 10.0;
    }

    // 配列メモリを解放する
    delete[] pEnergyRate;

    // 解放後の配列を表示しようとしているため危険
    for (int i = 0; i < 5; i++) {
        cout << pEnergyRate[i] << " ";
    }

    // 改行する
    cout << endl;

    // プログラムが正常に終わったことを示す
    return 0;
}

期待される実行結果

0 0.1 0.2 0.3 0.4

この問題では、配列の値を表示する前にdelete[]している点が問題です。

配列を使い終わってからdelete[]するように、処理の順番を修正しましょう。

プログラムを作成するヒント

このプログラムの流れは、次のようになっています。

順番処理問題があるか
1pEnergyRate = new double[5];問題なし
2pEnergyRate[i] = i / 10.0;問題なし
3delete[] pEnergyRate;ここが早すぎる
4cout << pEnergyRate[i];解放後アクセスなので危険

delete[]は、配列を使い終わったあとに書く必要があります。

今回のプログラムでは、配列の値を表示する処理があります。

for (int i = 0; i < 5; i++) {
    cout << pEnergyRate[i] << " ";
}

この表示処理が終わるまでは、配列メモリを解放してはいけません。

正しい順番は、次のようになります。

正しい順番処理
1new double[5]で配列を確保する
2for文で値を代入する
3for文で値を表示する
4delete[]で配列を解放する
5必要に応じてポインタにNULLを入れる

つまり、delete[] pEnergyRate;の位置を、表示処理のあとに移動します。

// 表示が終わってから解放する
delete[] pEnergyRate;

さらに、解放後にpEnergyRateへNULLを入れておくと、もう有効なメモリを指していないことが分かりやすくなります。

pEnergyRate = NULL;

図:正しい順番は 生成 → 代入 → 表示 → 解放

この図が示していること

この図では、動的配列を安全に扱う正しい順番を表しています。

まずnew double[5]で配列メモリを確保します。
次に、各要素へ値を代入します。
そのあと、配列の値を表示します。
最後に、使い終わった配列をdelete[]で解放します。

大切なのは、delete[]を表示の前に書かないことです。

プログラムの解答例

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

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    // 気力安定率を保存する配列を指すポインタ変数
    double* pEnergyRate = NULL;

    // double型5個分の領域をヒープ領域に動的確保する
    pEnergyRate = new double[5];

    // 5段階の気力安定率を設定する
    for (int i = 0; i < 5; i++) {
        pEnergyRate[i] = i / 10.0;
    }

    // 配列の値を表示する
    for (int i = 0; i < 5; i++) {
        cout << pEnergyRate[i] << " ";
    }

    // 改行する
    cout << endl;

    // 配列を使い終わったので、動的に確保した領域を解放する
    delete[] pEnergyRate;

    // 解放済みであることを分かりやすくする
    pEnergyRate = NULL;

    // プログラムが正常に終わったことを示す
    return 0;
}

実行結果

0 0.1 0.2 0.3 0.4

解説

この問題のポイントは、delete[]の位置です。

修正前のプログラムでは、配列の値を表示する前に、次の処理を行っていました。

delete[] pEnergyRate;

この時点で、pEnergyRateが指していたヒープ領域の配列メモリは解放されます。

つまり、気力メーターはもう片付けられた状態です。

そのあとに、次のように配列の値を読もうとしています。

cout << pEnergyRate[i] << " ";

これは、解放済みのメモリにアクセスする危険な処理です。

C++では、delete[]したあとの領域を使ってはいけません。
使ってしまうと、異常終了したり、正しくない値が表示されたり、たまたま動いているように見えたりします。

このような、結果が保証されない危険な状態を、未定義動作といいます。

delete[]後にアクセスしてはいけない理由

delete[] pEnergyRate;を実行すると、pEnergyRateが指していたヒープ領域は解放されます。

delete[] pEnergyRate;

この時点で、配列の領域はもうプログラムが自由に使ってよい場所ではありません。

しかし、ポインタ変数pEnergyRate自体はまだ残っています。

ここが少し分かりにくいところです。

状態pEnergyRateの状態配列領域の状態
new直後有効な領域を指している使える
delete[]直後解放済みの場所を指している可能性がある使ってはいけない
pEnergyRate = NULL後何も指していない使ってはいけない

delete[]を実行しても、pEnergyRateという変数そのものが消えるわけではありません。

しかし、pEnergyRateが指していた先のメモリは解放済みです。

そのため、delete[]後にpEnergyRate[i]へアクセスしてはいけません。

ドラゴンボール風に言えば、修行メーターを片付けたあと、まだ座標だけを覚えているからといって、そこにあるはずのメーターを読もうとしている状態です。

もうメーターは存在しないので、正しい値を読むことはできません。

修正前と修正後の違い

修正前と修正後の違いは、delete[]の位置です。

修正前の危険な流れ

pEnergyRate = new double[5];

for (int i = 0; i < 5; i++) {
    pEnergyRate[i] = i / 10.0;
}

delete[] pEnergyRate;

for (int i = 0; i < 5; i++) {
    cout << pEnergyRate[i] << " ";
}

この流れでは、配列を解放したあとに表示しています。

修正後の安全な流れ

pEnergyRate = new double[5];

for (int i = 0; i < 5; i++) {
    pEnergyRate[i] = i / 10.0;
}

for (int i = 0; i < 5; i++) {
    cout << pEnergyRate[i] << " ";
}

delete[] pEnergyRate;
pEnergyRate = NULL;

この流れでは、表示が終わったあとに配列を解放しています。

比較修正前修正後
値の代入
表示の前にdelete[]しているしていない
表示処理解放後に実行している解放前に実行している
安全性危険安全
期待結果得られない可能性がある得られる

pEnergyRate[i]の意味

今回のプログラムでは、pEnergyRate[i]を使って配列の各要素にアクセスしています。

pEnergyRate[i] = i / 10.0;

iが0のとき、pEnergyRate[0]に0が入ります。
iが1のとき、pEnergyRate[1]に0.1が入ります。
iが2のとき、pEnergyRate[2]に0.2が入ります。

i計算式代入される値
00 / 10.00
11 / 10.00.1
22 / 10.00.2
33 / 10.00.3
44 / 10.00.4

10.0のように小数を使っているため、結果も小数として計算されます。

もし10のように整数で割ると、整数同士の計算になり、期待した小数にならない場合があります。
今回のように小数値を作りたいときは、10.0のようにdoubleとして扱える値を使います。

配列メモリは使い終わるまで解放しない

C++の動的配列では、次の順番を守ることが大切です。

生成 → 代入 → 利用 → 解放

今回のプログラムなら、次のようになります。

段階コード内容
生成pEnergyRate = new double[5];double型5個分の配列を確保
代入pEnergyRate[i] = i / 10.0;各要素に値を入れる
利用cout << pEnergyRate[i];各要素を表示する
解放delete[] pEnergyRate;配列メモリを解放する

delete[]は、最後の段階です。

配列をまだ使う予定があるうちは、delete[]してはいけません。

図:修正前と修正後の流れを比較する

この図が示していること

この図では、修正前と修正後の処理順序の違いを比較しています。

修正前は、delete[]で配列を解放したあとに表示しようとしているため危険です。
修正後は、配列の値を表示してからdelete[]しているため安全です。

同じnew[]とdelete[]を使っていても、書く順番が違うだけで、プログラムの安全性が大きく変わります。

delete[]とpEnergyRate = NULLの役割

修正後のプログラムでは、最後に次の2行を書いています。

delete[] pEnergyRate;
pEnergyRate = NULL;

delete[] pEnergyRate;は、ヒープ領域に確保した配列メモリを解放する処理です。

pEnergyRate = NULL;は、ポインタ変数がもう有効なメモリ領域を指していないことを明確にする処理です。

コード役割
delete[] pEnergyRate;配列メモリを解放する
pEnergyRate = NULL;ポインタを何も指していない状態にする

delete[]だけでもメモリは解放されます。
ただし、pEnergyRateには解放済みのアドレスが残っている可能性があります。

そのまま誤ってpEnergyRate[i]を使ってしまうと危険です。

pEnergyRate = NULL;としておくと、もう使えないポインタだと分かりやすくなります。

この練習問題で確認したいポイント

この練習問題では、4章で学んだ動的メモリ管理の大切なポイントを確認しました。

確認ポイント内容
new[]配列メモリをヒープ領域に確保する
pEnergyRate[i]配列の各要素にアクセスする
delete[]new[]で確保した配列を解放する
解放後アクセスdelete[]後に配列へアクセスしてはいけない
処理順序生成 → 代入 → 表示 → 解放
NULL代入解放後のポインタを分かりやすくする

C++では、メモリを自由に確保できる一方で、使い終わったメモリを正しく解放する責任があります。

今回の問題では、delete[]自体は書かれていました。
しかし、書く場所が早すぎたため、解放後に配列へアクセスする危険な状態になっていました。

つまり、delete[]は「書けばよい」だけではありません。
配列を使い終わったあとに書くことが大切です。

ドラゴンボール風に言えば、修行メーターを読み取る前に片付けてはいけません。
戦闘力や気力安定率を確認してから、最後にメーターをきれいに片付ける必要があります。

C++でも同じように、配列メモリは、使い終わるまで保持し、最後にdelete[]で解放します。

この順番を守れるようになると、new[]とdelete[]を使ったメモリ管理がかなり安全に扱えるようになります。