第42回 プログラミングについて『動的にメモリを確保してみよう』

C言語には自動変数というものがあって、関数やブロックの開始時にメモリを確保することができます。このときに確保されるメモリはスタックと呼ばれる領域が使用されます。自動変数の場合はプログラムのソースに記述されている量しか使用することができません。
abc()
{
int data[100];
:
:
}
このように記述すると、int 型のサイズが4のときには関数 abcが呼び出されたときに変数 data 用にスタックを400バイト使用します。配列の宣言で要素数を大きくすれば当然大量のメモリを確保することができるのですが、現実はそう簡単にもゆかないのです。UNIXやVMSではうれしいくらいに大量のスタックを確保しようとしても叱られません(昔のUNIXでは64kバイトまでしか確保できないものもありましたが)。しかし、DOSやWindows3.1では情けないくらいに少ないスタックしか確保することができません。
もっとも今となってはWindows95が一般的になり、Windows95上で32ビットのコンパイラでDOSのプログラムを作ると、DOS窓の中で実行してもメモリ空間が32ビットになりますのであまりメモリに対して苦労することがなくなってしまいました。ですので今回はたくさんのメモリを確保することは目的とはせず、効率よくメモリを使いスマートにプログラムを書くことを目的として話を進めていきましょう。
スタックを使って配列のメモリを確保する場合その要素数が固定になってしまいます。例えばあるテキストファイルソートする場合、テキストの最大行数を規定できるのであればそれを要素数とすれば話は簡単になります。例えばその最大行数を1万行としたとします。ところが実際にプログラムで使用するテキストファイルはたかだか100行程度のものが殆どだったとすると、確保したメモリの殆どが誰も使わない広い空き地になっている訳です。もっともシングルユーザー/シングルタスクのシステムでは誰も文句は言いませんが、マルチユーザのシステムとなると馬鹿のようにメモリを食うプログラムを動作させると、「誰だ変なプログラムを動かしているのは!」とコンピュータではなく、人間に叱られてしまいます。
そこでプログラムの実行時に必要に応じてメモリを確保する必要が生じるのです。
FORTRAN77ではポインタというものがありませんので、OSからメモリを確保してもそれを利用するのは困難が伴います。昔、VAX/VMSでそれをやってみたのですが、かえって変なプログラムになってしまい途中であきらめてしまいました。動的にメモリを確保するVMSのシステムコールは忘れてしまったのですが、
INTEGER DATA
DATA=SYS$ALLOCATE_MEMORY(40000) !ここで40000バイト確保すると
!します。この SYS$ALLOCATE_MEMORY
!関数は架空のものです。
このように確保したメモリのアドレスを変数 DATA に代入したとします。ここまでは簡単なのですが、この確保したメモリに値を代入しようとしてもこのルーチン内では行なえないのです。変数 DATA はあくまでも整数型の変数ですので、DATA=10 などとやってしまうと、せっかく確保したメモリのアドレスが分からなくなってしまうのです。そこで、この40000バイトをサイズが4バイトの整数の配列とたときに20個目の要素に100という値をどうやって代入するかと言えば、変数 DATA の値を引数にして他の関数にそれをやってもらうのです。
CALL PUT(%VAL(DATA),20,100)
ここで %VAL はVAX FORTRANの組み込み関数でアドレスではなく値を引数として渡す機能を提供するものです。よびだされる PUT を、
SUBROUTINE PUT(DATA,N,IVALUE)
INTEGER DATA(*)
DATA(I)=IVALUE
RETURN
END
と普通に配列に値を代入するように記述するのです。これで実現できるのですが、これでは結構つらいものがあります。
FORTRAN77に比べてC言語ではポインタという便利なものがありますので、格段に簡単になります。
#include <stdlib.h>
#include <memory.h>
:
:
int *data; -(1)
data=(int *)malloc(sizeof(int)*10000); -(2)このように記述すれば、int型1万個分のメモリを確保できます。
(1) を int data[10000]; としてはいけません。このときには自動変数として関数の開始時にメモリが確保されてしまいますので、(2) はコンパイラに叱られてしまいます。
C言語ではポインタを配列のように記述してもいいので、
data[19]=100;
とすれば簡単に20個目の要素に100を代入できるようになります。当然、
*(data+19)=100;
としても同じことになります。
この malloc 関数は確保したメモリの型は void * として返しますので、ポインタ変数にそのアドレスを代入するときは正しくキャストしなければなりません。ですから、int 型を示すポインタの配列として確保するときには、
int **data;
data=(int **)malloc(sizeof(int *)*10000);
としなければなりません。
次の例を見てみましょう。
sub()
{
int *data;
data=(int *)malloc(sizeof(int)*10000);
}
この関数は問題なく動作するのですが、1つ重要なことを忘れています。この関数はメモリを確保した後、そのまま呼び出し側に戻りますので、その確保したメモリはそのまま確保されたままになっています。もしこの関数を100回呼び出すと、400万バイトのメモリを確保することになります。 malloc 関数で確保したメモリは自動変数ではありませんので、関数の終了時に消滅する訳ではありません。ですので不要になったメモリは責任を持って開放してあげなければなりません。メモリを開放するには、free関数を使用します。上の例では、
sub()
{
int *data;
data=(int *)malloc(sizeof(int)*10000);
free(data);
}
とすることで開放することができます。明示的にメモリを開放しないときでもプログラムが終了すればOSがプロセスの終了とともに確保されたままになっているメモリを開放してくれますので、この確保と開放のことを理解していれば明示的に開放するべきものと、プロセスの終了時にOSに開放させるものと区別してプログラムを書いても構いません(でも注意して行なって下さい)。
malloc関数を使って構造体用のメモリを確保するのも基本的には上の例と同じやり方で行ないます。
struct sample_strct{
char *name;
int id;
struct sample_strct *next;
};
struct sample_strct *data;
data=(struct sample_strct *)malloc(sizeof(struct sample_strct));
これで、sample_strct 型1つ用のメモリが確保されます。確保されたこのメモリのメンバー name に "abcde" を、 id に100を代入するには、
(*data).name="abcde";
(*data).id =100;
(*data).next=NULL;
とすることができますが、どちらかというとこの記述方法は次のようにするのが一般的のようです。
data->name="abcde";
data->id =100;
data->next=NULL;
どちらにしても1つだけ注意しなければならないのは、data->name="abcde"; の部分です。この式は文字列 "abcde" のアドレスを代入しているだけですので、実際に文字列を代入するには、
data->name=(char *)malloc(strlen("abcde")+1);
strcpy(data->name,"abcde");
とする必要があります。
malloc 関数の引数と戻り値の型のキャストを記述するのは結構面倒くさいので、次のようにマクロを定義しておくと楽になります。
#define NEW(type,element) (type *)malloc(sizeof(type)*(element))
このマクロを使うと上の例の
data=(struct sample_strct *)malloc(sizeof(struct sample_strct));
は、
data=NEW(struct sample_strct,1);
と記述することができますので、同じような記述をする必要はなくなります。
data->name=(char *)malloc(strlen("abcde")+1);
strcpy(data->name,"abcde");
も、
#define PUTSTR(variable,string) \
(variable)=(char *)malloc(strlen((string))+1); \
strcpy((abc),(string))
とマクロを定義しておけば、
PUTSTR(abc,"abcde");
と簡単になりますが、こればマクロではなく関数にしても同じことができますので好みで使い分けて下さい。
C言語の利点の1つはポインタがあることで、動的に確保したメモリを簡単に扱うことができることです。FORTRANに慣れた人はどうしても大きな配列を作りがちですが、メモリの有効利用と最大数の制限の点ではC言語で書くという理由が半減するような気がします。実際私も最初の頃は大きな配列を作っていましたが、これはUNIXなどのようなメモリを湯水のように使えるOSでは問題がなくても、DOSやWindows3.1のようにスタックサイズが小さいOSでプログラムを書くようになってからは、大きな配列が作れないのでだんだんとプログラムのスタイルが変化してきました。
特にWindows3.1で数メガのデータを扱うとなるとプログラムを書くのも骨の折れることで、near ポインタとか far ポインタとかポインタのサイズや性格も考慮しなければなりませんでしたからなおさらでした。どちらにせよとにかく慣れることが肝心です。慣れてしまえば面倒なこともそうではなくなってしまうのです。
それではまた次回。