WisdomSoft - for your serial experiences.

9.1 動的メモリ確保

実行するまで必要な記憶容量を確定できないデータを扱う場合、コンパイル時ではなく実行時にシステムから新しいメモリ領域を確保しなければなりません。C 言語では標準関数の malloc() 関数を用いてメモリ領域を確保し、free() 関数で解除できます。

9.1.1 実行時にメモリを割り当てる

通常、変数に割り当てるメモリサイズは静的でした。例えば、文字列を格納する配列を用意する場合、配列宣言時にそのサイズを指定してきました。しかし、配列の宣言にはサイズを整定数でしか指定することができず、変数は指定できません。これは、コンパイル時にプログラムが必要とするメモリサイズが判明していなければならないことを表しています。

しかし、これではハードウェアの性能をフルに生かすことができません。そのコンピュータがギガバイト以上のメモリを搭載していたとしても、コンパイル時に指定した配列のバイト数が64キロバイトであれば、残りの空きメモリを利用することはできません。もちろん、アプリケーションがそれ以上のメモリを必要としなければ問題はありませんが、編集ソフトウェアなどは上限が定まらないという問題があります。

例えば、テキストエディタの場合、コンパイル時にユーザーが入力するデータ量を決定することができません。しかし、これまでの方法ではアプリケーションが上限値を定めなければならないため、編集可能文字数をアプリケーションが強制することになります。これは優れたソフトウェアと呼ぶことはできません。ユーザーは、使用しているコンピュータの性能を十分に発揮できるソフトウェアを使いたいと思うはずです。

そこで、実行時に動的にメモリを割り当てるということが要求されます。動的にメモリを割り当てるというのは、プログラムの実行中に動的に確保する領域を変化させるということです。これができれば、必要に応じたメモリを割り当てることができます。本来、メモリをソフトウェアに提供する役割はシステムが行うものであり、メモリの確保や制御を行いたい場合はシステムが提供する API を呼び出す必要があります。しかし、こうした手法は複雑になり、しかもシステムに依存したコードを記述しなければならないことになります。

そこで C 言語は標準ライブラリにメモリ割り当て関数を提供しています。動的なメモリの割り当てには stdlib.h ヘッダファイルで宣言されている malloc() 関数を使用します。この関数は指定したサイズの領域を確保して void 型のポインタを返します。

malloc() 関数
void* malloc( size_t size );

size パラメータには、割り当てるサイズをバイト単位で指定します。malloc() 関数は、指定されたサイズの領域を確保し、その記憶領域へのポインタを返します。この void 型のポインタをキャストすることで、割り当てたメモリ領域を利用することができます。このように動的に割り当てられるメモリ領域のことをヒープ領域と呼びます。

もしも、要求したサイズを割り当てるだけのヒープ領域が存在しない場合、malloc() 関数はNULLを返します。メモリは有限であり、メモリが過度に消費されている場合は動的に割り当てることができないため、malloc()から受け取ったポインタはNULLかどうかを調べてから使用することが推奨されます。

コード1
#include <stdio.h>
#include <stdlib.h>
#define ALPHABET_COUNT 26

int main() {
	int iCount;
	char *str = (char *)malloc(ALPHABET_COUNT + 1);
	for(iCount = 0 ; iCount < ALPHABET_COUNT ; iCount++)
		str[iCount] = 0x41 + iCount;
	str[iCount] = 0;
	printf("%s\n" , str);
	return 0;
}
実行結果
コード1 実行結果

コード1は、malloc() 関数を用いて 255 バイトの記憶領域を確保し、そのアドレスを char 型のポインタ str に代入しています。これによって、str ポインタは 255 の要素を持つ char 型の配列を指していると解釈することができます。プログラムでは、記憶領域が正しく割り当てられているかどうかを確認するために、配列に値を代入して文字列としてそれを表示しています。0x41 から値を単純増加で代入しているため、ASCII コードの A から順番に文字列が表示されます。

