パソコン活用研究シリコンバレー(C、C++、の活用研究)

メモリの動的割り当て(malloc,free)

1 メモリの動的割り当て
初心者にとっては”動的なメモリー割当”とはすごく聞きなれない言葉ではないでしょうか。なんの
ことやら理解しにくい日本語で、これだけで頭が思考停止になってしまいそうです。しかし、慣れて
しまえば、どうってこともなくなりますので、最初だけ我慢してみて下さい。

動的割り当てに対して、”静的割り当て”という方法もあります。静的割り当ての代表的なものと
しては、配列があります。これならなじみが深いものなので、すぐに理解できるでしょう。”静的な”
という意味は、この場合、プログラム起動時にあらかじめ固定した大きさのメモリを確保するという
ことになります。
例えば、char s[1000][100]
という配列を宣言すると、1000*100=約100K byteのメモリがこの配列用に確保されます。
このような静的なメモリ割り当ては、
・余ってしまった時に無駄
・もっと必要になった時に拡張できない
といったデメリットがあります。

メモリの静的割り当てには上記のようなデメリットがあるため、配列ではうまく対応できないプログラム
があります。例えば、社員の氏名、住所を管理するプログラムで、社員数が10名の会社で使われるか
10万人の会社で使われるかわからない場合、配列を10万個用意するのはいかにも、無駄です。
このような場合には、プログラム実行中に必要に応じてメモリーを確保していく方法が適しています。
このプログラム実行中に必要に応じてメモリーを確保していく方法が、”動的なメモリー割当”です。
また、リスト構造自己参照構造体を使うような場合では、データ数がいくつになるかわからない
ことが多いので、通常動的なメモリ割り当てが使われます。

動的なメモリ割り当てには、一般的にはmalloc(),calloc(),realloc(),free()関数を使用します。
しかし、mallocによるメモリの動的割り当てにも、以下のようなデメリットがあるので、動的なメモリ割り当て
にするか、配列等で静的なメモリ割り当てにするかは、それぞれのプログラムの内容次第で選択する
必要があります。

・メモリを使う度に malloc を呼び出して獲得し、戻り値を NULL であるか確認したり、また、使い
 おわったら free するなどの処理が必要となり、面倒。またfreeし忘れてバグの元になることもある。
・非常に小さいサイズのメモリを多数使いたい場合には、 malloc は管理用の情報が必要な分、
 不経済な使い方になる。例えば、あと 100バイトメモリが空いている時に、1バイトの領域を100個
 取れるわけではない。

2 malloc
(1) 書式と基本的な使用例
書式を以下に示します。

【書式】
void *malloc( size_t size);

sizeに割り当てるサイズをバイト単位で指定します

戻り値
成功時 : 確保したメモリブロックを指すポインタ
失敗時 : NULL (メモリ不足により指定サイズ分のメモリが確保できないとき)
malloc()から受け取ったポインタはNULLかどうかを調べてから使用するのが一般的です

(2)基本的な使用例
mallocによるメモリの動的割り当ての基本的な使用例です。この使用例では、メモリの動的割り当ての
メリットは特にありませんが、使用方法を体得のために一番シンプルな例を書きました。
この例では、文字列のためのメモリを確保し、そのメモリに文字列を読み込んでいます。

mallocは確保したメモリブロックを指すポインタを返すので、通常このvoid型のポインタをキャストして
ポインタ変数でうけます。ここではchar型のポインタ変数strでうけています。
またmallocはメモリが確保できなかった時はNULLを返しますので、この例のように、if文で判定し、メモリの
確保に失敗した時の処理をしてやるのが常道です。
最後に使いおわった領域をfreeで開放します。

【使用例】
#include <stdio.h>
#include <stdlib.h>

int main(  )
{
   char *str;

   /* 文字列のためのメモリを確保 */
   str = (char *)malloc(100);
   if(str == NULL) {
      printf("メモリが確保できません\n");
      exit(1);
   }

   /* 文字列を入力 */
   gets(str);

   /* 文字列の表示 */
   puts(str);

   /* メモリの解放 */
   free(str);

   return 0;
}

