[←1つ前] [→1つ後] [↑質問一覧] [↑記事一覧] [ホームページ]
malloc, free
動的なメモリの獲得は、C言語ではしばしば使われる手法です。ポインタの理解
が十分なら、
malloc
の使い方を理解することにより、複雑なデータ構造を柔軟に処理することが可能
になるでしょう。あまり慣れない人は、十分な大きさの固定サイズの配列を用意
してしまいがちですが、
malloc
自体はそれほど難しいテクニックを必要としません。
malloc
は何と読むのか。
char *alloc_string(char *string) { char *p; size_t len; len = strlen(string); p = malloc(len); strcpy(p, string); return p; }
strlen
の戻す値、すなわち文字列の長さには、文字列の終了を示す
'¥0'
を含んでいません。
strcpy
を実行すると、この
'¥0'
も複写しようとするので、どこかおかしな所に
'¥0'
を書き込んでしまうことになります。
すなわち、ここでは
malloc(len + 1)
としなければなりません。
このミスは結構よくやります。プログラマーによっては、次のように書く人も います。
len = strlen(string) + 1; p = malloc(len);これは確かに正しいコードですが、
malloc(len + 1)
の書き方と混在していると、とても分かりにくくなるので、どちらかに統一した
方がよいのです。
len
には
strlen
が戻した値をそのまま保持した方が誤解を避けることができるので、
malloc
の時に1大きな領域を取るようにするのが意味としては明解ではないかと思います。
malloc
に失敗した時のことを考えていません。つまり、もしメモリが殆ど使われてしま
っていたら、
malloc
は新たな空きメモリ領域を確保できないかもしれません。メモリを使い果たすと
malloc
は
NULL
を戻すことになっています。その検査が必要です。
NULL
が戻ってきた場合はメモリは獲得されていません。
strcpy
の引数に
NULL
を渡してしまうと、おかしな結果になります。
NULL
であった場合のことを考えていません。もっとも、この場合は、使い方で回避す
ることもできます。つまり、
NULL
の時にはこの関数を呼ばないことを徹底していれば、何の問題もありません。
char *alloc_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; }
malloc
を使ってその都度メモリを獲得しようとするのか。あらかじめ適当なサイズの文
字配列を獲得しておき、使えばよいのではないか。
malloc
を使ってその都度メモリを獲得する利点は、主に二つあります。
malloc
でその都度メモリを獲得すれば、必要なだけメモリを使うことができる。
malloc
を使って必要な期間だけメモリを獲得し、使いおわった時点で
free
を使ってメモリを開放すれば、同じアドレスのメモリを複数の用途に使い回すこ
とができるので、メモリの節約ができる。
malloc
を呼び出して獲得し、戻り値を
NULL
であるか確認したり、また、使いおわったら
free
するなどの処理が必要となり、面倒である。
malloc
は管理用の情報が必要な分、不経済な使い方になるかもしれない。例えば、あと
100バイトメモリが空いている時に、1バイトの領域を100個取れるわけではない。
malloc
で獲得できるメモリ領域とは異なった所から獲得されることがあります。場合に
よっては
malloc
で取れる最大量より少ない限界でパンクするかもしれません。
free
しなければならないのか。あるいは、するべきか。
獲得したメモリを全て
free
してから終了すれば、このような奇妙な現象を回避できる確率は高くなりますが、
完璧であるとは限りません。
いずれにしても、獲得したメモリを
free
しなくても構わない、という考え方の方が一般的であるようです。
(参考: comp.lang.c FAQ 7.24)
free
で開放した直後の領域を使っても値が残っているのではないか。
struct list { struct list *next; char *body; }; free_list(struct list *listp) { while (listp != NULL) { free(listp); listp = listp->next; } }もしかすると、これがうまく動作することがあるかもしれません。しかし、原 則としては、ある領域を開放したら、次の瞬間にはその中身がどうなっているか は分からないのです。この例では、
free(listp)
を実行した瞬間に、その内容は保証対象外になってしまうので、
listp->next
の値が正しいとは限りません。たまたま正しい値が残っている可能性はあります
が、いずれにしても綱渡りをするようなものです。
次のように一時変数に代入してから開放すれば全く問題ないのですから、この ような手間は惜しまない方が安心です。
free_list(struct list *listp) { while (listp != NULL) { struct list *nextp; nextp = listp->next; free(listp); listp = nextp; } }(参考: comp.lang.c FAQ 7.20)
malloc
で得たポインタの値を使って
free
を呼び出し、一旦開放した。もう一度同じ値を使って
free
を呼び出すと何が起きるか。
NULL
の時を除いて未定義となっています。このような呼び出しは行ってはいけません。
(参考: comp.lang.c FAQ 16.5)
free(NULL)
を実行すると何が起きるか。
例えば、いくつかの外部変数のポインタがあり、プログラム実行時に、その中
のいくつかを使っているとします。プログラムを終了する時に、獲得したメモリ
を開放するには、次のリストのように、
free(str1)
をいきなり呼び出して構いません。もし
str1
が使われていなければ、値が
NULL
のまま残っているはずですから、何も問題はありません。もちろん、このように
使う場合は、どこかでメモリを開放したらすかさず
str1
を
NULL
にしておく必要があります。
str2
は、
NULL
であるかどうか判断してから
free
を呼び出しています。この処理自体は間違いではありませんが、冗長です。
str2
が
NULL
の時には
free
を呼び出さないで済むので、ごく僅か処理速度が有利かもしれませんが、多くの
場合、プログラム全体の効率に影響するほどの効果は得られないでしょう。
char *str1 = NULL; char *str2 = NULL; int main(int argc, char *argv[]) { /* 必要な処理を行う */ free(str1); /* str1がNULLでも構わない */ if (str2 != NULL) { /* この判断は冗長 */ free(str2); } return 0; }
malloc
を実行した後、
free
をさらに実行した。それぞれの時点で残りメモリを調べたが、
free
した時点でメモリが増えたようには見えない。
malloc
や
free
で領域を確保する場合、その大きな領域から少しずつこまぎれにメモリを割り当
ててやるといったことをします。このように実装されていれば、
free
でメモリを開放しても、最初に獲得した大きな領域の中の使える部分が増えるだ
けで、他のプログラムが使えるメモリ量が増えるわけではありません。
このような実装が好まれる理由は、
free
を呼び出す毎に他のプログラムが使えるような形でメモリを開放すると、処理の
オーバーヘッドが大きくなることがあるからです。
(参考: comp.lang.c FAQ 7.25)
malloc(0)
を実行すると何が起きるのか。
realloc(s, 0)
を実行すると何が起きるのか。
s
が指す領域を開放することになっています。つまり、これは
free(s)
と同じ動作になります。ただし、comp.lang.c FAQには、この方法は昔のコンパ
イラには通用しないことがあるので、移植性が良くないと書かれています。
(参考: comp.lang.c FAQ 7.30)
realloc(NULL, size)
を実行すると何が起きるのか。
malloc(size)
と同じことになります。
次に示す例の関数
foo()
は、
bar()
を呼び出す毎に値を保持して、
-1
という値が現れるまでそれを続けます。値を保持する領域は、
int
の配列に相当する大きさを、初回のみ
malloc
、二回めからは
realloc
を使って確保しています。
void foo(void) { int i = 0; int n; int *p; n = 10; p = malloc(sizeof(int) * n); if (p == NULL) return; do { if (i == n) { int *new_p; n += 10; new_p = realloc(p, sizeof(int) * n); if (new_p == NULL) { free(p); return; } p = new_p; } } while ((p[i++] = bar()) != -1); /* pを処理する */ free(p); return; }しかし、
realloc
の最初の引数に
NULL
を使うことができることを知っていれば、初期値を少し変えてやるだけで、次の
ように
malloc
を省略して書くこともできます。この場合、最初の呼び出しで
realloc
の第一引数が
NULL
になっています。
void foo(void) { int i = 0; int n = 0; int *p = NULL; do { /* 以下同様 */ただし、これはあまり良い例ではありません。
calloc
はどのような時に使うのか。
calloc
は獲得した領域のビットが全て0になっている点だけが
malloc
と動作と異なります。います。整数を割り当てるための領域を獲得したい場合に
は、これは領域を獲得してから全てを
0
という値にすることに相当します。
malloc
で領域を獲得した場合には、その中に入っている値は何であるか分かりません。
初期値としてゴミが入っていることになります。
浮動小数点の値やポインタを保存する領域を獲得する場合、
calloc
を使うのは意味がありません。全てのビットが
0
であることは、浮動小数点表現の数の
0.0
に対応するとは限らないし、また、ポインタの
NULL
という値に対応しているとも限りません。このような場合には、明示的に初期化
することが必要です。
struct xlist { struct xlist *next; double d; int i[100]; }; struct xlsit *calloc_xlist(void) { struct xlist *p; p = calloc(1, sizeof(struct xlist)); if (p) { p->next = NULL; /* ポインタは、明示的な初期化が必要 */ p->d = 0.0; /* 浮動小数点変数も初期化が必要 */ /* i[0]..[99]は0になっている */ } return p; }
次のサンプルプログラムは、指定ファイルから一行ずつ読み込み、その内容を データ要素として新たなリストに格納し、新たなリストをリストの先頭に追加し ます。ファイルが終了したら、リストの順に読み込んだ行を表示しますが、最後 に読んだ行がリストの先頭になるため、表示は元のファイルの最後の行から始ま り最初の行で終わります。
単にリストの要素を保持できればよく、順番は重要でない場合は、このように
リストの先頭に要素を追加するのが簡単です。リストの最後に要素を追加する方
法もあります。最後に追加したリストへのポインタ、
struct list *last
を用意することによって、簡単に実現できます。
一般に、リスト構造は、その都度
malloc
などでメモリを獲得することによって新たなリスト要素を追加するように実現す
ることが多く、この方法によって、メモリの制限内で自由な数の要素を処理でき
るという特徴があります。
/* 指定したファイルの内容を最後の行から表示する */ #include <stdio.h> #include <stdlib.h> #include <string.h> struct list { struct list *next; char *str; }; void foo(FILE *fp) { char buf[128]; struct list *next; /* メモリ開放時の作業用 */ struct list *p; struct list *top = NULL; while (fgets(buf, 128, fp) != NULL) { p = malloc(sizeof(struct list)); if (p == NULL) goto quit; /* メモリが足りないようだ */ p->str = malloc(strlen(buf) + 1); if (p->str == NULL) { free(p); goto quit; } strcpy(p->str, buf); if (top == NULL) { /* 最初のリスト要素なら */ p->next = NULL; /* 次のリスト要素はない */ } else { p->next = top; /* 今までの先頭を次のリスト要素とする */ } top = p; /* 今獲得したリスト要素を先頭とする */ } /* リストの要素を表示する。 * 最後の行から先頭の行に向かって表示することになる。 */ for (p = top; p != NULL; p = p->next) { fputs(p->str, stdout); } /* リストに使ったメモリを開放しておく */ quit: for (p = top; p != NULL; p = next) { next = p->next; free(p->str); free(p); } } int main(int argc, char *argv[]) { --argc; ++argv; while (argc-- > 0) { FILE *fp; fp = fopen(*argv, "r"); if (fp != NULL) { foo(fp); fclose(fp); } argv++; } return 0; }
/* 双方向リスト */ #include <stdio.h> #include <stdlib.h> #include <string.h> struct list { struct list *next; struct list *prev; char *str; }; void foo(FILE *fp) { char buf[128]; struct list *last = NULL; struct list *next; /* メモリ開放時の作業用 */ struct list *p; struct list *top = NULL; while (fgets(buf, 128, fp) != NULL) { p = malloc(sizeof(struct list)); if (p == NULL) goto quit; /* メモリが足りないようだ */ p->str = malloc(strlen(buf) + 1); if (p->str == NULL) { free(p); goto quit; } strcpy(p->str, buf); if (top == NULL) { /* 最初のリスト要素なら */ p->prev = NULL; p->next = NULL; top = last = p; } else { /* 最後の要素と連結する */ p->prev = last; p->next = NULL; last->next = p; /* 今まで最後だった要素の次に加える */ last = p; /* この要素を最後の要素とする */ } } /* リストの要素を表示する。 */ for (p = top; p != NULL; p = p->next) { fputs(p->str, stdout); } puts("---- 逆順に表示 ----"); for (p = last; p != NULL; p = p->prev) { fputs(p->str, stdout); } /* リストに使ったメモリを開放しておく */ quit: for (p = top; p != NULL; p = next) { next = p->next; free(p->str); free(p); } } int main(int argc, char *argv[]) { --argc; ++argv; while (argc-- > 0) { FILE *fp; fp = fopen(*argv, "r"); if (fp != NULL) { foo(fp); fclose(fp); } argv++; } return 0; }