前回は構造体の配列を使って,簡単なロール・プレイング・ゲームを作ってみました。お楽しみいただけたでしょうか? 今回は構造体を発展させていきます。動的にメモリーを確保して構造体を生成していく方法,そして線形リストへと解説を進めます。C言語で本格的なプログラムを記述するためには,ぜひ覚えておいてほしいテクニックです。最後までお付き合いください。
動的なメモリーの確保が必要な理由
まず,動的なメモリーの確保とは何か,またなぜそのようなテクニックを使う必要があるのかを説明しましょう。
前回は構造体の「配列」を扱いましたが,配列の不便なところは,要素の数を変化させることができない点にあります。使わなくてもメモリー上に場所をとるし,足りなくなっても追加するすべがありません。
学生寮の夕食をイメージしてみましょう。配列というのは,「今日もたぶん,100名くらいが夕食を食べにくるだろう」と予測して食事の用意をするようなものです。80名しか来なかったら,余ってもったいないですが,100名分きっかりしか作らないと,101番目に来た人には「ごめん,もうなくなった」と断るしかありません。足りなくなってはどうしようもないので,通常は予想した数より,いくらか余裕をみた要素数の配列を用意します。
要素数を予想できる場合はそれでいいでしょう。しかし,推測しにくいものや上限を決めたくない種類のデータもありますね。例えば,何かの会や組織の会員数です。当初の会員は20名でも,目標は2000名となると配列では無駄が多すぎます。そんなときに「動的なメモリーの確保」というテクニックを使えば,その都度,適切な量のメモリーを取得して使用することができます。
リスト1は,配列を使って5人分のテストの点数を入力してもらい,5人の点数を表示しながら合計と最高点を求め,最高点と平均を表示するプログラムです。一方リスト2は,同じ処理を「動的なメモリーの確保」を使って行うプログラムです(図1)。何人分のデータを入力するかとたずねてから,メモリー・ブロックを確保していますね。前もって夕食の予約を取ってから,作るようなものです。
|
リスト1●配列を使って5人の成績を表示するプログラム |
リスト2●リスト1のプログラムを「動的なメモリーの確保」を使って人数を設定できるように修正したプログラム |
図1●リスト2を実行したところ |
リスト2は,malloc関数を使ってメモリー領域(メモリー・ブロック)を確保し(1),ポインタを使って操作している点がリスト1とは異なります。mallocは指定したサイズのメモリー領域を確保する関数で,確保できた場合,その領域の先頭へのポインタを返します。領域の確保に失敗した場合はNULLを返します(2)。
ここではっきり意識してほしいのは,配列は要素のデータ型と数を最初に決めて宣言するので,同じ型のデータを格納できる箱を複数作成するイメージですが(図2の(a)),動的なメモリーの確保はメモリーのかたまりを自分のプログラムで自由に使える領域として確保しているだけ,という点です(図2(b))。動的にメモリーを確保した関数が返すメモリー領域のアドレスを,「(int *)」のようにint型へのポインタにキャスト(型変換)して,ポインタでメモリーを区切って使っているのです。サクどりされたマグロを買ってきて,都合の良い大きさに切って使うようなものですね。「(ninzu*sizeof(int)」で確保した領域のポインタを「(char *)」でキャストして,char型の値を20個入れる領域として使うこともできます*1。
図2●リスト1とリスト2でメモリーを確保したときのイメージ |
連載の4回目で説明したように,メモリーはヒープ,データ・セグメント,コード・セグメント,スタック3の四つの部分にエリア分けされています。関数の引数やローカル変数はスタックに格納されるのでしたね。一方,malloc関数などで,動的に確保するメモリー領域はヒープに作成されます。ヒープに作成されるということは,ローカル変数とは異なり,プログラムのどの部分からでも参照できるということになります。
動的なメモリーの確保に使用する四つの関数
では次にリスト3を使って,動的なメモリーの確保に使用する標準ライブラリ関数を見ていきましょう。malloc,calloc,realloc,freeの四つです(表1)。これらの関数を使う場合は,リスト3の(1)のようにstdlib.hをインクルードすることを忘れないでください(インクルードしないとコンパイル・エラーになります)。
リスト3●動的なメモリーの確保に使う関数の使用例 |
表1●動的なメモリーの確保に使用する関数。ヘッダー・ファイルstdlib.hをインクルードする必要がある。*size_tはヘッダー・ファイルの中で,unsigned intと宣言されている |
(2)のmalloc関数はリスト2でも出てきたように,メモリー領域を確保する関数ですが,実はcalloc関数を使っても同じことを実行できます。malloc関数は引数が一つなので「100*sizeof(char)」のように指定しましたね。一方calloc関数ではこの引数を二つにばらして,(3)のように「calloc(100,sizeof(char))」(100個,char型の大きさのメモリーを)という具合に指定します。calloc関数は,確保したメモリー領域のすべてのビットを0で初期化するという点でmalloc関数とは違います。
どちらの関数を使うにせよ動的なメモリーの確保を行う場合に気を付けなければいけないのは,ヒープは要求する大きさのメモリー領域を必ずしも確保できるわけではないということです。リスト2や3で,NULLポインタが返ってきた場合の処理を記述してあるのはそのためです。
動的に確保したメモリーは,配列とは異なり後で変更できます。それがrealloc関数を使ってメモリー領域の大きさを変更している(4)の部分です。realloc関数を使うときの注意点は,先に確保した領域のサイズを大きくしようとした場合に,現在の領域の先頭アドレスから連続して,必要な領域を確保できないケースがあるということです。
図3●リスト3の(4)でp1のメモリー領域を200バイト広げた結果,p1の先頭アドレスが変わってしまった |
また,realloc関数が返すポインタをいったん,ptという別のポインタ変数に代入してからp1に入れているのは,reallocに失敗してNULLが返ってきたときに,もとのp1の値が失われてしまうのを避けるためです。いままで使っていたメモリー領域のアドレスがわからなくなると,free関数で解放することもできませんからね。realloc関数を使う場合は,このようなテクニックが必要なことを覚えておいてください。