(3) 確保するサイズの指定および確保したメモリの利用
上記の例ではmalloc(100) として100byteの領域を確保しましたが、例えばint型の領域を50個分
確保したい、というような時は、
int *n;
n = (int *)malloc(sizeof(int) * 50);
のようにsizeof演算子を使うのが常道です。処理系により、int型、short型、long型などサイズが違う
ことがあるので、sizeof演算子を使えば不用意なバグの発生をおさえられます。
例えばint型が2byteの処理系では、malloc(100) としても、malloc(sizeof(int) * 50)としても、どちらも
100byteのメモリが確保されますが、malloc(sizeof(int) * 50) としておいたほうがいいでしょう。

次に、その確保したメモリをどう利用するか、について簡単に説明しておきます。
malloc(100) として、100byteのメモリが確保した場合、上記のように(int *)mallocとしてint型にキャスト
すると、int型が2byteの処理系では100byteのメモリは、2byte * 50個として利用することになります。
もしlong型が4byteで、(long *)maloocとすると、100byteを 4byte * 25個として利用することになります。

例えば、100byteを確保した場合、int型が2byte、long型が4byteの処理系では、その100byteを以下の
ように利用することになります。

char *c;
int *n;
long *l;
c = (char *)malloc(100);
n = (int *)malloc(100);
l = (long *)malloc(100);

char型(1byte) int型(2byte) long型(4byte)
c[0] n[0] l[0]
c[1]
c[3] n[1]
c[4]
c[5] n[2] l[1]
c[6]
c[7] n[3]
c[8]
... ... ...
c[96] n[48] l[24]
c[97]
c[98] n[49]
c[99]

それぞれの値にアクセスするときには、配列ではなくポインタを使って、*c, *(c+1), *(c+2),*n, *(n+1)
というようにしても構いません。

3 malloc使用上の諸注意
使い慣れないと、うっかりやってしまいがちな間違いの代表例を2,3あげておきます

(1) freeに渡すポインタが違う
free関数に渡す値はmallocが返す戻り値をそのまま渡さなければいけないのですが、戻り値を受けた
ポインタ変数の値をうっかり変更してしまうというミスがありがちです。
本来とは違う領域をfreeで解放することになるので、バグ発生の要因となります。
また、本来の領域は解放されないまま残りますので、いわゆるメモリーリークの原因になったりします。

例えば、以下のようなコードです。どこがまずいでしょうか。 
char *buff;
int i;
buff = (char*)malloc( sizeof( char ) * 100 );

for( i=0 ; i<100 ; i++ ){
    *buff = '0';
    buff++;
}

free( buff );

上の例では、buffに代入されたmallocで得た戻り値の値を buff++ の部分で変更してしまっています。
buffの値を変更するというのは別に構わないのですが、freeに渡す前に元の値に戻す必要があります。
しかし、変更したポインタを後で元に戻すというような操作は面倒だし、バグの元になりやすいので、通常
は別のポインタ変数にアドレスを代入しmallocが返した戻り値は直接変更しないようにします。

以下のようにすれば問題なしです。buffはいじらずに、workに代入しworkの値を操作します。

char *buff,*work;
int i;

buff = (char*)malloc( sizeof( char ) * 100 );
work = buff;

for( i=0 ; i<100 ; i++ ){
    *work = '0';
    work++;
}

free( buff );

(2) 文字列のために確保する領域のサイズが違う
取得した文字列分のメモリをmallocで確保する、というのは非常によくあるパターンですが、
以下のような間違いをしやすいケースです。どこがまずいかわかるでしょうか。

char *get_string(char *string)
{
char *p;
size_t len;

len = strlen(string);
p = malloc(len);
strcpy(p, string);
return p;
}


上記のプログラムはmallocで確保する領域の大きさが足りません。 strlen の戻す値(すなわち
文字列の長さ)には文字列の終了を示す '\0' を含んでいません。 strcpy を実行すると、この '\0'
も複写しようとするので、どこかおかしな所に '\0' を書き込んでしまうことになります。

