初級C言語Q&A(4)

初出: C MAGAZINE 1995年9月号
Updated: 1996-03-12

[1つ前] [1つ後] [質問一覧] [記事一覧] [ホームページ]


malloc, free

動的なメモリの獲得は、C言語ではしばしば使われる手法です。ポインタの理解 が十分なら、 malloc の使い方を理解することにより、複雑なデータ構造を柔軟に処理することが可能 になるでしょう。あまり慣れない人は、十分な大きさの固定サイズの配列を用意 してしまいがちですが、 malloc 自体はそれほど難しいテクニックを必要としません。


Q 【malloc】

 malloc は何と読むのか。

 「えむあろっく」と読む人と「まろっく」と読む人を知っていますが、それ以 外の読み方は聞いたことがありません。個人的には、本来の意味がmemoryを allocateする、ということなので、m+allocと考えるのが自然だと思います。

Q 【文字列を保存する領域】

 次のコードが含まれているプログラムを動かしていると、何か変だ。
char *alloc_string(char *string)
{
    char *p;
    size_t len;

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

 このコードの不審な所は三つあります。
  1. 確保する領域の大きさが足りません。 strlen の戻す値、すなわち文字列の長さには、文字列の終了を示す '¥0' を含んでいません。 strcpy を実行すると、この '¥0' も複写しようとするので、どこかおかしな所に '¥0' を書き込んでしまうことになります。

     すなわち、ここでは malloc(len + 1) としなければなりません。

     このミスは結構よくやります。プログラマーによっては、次のように書く人も います。

        len = strlen(string) + 1;
        p = malloc(len);
    
     これは確かに正しいコードですが、 malloc(len + 1) の書き方と混在していると、とても分かりにくくなるので、どちらかに統一した 方がよいのです。 len には strlen が戻した値をそのまま保持した方が誤解を避けることができるので、 malloc の時に1大きな領域を取るようにするのが意味としては明解ではないかと思います。

  2. malloc に失敗した時のことを考えていません。つまり、もしメモリが殆ど使われてしま っていたら、 malloc は新たな空きメモリ領域を確保できないかもしれません。メモリを使い果たすと mallocNULL を戻すことになっています。その検査が必要です。

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

  3. 引数が 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;
}

Q 【固定長の文字列にしない理由】

 なぜ文字列を扱う時に malloc を使ってその都度メモリを獲得しようとするのか。あらかじめ適当なサイズの文 字配列を獲得しておき、使えばよいのではないか。

 場合によって使い分けるべきです。

 malloc を使ってその都度メモリを獲得する利点は、主に二つあります。

 いずれも、メモリを無駄なく使うことが可能になります。しかし、若干のデメ リットもあります。

Q 【自動変数との差】

 一時的にメモリを獲得したいのなら、自動変数の配列を使えば同じことができ るのではないか。

 可能な場合もあります。ただし、自動変数の場合、サイズはコンパイル時に決 定されるので、実行時に動的にメモリを獲得することはできません。また、自動 変数は malloc で獲得できるメモリ領域とは異なった所から獲得されることがあります。場合に よっては malloc で取れる最大量より少ない限界でパンクするかもしれません。

Q 【free】

 獲得したメモリはプログラム終了時に、 free しなければならないのか。あるいは、するべきか。

 おそらく、殆どの処理系では、建前として、プログラムが終了した時点で獲得 されたメモリは全て自動的に開放されることになっていると思われます。しかし、 なぜだかプログラムを実行する度に空きメモリが減ってゆくという奇妙な現象が 発生するシステムを目撃することもしばしばあるようです。

 獲得したメモリを全て free してから終了すれば、このような奇妙な現象を回避できる確率は高くなりますが、 完璧であるとは限りません。

 いずれにしても、獲得したメモリを free しなくても構わない、という考え方の方が一般的であるようです。

(参考: comp.lang.c FAQ 7.24)


Q 【freeした直後の領域】

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)

Q 【freeを二度呼び出す】

 malloc で得たポインタの値を使って free を呼び出し、一旦開放した。もう一度同じ値を使って free を呼び出すと何が起きるか。

 何が起きるか分かりません。仕様では、この処理は与える値が NULL の時を除いて未定義となっています。このような呼び出しは行ってはいけません。

(参考: comp.lang.c FAQ 16.5)


Q 【free(NULL)】

