第十一回-02 new 演算子によるメモリの動的確保



配列の定義再考 (1) ~配列サイズは定数でなければならない

第十一回-01の例で、配列の宣言をしている以下の部分に着目しよう。

int array[5];  // 要素数 5 の配列を宣言
int n=5;       // 配列の要素数

Visual Basic でも、このような記述を何度かしてきた。 (例えばこちら)

しかし、この記述は何か奇妙だと思わないだろうか?例えば、配列の要素数を 10 に変えたいと思った場合、
1 行目と2行目に含まれている2箇所の 「5」を「10」に変更せねばならず、無駄が多い。

むしろ、プログラムの美しさを考えれば以下の記述の方が自然だと思うかもしれない。

int n=5;       // まずは配列の要素数を決める
int array[n];  // 要素数 n の配列を宣言 (しかしコンパイルエラー!!)

しかし、この記述は「int array[n];」 という行に対して以下のコンパイルエラーが出る。



この1行目にエラーの理由が書かれているが、
配列の宣言の際、配列のサイズは定数でなければいけないというルールが守られていないため、エラーが出るのである。

「int array[n];」の n は、すぐ上の行で n=5 と値が定まっていると思うかも知れないが、
文法上は n は変数であるため、定数とは見なされないのである。

ではどうすれば良いか。C++ では定数の書き方が決まっていて、
以下のように n を const (定数:constant) として記述すれば、コンパイルエラーも出ずに実行できる。

const int n=5; // まずは配列の要素数を定数として定める
int array[n];   // 要素数 n の配列を宣言 (これなら n は定数なのでコンパイルは通る)

なお、こうすると n は文字通り定数 (constant) となるので、値を変更することはできない

(補足)
なお、C 言語では上記の記述を以下のように書くことが多かった。
「#define SIZE 5」とは「SIZE という文字列を強制的に 5 に置き換える」という意味である

#define SIZE 5