もちろん、malloc() 関数で割り当てた以上の領域にアクセスすることはできません。コード1の場合は str ポインタから 255 以上のアドレスに対してアクセスすることは許可されていません。char 型以外の型を割り当てる場合は sizeof 演算子を使ってバイト数を得るとよいでしょう。int 型の配列へのポインタを malloc() 関数を使って得る場合は sizeof(int) に要素数を乗算することで必要なバイト数を得ることができます。例えば4つの要素を持つ配列を作成する場合は sizeof(int) * 4 を計算することで必要なバイト数が得られます。

しかし、malloc() で割り当てたメモリには通常の変数では発生しない特殊な問題があります。通常のローカル変数であれば、関数から制御を抜け出す時にメモリが解放されますが、malloc() 関数で割り当てたメモリは、プログラムが終了するまで自動的に解放されることはないのです。

コード2
#include <stdio.h>
#include <stdlib.h>

int main() {
	while(1) {
		int *iPo = (int *)malloc(sizeof(int) * 0x100000);
		if (iPo == NULL) {
			printf("メモリが割り当てられませんでした\n");
			break;
		}
		printf("iPo = %p\n" , iPo);
	}
	return 0;
}
実行結果
コード2 実行結果

コード2は while ループ内で malloc() 関数を用いて 1048576 個の要素を持つ int 型の配列を保存するために必要なメモリを連続的に割り当てています。通常のローカル変数であれば、ループが終了するたびにメモリが初期化されますが、malloc() の場合はメモリが解放されるようなことはありません。malloc() 関数で割り当てるメモリは、コンパイル時に決定されている変数とは違って管理対象とはなりません。動的メモリの管理は開発者に委ねられているのです。

コード2の場合、malloc() をループで呼び出すことによって、何度もメモリを割り当てますが、解放されることはありません。しかし、ポインタは失われているため、誰も利用することができないメモリ領域が作られてしまうのです。このようなメモリの流出をメモリリークと呼び、高度なプログラマも頭を悩ますバグのひとつです。

メモリリークの何が問題なのかはコード2を実行すればよくわかるでしょう。メモリを解放することなくメモリを割り当て続けるため、利用可能物理メモリの残量がどんどん減り続けます。実行したコンピュータのメモリが少なければ、メモリ残量がなくなり malloc() は NULL を返してしまうでしょう。これは、銀行が返済見込みのない融資先に融資を続けて不良債権が膨れ上がる仕組みと同じです。

システムから借りたメモリ領域は解放するという形で、いつか返さなければならないのです。そうしなければ、他のプログラムやシステムを正常運用するために必要なメモリが失われてしまいます。短期間しか実行しないアプリケーションの少量のメモリリークであれば問題ありませんが、長期間実行するサーバーなどのプログラムにメモリリークが存在する場合は致命的です。

しかし、残念ながらシステムは借金取りのように「メモリを返せ!」とは警告してくれません。そこで、割り当てたメモリは free() 関数を使って明示的に解放しなければなりません。メモリが不要になれば free() 関数に割り当てたメモリへのポインタを渡しましょう。

free() 関数
void free( void * memblock );

memblock には、解放する以前割り当てたメモリへのポインタを指定します。この関数には malloc() などのメモリ割り当て用関数が返したポインタ以外の値や、すでに解放されたポインタを渡してはいけません。

コード3
#include <stdio.h>
#include <stdlib.h>

int main() {
	int *iPo;
	while(1) {
		int *iPo = (int *)malloc(sizeof(int) * 0x100000);
		if (iPo == NULL) {
			printf("メモリが割り当てられませんでした\n");
			break;
		}
		printf("iPo = %p\n" , iPo);
		free(iPo);
	}
	return 0;
}
実行結果
コード3 実行結果

コード3は、ループの最後で free() 関数を呼び出して割り当てたメモリを解放しています。malloc() 関数と free() 関数が互いに対応しているため、メモリリークが発生することはありません。

しかし、実践の開発では、このようにわかりやすい関係になってくれることはほとんどありません。メモリの割り当てと解放が複数の関数で行われるなど、複雑な構造になることがあります。開発者はどのようなタイミングで free() 関数を呼び出すかを十分に検討し、ポインタの所有権を明確にしておく必要が必要があります。