すなわち、ここでは'\0'の分を加えて、 malloc(len + 1) としなければなりません。うっかりして
いると、このミスは結構よくやります。特に普段はBASICでプログラムを組んでいるとうっかりやって
しまう代表格のミスかもしれません。

ここの処理は、次のように書くこともできます。
len = strlen(string) + 1;
p = malloc(len); 
コードとしてはこれも正しいのですが、あとあと、コードがとても分かりにくくなりますし、プログラムを
改変する時にバグを起こしやすくなるかと思います。一般的には、文字列の長さを保持する変数lenは
文字列の長さが保持されていると考えますので、strlen が戻した値をそのまま保持した方が誤解を
避けることができるでしょう。
おじさん的には、malloc(len + 1) がbetterだと思います。


(3) mallocが領域確保に失敗した時の処理

上で取り上げたコードにはもう一点改善すべき点があります。上のコードは、malloc に失敗した時の
ことを考えていません。つまり、もしメモリが殆ど使われてしまっていたら、 malloc は新たな空きメモリ
領域を確保できない可能性があります。メモリが確保できないと mallocNULL を戻します。
従ってmallocの戻り値のチェックが必要です。

NULL が戻ってきた場合はメモリは獲得されていません。 strcpy の引数に NULL を渡してしまうと、
おかしな結果になります。NULLかどうかチェックしてNULLでない場合のみsrtcpyに値を渡すようにします。

また、引数が NULL であった場合のことも考えていません。しかし、この場合は、NULL の時にはこの関数を
呼ばないようするという手段で回避することもできます。

これらの問題を考慮して書き直すと、例えば次のようになります。これがmallocを使用する場合の標準形
の書き方になります。(とは言っても、自分で使う小さなプログラムではmallocの戻り値のチェックなんか面倒
で省くことが多いと思いますが。)

char *get_string(char *string)
{
    char *p = NULL;
    size_t len;

    if (string != NULL) {
        len = strlen(string);
        p = malloc(len + 1);
        if (p != NULL)
            strcpy(p, string);
    }

    return p;
}


4 free, exit
mallocで確保した領域を使い終わったあとに開放するのがfreeです。freeで開放することにより、
その領域を他の用途で使用することができるようになります。

【書式】
free(p);   pはmallocで確保した領域の先頭アドレス=ポインタ

なお、確保した領域がプログラムの終了まで使われる場合はfreeする必要はありません。
プログラムの終了と同時に自動的にその領域は開放されるようになっています。

freeの注意点ですが、freeは返り値をもたず、エラー管理をしていません。従って、freeに記述
する引数(上記の書式のp)に正しいポインタを渡さないと、メモリ管理がおかしくなります。


ついでに、プログラムを強制終了するexitも、mallocで確保した領域を開放します。


5 ヒープ領域

蛇足ながら、mallocで確保するメモリというのは、どこから確保されるのかについて、ない知識を
ふりしぼりつつ、ちょっと触れておきます。Cの処理系によってmallocがどういう仕組みで、どこから
メモリを確保するのかは異なるのですが、MS-DOSやWindows上のCでは、ヒープ領域と呼ばれる
領域から確保されます。
ヒープ領域というのは、プログラムが起動して確保されたユーザー領域のうち、プログラムの実行コード
自体がおかれる領域、スタック領域、静的変数領域・・などを除いたその他残りの領域です。
mallocで確保した領域をfreeで開放すると、ヒープ領域に”使用してない領域”として返されて管理
されます。この返された領域は、次のmallocでまた使用することが可能です。

というわけで、mallocしたりfreeしても、OSから領域を切り取ってきたり、またOSに返したりしている
わけではないので、OS上の未使用領域が増えたり減ったりするわけではないようです。Linux上の
CではfreeするとOSにその領域を返すような動きになるものがあるようですが・・・。

TopPage