int main(int argc, char *argv[]){

    int array[SIZE];   /* 要素数 SIZE の配列を宣言 */

    int i;
    for(i=0 ; i<SIZE ; i++){
       /* 何らかの処理 */
    }




配列の定義再考 (2) ~配列サイズはコンパイル時に確定していなければならない

上の例で、「配列サイズは定数で指定しなければならない」ことはわかった。
では、次にプログラム実行中に配列のサイズを決めることはできないかを考えよう。

例えば以下のような例。プログラム実行中に、キーボードから n の値を入力させ、 その n のサイズの配列を確保しようとしている。

int n;       // 配列の要素数として用いる

std::cout << "配列の要素数を入力してください : ";
std::cin >> n;  // この行は初めて登場するが、「コンソールから入力された値を n に代入する」という意味である
std::cout << "読み込まれた n の値は" << n << "です\n";

int array[n];  // 要素数 n の配列を宣言 (n は定数ではないのでコンパイルエラー!!)

しかし、n は 定数ではなく変数であるのでもちろんコンパイルエラーになる。

じゃあ、と思って n を const 宣言するとどうなるか。

const int n;       // 配列の要素数として用いる

std::cout << "配列の要素数を入力してください : ";
std::cin >> n;  // 今度は定数 n を変更しようとしてコンパイルエラー!
std::cout << "読み込まれた n の値は" << n << "です\n";

int array[n];  // ここまで到達しない)

今度は定数 n の値を変更しようとしてコンパイルエラーになる。
定数は宣言時に初期化しなければならないからである。

結局わかったことは、ここまでの知識では、配列のサイズはコンパイル時に確定していなければならないということである。

しかし、「配列のサイズはコンパイル時に確定していなければならない」という制約は 非常に厳しい制約である。
例えば、第三回-02 配布したGUIアプリケーションの概略で学んだように、画像処理アプリケーションでは画像データを配列に格納していた。

「配列のサイズはコンパイル時に確定していなければならない」ということをこの画像処理アプリケーションの例で言い替えると、
「画像のサイズはプログラムを作成するときに確定していなければならない」ということである。

つまり、ここまでの知識で画像処理アプリケーションを作ったとすると、 「ある決まった大きさの画像しか開けないアプリケーション」になってしまう。
(より正確に言うと「ある決まった大きさより小さい画像しか開けないアプリケーション」だが)
そんなアプリケーションは使い物にならないことはすぐにわかるだろう。

ここまでの説明でプログラム実行中に配列のサイズを決めることの重要性がわかってもらえただろうか。
そして、それを実現するためにポインタが関わっている、というわけである。


new 演算子によるメモリの動的確保

前置きが長くなったが、早速「プログラム実行中に配列のサイズを決める」例を見て行こう。
このことを「配列を動的に確保する」と呼ぶこともある。



ポイントは の4つである。

このとき、ポインタ a_heap と new 演算子により確保される配列のメモリ上での模式図は以下のようになる。
ポインタ変数 a_heap 自体は自動変数であるので今まで通りスタック領域に配置されるが、
new 演算子により確保されるメモリは下図のようにヒープ領域に確保されるというところがポイントである。



そして new 演算子により確保された配列 a_heap を使い終ったら delete[] a_heap によりヒープ領域のメモリを解放しなければならない

その理由を、やや細かくなるが解説しよう。
一般に、あるプログラムで使われるメモリ領域は、スタック領域、ヒープ領域を問わずプログラム終了時に自動的に解放される。
なので、即座にプログラムが終了する場合は、メモリの解放し忘れが深刻な自体を引き起こすことは少ない。

しかし、数時間とかあるいは数日間動き続けるアプリケーションがあったとしよう。
そして、ある関数 func から new 演算子によりヒープ領域から配列が確保されたとしよう (下図の上)。



もし、関数 func が終了される時に、delete[] により配列が解放されなかったとすると、
上図の下のように、ヒープ領域上の配列は、誰にも参照されない (使われない) ままプログラム終了まで残り続けることになってしまう。
このような事態のことをメモリリークと言い、C/C++ のプログラミングでは避けねばならない事態である。

しかし、複雑なプログラムになるほどメモリリークは発見しにくいトラブルであると言える。

さて、以上で見た「new でメモリを確保し、delete で解放」という手続きは C++ では常套手段である。
(C の場合 new/delete の組合せは malloc/free であったが、ここでは省略する)
配列の確保だけではなく、クラスのオブジェクトを new/delete することも頻繁に行われる。

このように、プログラムで使用するメモリを自分で管理するのが C/C++ でのプログラミングの特徴である。

(補足)
ここで見たように、C/C++ では使用するメモリは自分で管理するのが特徴なのだが、上記にあるように、
プログラムが複雑になればなるほど、メモリリーク (メモリの解放し忘れ) のバグを引き起こしがちである。

その問題を解決するため、C++ より後に作られた Java や C# という言語では
  • ポインタを廃止する (ただし C# ではポインタを使う手段が残されている)
  • メモリの解放をシステムに任せる (delete が存在しない。自動的なメモリ解放のことをガベージコレクション (GC) と言う)
という点に大きな特徴がある。

このことを、上の「int 型の配列確保」を例に用いて解説しよう。

C++ では配列の確保の方法は以下の二通りがあることはこれまで学んできた。

int array1[5]; // C++ : 方法1

int *array2; // C++ : 方法2
array2 = new int[5];

一方、Java や C# では配列を用いる際に以下の方法を用いる。

int[] array; // Java/C# : 方法
array = new int[5];

注意深く観察すると、Java/C# の方法は「C++ : 方法2」から生まれた記法であることが分かるだろう。

また、Java や C# では (原則として) ポインタは存在しないので、宣言が「int[] array;」 となっているが、
ポインタを理解した後でこの記法を見ると、ポインタは存在しないのではなく、
単にユーザーから隠蔽しただけであることもわかる。

また、メモリの解放については、

delete[] array ; // C++

(書かないくて良い) // Java/C#

という違いがある。
ただし、Java/C# では array = null; のように array にヌルポインタを代入して、
array がどこも指さないようにする、ということはしばしば行われる。

C++ のメモリ管理を体験した後で Java/C# に移行すると、メモリ管理をシステムに任せることができるのは
非常にプログラミングが楽になる、ということが実感できる。

しかし個人的には、Java/C# からプログラミングを学ぶ者でも、
この講義で学ぶ程度のメモリ管理の知識は最低限身につけて欲しいと考えている。
それは、これらについて知っていた方が、効率的なプログラムを書けると考えられるからである。





←第十一回-01 配列を指すポインタ第十一回-03 関数に配列へのポインタを渡す→

非情報系学生のための C/C++ 入門に戻る