free(NULL) を実行すると何が起きるか。

 何も起きません。言い換えれば、このような呼び出しが出来ることは、仕様に よって保証されています。

 例えば、いくつかの外部変数のポインタがあり、プログラム実行時に、その中 のいくつかを使っているとします。プログラムを終了する時に、獲得したメモリ を開放するには、次のリストのように、 free(str1) をいきなり呼び出して構いません。もし str1 が使われていなければ、値が NULL のまま残っているはずですから、何も問題はありません。もちろん、このように 使う場合は、どこかでメモリを開放したらすかさず str1NULL にしておく必要があります。 str2 は、 NULL であるかどうか判断してから free を呼び出しています。この処理自体は間違いではありませんが、冗長です。 str2NULL の時には free を呼び出さないで済むので、ごく僅か処理速度が有利かもしれませんが、多くの 場合、プログラム全体の効率に影響するほどの効果は得られないでしょう。

char *str1 = NULL;
char *str2 = NULL;

int main(int argc, char *argv[])
{
    /* 必要な処理を行う */

    free(str1); /* str1がNULLでも構わない */

    if (str2 != NULL) { /* この判断は冗長 */
        free(str2);
    }

    return 0;
}

Q 【freeしてもメモリが増えない】

 プログラム中で malloc を実行した後、 free をさらに実行した。それぞれの時点で残りメモリを調べたが、 free した時点でメモリが増えたようには見えない。

 メモリを獲得する具体的な実装方法は、処理系に任されています。多くの処理 系では、あらかじめ大きな領域を確保しておき、 mallocfree で領域を確保する場合、その大きな領域から少しずつこまぎれにメモリを割り当 ててやるといったことをします。このように実装されていれば、 free でメモリを開放しても、最初に獲得した大きな領域の中の使える部分が増えるだ けで、他のプログラムが使えるメモリ量が増えるわけではありません。

 このような実装が好まれる理由は、 free を呼び出す毎に他のプログラムが使えるような形でメモリを開放すると、処理の オーバーヘッドが大きくなることがあるからです。

(参考: comp.lang.c FAQ 7.25)


Q 【0バイト獲得】

 malloc(0) を実行すると何が起きるのか。

 この結果は、処理系により定義が異なります。移植性が低下するので、このよ うな書き方はしない方が望ましいと思います。つまり、呼び出す時点で0になるの であれば、予期する処理になるように、移植性のある書き方をすべきです。もし どうしても確実なポインタが欲しいのなら、0の代わりに1で呼び出せば、メモリ をほんの少し無駄にする代わりに移植性のある書き方にできます。

Q 【reallocの0バイト獲得】

realloc(s, 0) を実行すると何が起きるのか。

ANSI Cでは、このような呼び出しを行った時には、 s が指す領域を開放することになっています。つまり、これは free(s) と同じ動作になります。ただし、comp.lang.c FAQには、この方法は昔のコンパ イラには通用しないことがあるので、移植性が良くないと書かれています。

(参考: comp.lang.c FAQ 7.30)


Q 【realloc(NULL, size)】

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 {
        /* 以下同様 */
 ただし、これはあまり良い例ではありません。

Q 【calloc】

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;
}

Q 【リスト】

 リスト構造とは何か。

 リストとは、データ要素そのものと、次の構造を指すポインタを要素として持 つ構造体によって実現できるデータ構造を言います。

 次のサンプルプログラムは、指定ファイルから一行ずつ読み込み、その内容を データ要素として新たなリストに格納し、新たなリストをリストの先頭に追加し ます。ファイルが終了したら、リストの順に読み込んだ行を表示しますが、最後 に読んだ行がリストの先頭になるため、表示は元のファイルの最後の行から始ま り最初の行で終わります。

 単にリストの要素を保持できればよく、順番は重要でない場合は、このように リストの先頭に要素を追加するのが簡単です。リストの最後に要素を追加する方 法もあります。最後に追加したリストへのポインタ、 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;
}

Q 【双方向リスト】

 双方向リストとは何か。

 前述のリスト構造は、ポインタが一方向しかないため、先頭から最後の要素に 向かってたどるのは簡単ですが、一つ前の要素に戻るのは面倒です。そこで、次 の要素だけでなく、直前の要素を指すようなポインタも追加すたデータ構造にし たものが双方向リストと呼ばれるものです。
/* 双方向リスト */

#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;
}

(C) 1995-1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。