【C言語】free関数の使い方と注意点について解説

C言語のfree関数の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、C言語の free 関数について解説していきます。

「引数は1つだけ&戻り値は無し」なので非常に簡単に扱えそうな関数ではありますが、この free 関数を実行した時にプログラムがクラッシュ・強制終了してしまうことは結構多いです。

途中までプログラムは快適に動作するのに、プログラムの最後の free 関数実行時に強制終了してしまって困ったことがある方も多いのでは無いでしょうか?私はこの経験があって、結局その時は free 関数実行部分をコメントアウトしました…。

そういった free 関数実行時のプログラムの強制終了・クラッシュに困っている方も多いと思いますので、このページでは free 関数の使い方だけでなく、free 関数使用時の注意点や free 関数でプログラムが強制終了する原因についても解説していきたい思います!

free 関数とは

free 関数とは下記のような関数によって確保されたメモリを解放する関数になります。

  • malloc
  • realloc
  • calloc

特に malloc 関数については、メモリの確保も含めて下記ページで詳しく解説していますので、詳細を知りたい方は下記ページを別途ご参照していただければと思います。

malloc解説ページのアイキャッチ 【C言語】malloc関数(メモリの動的確保)について分かりやすく解説

メモリの解放

上記の関数は全て、メモリを(動的に)確保する関数です。

メモリを確保すれば、そのメモリは自身のプログラムで自由自在に扱うことができますし、そのメモリが他のプログラムによって利用されるようなこともありません(基本的には)。

逆に考えると、確保したメモリは他のプログラムからは利用できないので、メモリを確保し続けると他のプログラムが使用できるメモリが減ることになります(パソコンで使えるメモリは有限です)。

ですので、確保したメモリが不要になった際には「もうこのメモリは不要です」と宣言し、他のプログラムが使用できるようにしてやる必要があります。そして、「もうこのメモリは不要です」と宣言することが、メモリの解放となります。

このメモリの解放により、他のプログラムがそのメモリを確保し、使用することができるようになります。

そして、このメモリの解放を行うのが、このページで扱う free 関数となります。

スポンサーリンク

メモリの解放の重要性

基本的にプログラム内で確保したメモリは、プログラムが終了した際に全て自動的に解放されるようになっているはずです。なので、malloc 関数などで確保したメモリを解放し忘れたとしても、プログラムがすぐに終了するのであればパソコンなどに特に悪影響はない場合が多いです。少なくとも電源を切ればメモリの状態はリセットされるはずです。

ただし、長時間起動するプログラムの場合、このメモリの解放忘れが致命的なバグとなることが多いです。

例えば24時間ずっと起動しているようなプログラムで、さらに定期的にメモリを確保するような場合、メモリを使い終わったのにそのメモリの解放を怠っていると、そのプログラム内で確保したメモリがどんどん増えていくことになります。

逆にいうと、他のプログラムが使用できるメモリがどんどん減ることになります。

コンピュータ内で扱えるメモリは有限なので、いずれは使用可能なメモリが尽きることになります。この場合、同じコンピュータ内のプログラムはメモリを確保できなくなり、正常に動作できなくなってしまいます。

例えば自動運転している車のプログラムでこのようなことが起こると大変なことになりますよね…。

こういったことが起こらないように、特に長時間動作するようなプログラムにおいては、確保したメモリが不要になった際には必ず解放してやる必要があります。

もちろん前述のように、すぐに終了するようなプログラムの場合はメモリの解放を怠ったとしても悪影響がないことはありますが、将来開発の現場でプログラミングをするような場合に備えて、メモリの解放は習慣づけておいた方が良いです。

free 関数の概要

では、次は free 関数について解説していきます。free 関数の書式は下記となります。

free関数
#include <stdlib.h>

void free(void *ptr);

まず、free 関数を利用する際には stdlib.h をインクルードしておく必要があります。

また、free 関数の戻り値の型は void です。つまり何も返却しません。

さらに、引数 ptr の型は void * です。

要は引数する変数の型としては、int * でも char * でも、さらには int *** などでも、ポインタ型であればなんでも指定可能ということになります。

void * 型については下記ページで詳細を解説していますので、詳しく知りたい方は別途参照していただければと思います。

voidとvoid*型の解説ページのアイキャッチ 【C言語】void型とvoid*型(void型ポインタ)について解説

ただ、型としてはポインタであればなんでも指定可能というだけで、実際には、前述でも紹介した malloc 関数・realloc 関数・calloc 関数等によって動的に確保したメモリの先頭アドレス、もしくは NULL を引数 ptr に指定する必要があります。

malloc 関数・realloc 関数・calloc 関数等によって動的に確保したメモリの先頭アドレスを引数 ptr に指定して free 関数を実行した場合、そのアドレスのメモリが解放されます(つまり「もうこのメモリは不要です」と宣言する)。

引数で指定するのはあくまでもアドレスのみで、サイズの指定は不要です。標準ライブラリ側でその引数 ptr のアドレスに紐づけてサイズも管理してくれているはずなので、free 関数内部で ptr からそのサイズ分のメモリをしっかり解放してくれます。

また、引数 ptrNULL を指定して free 関数実行した場合は、free 関数は何も行いません。この free(NULL) が正常に動作することを利用して、後に説明する free 関数使用時の注意点 の一部を避けることが可能になります。

malloc 関数・realloc 関数・calloc 関数等によって確保したメモリの先頭アドレス or NULL 以外のアドレスを引数 ptr に指定した場合の free 関数の動作は未定義となっています。

以上で解説した、free 関数の引数と free 関数の動作との関係をまとめたものが下記となります。

  • NULL:何もしない
  • malloc 関数等の返却アドレス:そのアドレスのメモリを解放
  • 上記以外:未定義(どう動作するか分からない)

free 関数の使い方

では実際に free 関数の使い方を、例を示しながら解説していきたいと思います。

スポンサーリンク

free 関数の基本的な使い方

free 関数の一番簡単な使用例は下記になります。

free関数の使用例
#include <stdio.h>
#include <stdlib.h>

int main(void) {

    int *ptr = NULL;
    int i;

    /* ptrに確保したメモリの先頭アドレスを格納 */
    ptr = (int*)malloc(sizeof(int) * 5);
    if (ptr == NULL) {
        return -1;
    }

    /* 確保したメモリを使用して処理 */
    for (i = 0; i < 5; i++) {
        ptr[i] = i * 1024;
    }

    for (i = 0; i < 5; i++) {
        printf("%d\n", ptr[i]);
    }

    /* 確保したメモリを解放 */
    free(ptr);

    return 0;
}

下記部分で malloc 関数によりメモリが確保され、そのメモリの先頭アドレスが malloc 関数から返却されます。したがって、ptr にはその確保されたメモリの先頭アドレスが格納されることになります。

確保したメモリの先頭アドレスがptrに格納される
ptr = (int*)malloc(sizeof(int) * 5);

さらに malloc 関数実行後に、その確保したメモリを使用してなんらかの処理が実行されます。上記の例では適当な数値をメモリに格納し、さらにそのメモリに格納した値の表示を行なっています。

最後に下記部分で free 関数が実行され、ptr に格納されているアドレスを先頭アドレスとするメモリが解放されます。

ptrのアドレスのメモリが解放される
free(ptr);

free 関数使用時の基本的な流れはこれだけで、要は malloc 関数等で確保されたメモリの先頭アドレスをポインタ変数に格納しておき、そのメモリを使い終わったら、free 関数にそのポインタ変数を指定してメモリを解放するというのが基本的な流れになります。

動的確保したメモリは漏れなく free 関数で解放

メモリの解放の重要性 でも解説したように、malloc 関数等で確保したメモリは不要になった際には解放してやる必要があります。この不要になったメモリの解放をし忘れることをメモリリークと呼びます。

例えば下記の関数はメモリリークが発生する可能性のある関数になります。

メモリリークが発生する例
int func(void) {
    int *ptr = NULL;
    int ret;

    ptr = (int*)malloc(sizeof(int) * 100);
    if (ptr == NULL) {
        printf("malloc error\n");
        return -1;
    }

    ret = funcA(ptr, 100);
    if (ret < 0) {
        printf("funcA error\n");
        return ret;
    }

    ret = funcB(ptr, 100);
    if (ret < 0) {
        printf("funcB error\n");
        return ret;
    }

    free(ptr);

    return ret;
}

上記関数においては、funcAfuncB0 よりも小さい値を返却した場合、malloc 関数が確保したメモリのアドレス ptr に対して free 関数が実行されずに関数が終了してしまうので、メモリリークが発生します。

上記の関数であれば、malloc 関数でのメモリ確保成功後に実行される return の直前で free 関数を実行するようにしてやれば、メモリリークは防ぐことができます。

メモリリークを防ぐ例1
int func(void) {
    int *ptr = NULL;
    int ret;

    ptr = (int*)malloc(sizeof(int) * 100);
    if (ptr == NULL) {
        printf("malloc error\n");
        return -1;
    }

    ret = funcA(ptr, 100);
    if (ret < 0) {
        printf("funcA error\n");
        free(ptr);
        return ret;
    }

    ret = funcB(ptr, 100);
    if (ret < 0) {
        printf("funcB error\n");
        free(ptr);
        return ret;
    }

    free(ptr);

    return ret;
}

上記では malloc 関数に失敗した後の return の前で free(ptr) を実行していませんが、これはメモリリークにはならないのでしょうか?

malloc 関数がエラーになった際にはメモリは確保されませんので、この場合はメモリの解放は不要です。ただし、malloc 関数がエラーになった場合には戻り値には NULL が格納されますので、malloc で失敗した後に free(ptr) を実行しても問題はないです。

また、下記のように関数内で return を行う箇所を1つに絞ることでメモリリークを防ぐようなこともできます。

メモリリークを防ぐ例2
int func(void) {
    int *ptr = NULL;
    int ret;

    ptr = (int*)malloc(sizeof(int) * 100);
    
    if (ptr != NULL) {

        ret = funcA(ptr, 100);
    
        if (ret >= 0) {
            ret = funcB(ptr, 100);
        }
    }

    free(ptr);
    
    return ret;
}

free 関数使用時の注意点

ここからは、free 関数使用時の注意点について解説していきたいと思います。

ここからは解説を簡単にするため、動的確保を行う関数を malloc 関数を前提として解説していきます。が、ここから解説する内容は malloc 関数だけでなく、他の動的確保を行う関数(realloccalloc)などにも当てはまる内容ですので、この点は勘違いされないようご注意ください。

スポンサーリンク

まず最初の注意点です。

free 関数の引数には、malloc 関数で確保したメモリの “先頭アドレス以外” は指定してはいけません(NULL を除いて)。

つまり、malloc 関数の戻り値を格納したポインタ変数の値を変更した場合、そのポインタ変数は free 関数に指定してはいけません。

要は、free 関数の引数には、malloc 関数の返却値をそのまま指定する必要があります。

例えば下記は、free 関数の使い方 で示したソースコードを少しだけ変更したものになります。これはダメな例です。なぜダメだか分かるでしょうか?

先頭アドレス以外をfree関数に指定する例
#include <stdio.h>
#include <stdlib.h>

int main(void) {

    int *ptr = NULL;
    int i;

    /* ptrに確保したメモリの先頭アドレスを格納 */
    ptr = (int*)malloc(sizeof(int) * 5);
    if (ptr == NULL) {
        return -1;
    }

    /* 確保したメモリを使用して処理 */
    for (i = 0; i < 5; i++) {
        ptr[i] = i * 1024;
    }

    for (i = 0; i < 5; i++) {
        printf("%d\n", *ptr);
        ptr++;
    }

    /* 確保したメモリを解放 */
    free(ptr);

    return 0;
}

このソースコードをコンパイルして実行すると私の環境では下記のように表示されてプログラムが強制終了します。

0
1024
2048
3072
4096
free.exe(30347,0x10d635e00) malloc: *** error for object 0x7ff897504e94: pointer being freed was not allocated
free.exe(30347,0x10d635e00) malloc: *** set a breakpoint in malloc_error_break to debug
zsh: abort      ./free.exe

ポイントは2つ目の for ループの中で実行している下記部分ですね!

ptrの値を変更する処理
ptr++;

上記の処理では、要は ptr = ptr + 1 が実行されるので ptr の値が変化することになります。

つまり、ptr++ を実行した時点で、もうこのポインタ変数 ptrmalloc 関数で確保したメモリの先頭アドレスを指していないことになります。

したがって、このポインタ変数 ptrfree 関数に指定してはいけません。

こういった、(NULL を除く)malloc 関数の返却値以外のアドレスを free 関数に指定した場合の動作は未定義、つまり free 関数がどう動作するかが分かりません。

私の環境では上記のようにプログラムが強制終了しますが、環境によっては正常にプログラムが終了するような場合もあると思います(ちなみに、上記のソースコードの 5 を全て 4 に変更した場合、私の環境だとプログラムが正常終了するようになってしまいました…)。

ポインタ変数は上記のようなインクリメント処理等により値を変更することは可能ではありますが、特に free 関数で確保したメモリのアドレスを格納しているポインタ変数の場合、ポインタ変数の値の変更は行わない方が無難だと思います。

もし変更するのであれば、下記のように他のポインタ変数を用意し、そのポインタ変数で free 関数に指定するアドレスを保持するようにしておきましょう。

先頭アドレスを別のポインタで保持する例
#include <stdio.h>
#include <stdlib.h>

int main(void) {

    int *ptr = NULL;
    int i;
    int *head = NULL;

    /* ptrに確保したメモリの先頭アドレスを格納 */
    ptr = (int*)malloc(sizeof(int) * 5);
    if (ptr == NULL) {
        return -1;
    }

    /* 確保したメモリを使用して処理 */
    for (i = 0; i < 5; i++) {
        ptr[i] = i * 1024;
    }

    /* 確保したメモリの先頭アドレスをheadに退避 */
    head = ptr;
    for (i = 0; i < 5; i++) {
        printf("%d\n", *ptr);
        ptr++;
    }

    /* 確保したメモリを解放 */
    free(head); /* ptrではなくheadを指定 */

    return 0;
}

解放済みのメモリのアドレスを指定してはダメ

また、malloc 関数の返却値であったとしても、一度解放を行なったメモリのアドレスを再度 free 関数に指定するのはダメです。

二重解放ってやつですね。

この場合の動作も未定義となっており、どう動作するかが分かりません。

ちなみに私の環境で二重解放を行うと下記のようなメッセージが表示されてプログラムが強制終了します。

free.exe(32010,0x7000002f4000) malloc: *** error for object 0x1005041a0: pointer being freed was not allocated
free.exe(32010,0x7000002f4000) malloc: *** set a breakpoint in malloc_error_break to debug

例えば下記は二重解放をしてしまうソースコードの例になります。

二重解放の例
#include <stdlib.h>

int main(void) {
    int *ptr = NULL;

    ptr = (int*)malloc(sizeof(int));
    if (ptr == NULL) {
        return -1;
    }

    /* 何かしらの処理を実行 */

    /* アドレスptrのメモリを解放 */
    free(ptr);

    /* 解放済みのアドレスを再度解放 */
    free(ptr);

    return 0;
}

特にプログラム内で並列動作を行うような場合(マルチスレッドなどを用いて)、二重解放を行なっていると、他の関数に影響を及ぼし、開発チームの他のメンバーに迷惑がかかることもあるので特に注意が必要です。

例えば下記の関数 funcAA さんが作成し、

二重解放を行う関数funcA
int funcA(void) {
    
    int *ptrA = NULL;
    
    ptrA = (int*)malloc(sizeof(int));
    if (ptrA == NULL) {
        return -1;
    }

    /* 何かしらの処理を実行 */

    free(ptrA);

    /* 何かしらの処理を実行 */
 
    /* 間違って再度ptrAをfree*/
    free(ptrA);

    return 0;
}

さらに下記の関数 funcBB さんが作成したとします。

正常に解放を行う関数funcB
int funcB(void) {
    
    int *ptrB = NULL;
    
    ptrB = (int*)malloc(sizeof(int));
    if (ptrB == NULL) {
        return -1;
    }

    /* 何かしらの処理を実行 */

    free(ptrB);

    return 0;
}

さらに、プログラム内で funcAfuncB が並列動作する可能性がある場合、タイミングによっては下記のようなことが起こり得ます。

  • funcAmalloc 関数を実行する
  • malloc 関数からアドレス 0x1234 が返却され、ptrA0x1234 が格納される
  • funcAfree(ptrA) を実行して 0x1234 のメモリを解放する
  • funcBmalloc 関数を実行する
  • malloc 関数からたまたま同じアドレス 0x1234 が返却され、ptrB0x1234 が格納される
  • funcAfree(ptrA) を実行して 0x1234 のメモリが解放する
    • ここで funcB から確保されたメモリが解放されてしまう…
  • funcBfree(ptrB) を実行して 0x1234 のメモリを解放しようとする
    • funcA が既に 0x1234 のメモリを解放しているしているので二重解放!!!

先程紹介した funcAfuncB について考えると、明らかに関数の作りとして問題があるのは funcA です。

ですが、funcA で二重解放していることに気づいていない場合、もし二重解放時にプログラムが強制終了し、さらに funcBfree(ptrB) を実行した際にプログラムが強制終了していることが判明したとすれば、まず原因として疑われるのは funcB でしょうね…。funcB で強制終了しているのですから…。そして B さんはプログラムが強制終了した原因を追求することを求められるでしょう…。

もちろん関数が上記の funcAfuncB くらいしかないのであれば、すぐに funcA に原因があることは分かると思いますが、プログラムが大規模だとどこに本質的な原因があるかを見つけるのは難しいです…。

こんな感じで、二重解放をすると他の関数にまで影響を及ぼす可能性があるので注意が必要です(そもそもいろんなスレッドから malloc しない方が良いかも)。

もちろん二重解放するようなソースコードを書かないことが一番重要ですが、人為ミスはあり得ます。そのミスが起こりうることを考慮すれば、二重解放を防ぐための手段として有効なのは、free したポインタに NULL を格納しておくことだと思います。

例えば、上記の funcA の場合であれば、free(ptrA) の直後に ptrA = NULL を行なっておけば、例えミスで free(ptrA) を二回行なったとしても、二重解放を防ぐことはできます。

二重解放を行う関数funcA
int funcA(void) {
    
    int *ptrA = NULL;
    
    ptrA = (int*)malloc(sizeof(int));
    if (ptrA == NULL) {
        return -1;
    }

    /* 何かしらの処理を実行 */

    free(ptrA);
    ptrA = NULL; /* NULL代入 */

    /* 何かしらの処理を実行 */
 
    /* 間違って再度ptrAをfree*/
    free(ptrA); /* ptrAはNULLなので問題なし */

    return 0;
}

なぜ二重解放のミスが発生するかというと、これは “解放したメモリのアドレスをわざわざ覚えてしまっている” からです(ポインタ変数にアドレスを格納したままになっている)。

解放したメモリのアドレスなど忘れてしまえば、二重解放はまず起こり得ません。アドレスを忘れているのですから free 関数にそのアドレスを指定することなんてできないですよね。

このアドレスを忘れるための処理がポインタ変数への NULL の代入であると考えることができます。そして、たとえそのポインタ変数を free しようとしたとしても、NULL が格納されているので free は正常に終了します(free(NULL) では何も行われない)。

ポインタを扱うときは、この NULL をうまく扱うことが非常に重要です。これについては下記ページで解説していますので、詳しく知りたい方はぜひ読んでみてください。

NULLの解説ページアイキャッチ 【C言語】「NULL」の意味とNULLを用いた「安全なポインタの使い方」

ただ、free 直後の NULL 代入さえ行えば必ず二重解放を防ぐことができるというわけではないので注意してください。

動的確保したメモリ以外のアドレスを指定してはダメ

また、free 関数はあくまでも malloc 等で動的に確保したメモリを解放する関数です。

したがって、malloc 等で動的に確保したメモリ以外のアドレスを free 関数の引数に指定してはいけません。このように引数を指定した場合の動作も未定義となります。

ですので、配列などのアドレスを free 関数に指定するのはダメです。

これに関しては、メモリの “先頭アドレス以外” の値を指定してはダメ で解説したように、”free 関数の引数には malloc 関数の返却値以外のアドレスは指定してはいけない” ということを理解していれば、だいたい避けられる注意点だと思います。

ただし、これを理解していてもやりがちな失敗が下記の例になります。何が問題か分かりますか?

不定値に対するfree
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *ptr;
    int x;

    scanf("%d", &x);

    if (x > 0) {
        ptr = (int*)malloc(sizeof(int) * x);
        if (ptr == NULL) {
            return -1;
        }
    }

    free(ptr);

    return 0;

}

上記の場合、scanf で入力される値が 0 以下だと malloc 関数が実行されず、ptrの値が更新されません。この場合、ptr は値が更新されずに free 関数が実行されるため、free 関数には不定値のアドレスが指定されることになります(ptr は変数宣言時に初期化されていないので何の値が格納されているかは不定)。

割と条件分岐の中で malloc を行うような場合にやりがちな失敗ですね。

free 関数の引数には malloc 関数の返却値以外のアドレスは指定してはいけない” ということを理解していれば、自ら配列などのアドレスを free 関数に指定することは無いと思います。ですが、誤って不定値をそのまま free 関数に指定するのは結構やりがちなので注意してください。

変数宣言時に NULL で代入しておけば、上記のようなケースにおいても free 関数で強制終了することはありませんので、特にポインタ変数は変数宣言時に NULL で初期化しておく方が安全です。

スポンサーリンク

解放した後のメモリにアクセスしてはダメ

これは free 関数実行時の注意点というよりも、free 関数実行後の注意点になります。

free 関数で解放した後のメモリにアクセス(メモリの変更やメモリからのデータの取得)をしてはいけません。この場合の動作は未定義となります。

メモリの解放 でも説明した通り、free 関数は「もうこのメモリは不要です」と宣言するための関数です。

この不要であると宣言されたメモリは、以降で他の関数や他のプログラムから malloc 関数が実行された際に確保される可能性があります。

したがって、free 関数で解放したメモリを変更してしまうと、他の関数や他のプログラムが使用しているメモリを変更してしまうことになりかねません。もしこれが起こると、意図せずメモリのデータが変更されているのですから、そのメモリを使用している関数や他のプログラムはおそらく正常には動作できないでしょう。

ですので、free 関数で解放したメモリにはアクセスしないように気をつける必要があります。

例えば下記は free 関数で解放した後に、その解放したメモリを変更するソースコードの例になります。

free後のメモリへのアクセス
#include <stdio.h>
#include <stdlib.h>

#define SIZE (1024*1024*100)

int main(void) {
    int *ptr = NULL;
    int i;

    ptr = (int*)malloc(sizeof(int) * SIZE);
    if (ptr == NULL) {
        return -1;
    }

    free(ptr);

    for (i = 0; i < SIZE; i ++) {
        ptr[i] = i;
    }

    return 0;
}

上記のように free 後のメモリにアクセスした場合の動作は未定義ではありますが、解放した後のメモリが他のプログラムによって確保されたような場合、上記のような処理を行うと強制終了することが多いんじゃないかなぁと思います。

私の環境だと上記のプログラムを実行すれば毎回次のメッセージが出て強制終了してくれます。

zsh: segmentation fault  ./free.exe

厄介なのは、解放したアドレスのメモリが再び同じプログラム内で確保されてしまった時ですね…。この場合は、free したメモリにアクセスしているように見えても、実は同じプログラム内で後から確保したメモリにアクセスすることになるので、強制終了することは無いはずです。

強制終了しないからオーケーというわけではなく、他の関数が使用しようとしているメモリの中身を変更しようとしているのですから、当然プログラムとしての動作は異常になると思います。むしろ強制終了してくれないので、バグが発生していることに気づきにくくなるので厄介です。

free した後のメモリにアクセスしてしまうことを防ぐのに有効なのは、これも free 直後にポインタ変数に NULL を格納しておくことです。これにより解放後のメモリのアドレスをポインタ変数が忘れることになるので(NULL で上書きされるから)、そのアドレスのメモリへのアクセスができなくなります。

もし、そのポインタ変数の指すデータにアクセスしようとした場合でも、ポインタ変数に NULL が格納されているので NULL アクセスになり、多くの環境で強制終了してくれるようになると思います。なのでバグが発生していることに気づきやすくなります。

free 関数で強制終了する原因

最後に、free 関数を実行するとプログラムが強制終了する・プログラムが落ちるといったケースが発生する原因について解説しておきます。

他の原因で free 関数実行時にプログラムが強制終了する可能性もありますが、まず確認すべきは下記の2点だと思います。

free 関数の使い方が誤っている

まず疑うべきは free 関数の使い方ですね。

より具体的には、free 関数使用時の注意点 で紹介した下記の3つに当てはまるような使い方をしていないかどうかを確認するのが良いと思います。

  • 解放済みのメモリのアドレスを指定してはダメ
  • 動的確保したメモリ以外のアドレスを指定してはダメ

これらを行なった時の free 関数の動作は未定義ではありますが、多くの環境では上記を行うとプログラムが強制終了するのではないかと思います。

もし free 関数が強制終了する原因が、free 関数の使い方の誤りであったのであれば結構ラッキーです。

スポンサーリンク

確保したメモリの使い方が誤っている(メモリが破壊されている)

free 関数が強制終了する原因を追求するのに大変なのが、free 関数の使い方が正しいのにも関わらず free 関数が強制終了してしまう場合です。

free 関数の使い方が正しい場合であっても、free 関数で強制終了することはあり得ます。

このような現象の多くは、malloc 関数等で確保したメモリよりも外側のメモリ(つまり確保していないメモリ)を変更してしまうことが原因で発生します(いわゆるバッファオーバーラン)。

もう少し具体的に言えば、malloc 関数では引数には確保したいメモリのサイズをバイト単位で指定します。そうすることにより、malloc 関数の返却値のアドレスから、引数で指定したサイズ分のメモリが確保され、そのメモリのみをプログラム内で自由に使用できるようになります。

malloc関数で確保したメモリと変更可能なメモリの関係図1

逆にいうと、それ以外のメモリの使用は許可されていませんので、malloc 関数の返却値のアドレスよりも小さな値のアドレスであったり、malloc 関数の返却値のアドレス + 引数で指定したサイズよりも大きな値のアドレスのメモリの変更は行ってはいけません(変更だけでなく取得するのも良くない)。

malloc関数で確保したメモリと変更可能なメモリの関係図2

こういった確保したメモリの外側のメモリを変更するとメモリが破壊されてしまう可能性があり、プログラムが突然強制終了したり、強制終了しないもののプログラムの動作が異常になったり、さらには free 関数実行時に強制終了してしまうようなことが起こり得ます。

このメモリの破壊は 解放した後のメモリにアクセスしてはダメ で紹介した解放後のメモリの変更によっても起こり得ますし、解放済みのメモリのアドレスを指定してはダメ で紹介した二重解放もメモリ破壊の原因になり得ます。要は確保中のメモリ以外のメモリを変更すると起こり得ます。

ちなみに、確保したメモリの外側のメモリを変更した瞬間に上記のような現象が必ず起こるというわけではなく、ある程度時間が経ってからいきなりプログラムが突然終了するようなこともありますし、free 関数実行した時に強制終了することもあります。

予期せぬタイミングでプログラムが強制終了するので、非常に原因の特定の難しいバグになります。

MEMO

このメモリ破壊を特にヒープの破壊と呼ぶこともあります

ヒープとは、malloc 関数等での取得先となるメモリ領域のことをいいます

例えば下記の2つの関数は、どちらもメモリを破壊する可能性のある関数になります。なぜメモリ破壊する可能性があるかは関数内にコメントで記載しています(例2に関しては string.h をインクルードする必要があります)。

おそらく下記の関数を実行しても正常にプログラムが動作する場合もあると思います。ですが、そうであったとしても、メモリ破壊が発生する可能性があるので全て良くない関数になります。

メモリ破壊の例1
/* 型のサイズを考慮せずにメモリを確保する例 */
int funcA(void) {
    int *ptr = NULL;
    int i;

    /* 100バイト分のメモリしか確保していない */
    ptr = (int*)malloc(100);
    if (ptr == NULL) {
        return -1;
    }

    /* ptrからint型のサイズ*100バイト分のメモリを変更している */
    for (i = 0; i < 100; i++) {
        ptr[i] = i;
    }

    for (i = 0; i < 100; i++) {
        printf("%d,", ptr[i]);
    }

    free(ptr);

    return 0;
}
メモリ破壊の例2
/* 文字数を考慮せずにメモリを確保する例 */
int funcB(char str[]) { /* 文字列の最後はヌル文字で終端されている前提 */
    char *ptr = NULL;

    /* 100文字分のメモリしか確保していない */
    ptr = (char*)malloc(sizeof(char) * 100);
    if (ptr == NULL) {
        return -1;
    }

    /* strの文字列長が99文字を超えると確保したメモリ外が変更される */
    strcpy(ptr, str);

    printf("%s,", ptr);

    free(ptr);

    return 0;
}

上記2つの関数をメモリ破壊しないように修正したのが下記の関数になります。

メモリ破壊の修正例1
/* 型のサイズを考慮せずにメモリを確保する例の修正例 */
int funcA(void) {
    int *ptr = NULL;
    int i;

    /* int型のサイズ*100バイト分のメモリを確保 */
    ptr = (int*)malloc(sizeof(int) * 100);
    if (ptr == NULL) {
        return -1;
    }

    /* ptrからint型のサイズ*100バイト分のメモリを変更している */
    for (i = 0; i < 100; i++) {
        ptr[i] = i;
    }

    free(ptr);

    return 0;
}
メモリ破壊の修正例2
/* 文字数を考慮せずにメモリを確保する例の修正例 */
int funcB(char str[]) { /* 文字列の最後はヌル文字で終端されている前提 */
    char *ptr = NULL;

    /* 文字数を考慮してメモリを確保する */
    ptr = (char*)malloc(sizeof(char) * (strlen(str) + 1));
    if (ptr == NULL) {
        return -1;
    }

    strcpy(ptr, str);

    printf("%s,", ptr);

    free(ptr);

    return 0;
}

上記は全て、もともと確保したメモリよりも外側のメモリを変更する処理になっていたので、確保するメモリのサイズ(malloc 関数の引数の値)を調整することで修正しています。

では上記の修正後の関数であれば、必ず free が正常に行えるかというと、実はそういうわけでは無いです。それは、他の関数からメモリが破壊されている可能性があるからです。

つまり、他の関数で malloc 関数等で確保したメモリを使用している場合、その関数がメモリを破壊していて、 free 関数で強制終了する等の原因になっている可能性があります。

また、前述の通りメモリが破壊された瞬間にプログラムが強制終了するわけでもないので、どの関数が原因でメモリが破壊されているかを特定するのが難しいです。

free 関数の使い方が正しいのにもかかわらず free 関数で強制終了するような場合、ソースコードの規模が小さいのであれば、malloc 関数等で確保したメモリを変更する箇所を洗い出し、確保したメモリの外側にアクセスしていないかどうかをまず確認してみましょう。

ただ、ソースコードの規模が大きいと動的確保したメモリを変更する箇所を全て確認するのは大変です。ですので、そういった場合はメモリ破壊を検出するようなツールを利用するのが良いと思います。

例えば Linux 環境だと Valgrind などのツールでメモリ破壊を検出することができるはずです。一応私が使っている MacOSX でもインストールはできたのですが、使ってもメモリ破壊もメモリリークも検出してくれませんでした…。

おそらく「ヒープ破壊 検出 C言語 [OS名]」などで検索をすれば、検出するためのツールの情報を得ることができると思います。

まとめ

このページでは、C言語の free 関数についての解説を行いました!

引数や戻り値は単純ではありますが、使い方を誤ると簡単にプログラムが強制終了してしまうこともあるので注意が必要です。

特に free 関数を扱う際には下記に注意するようにしましょう!

また、free 関数実行時にプログラムが強制終了するような場合は、まずは下記のようなことが起こっていないかを調べるのが良いと思います。

free 関数は動的確保したメモリを解放する重要な関数です。ぜひ使い方をマスターしておきましょう!

オススメの参考書(PR)

C言語一通り勉強したけど「ポインタがよく分からない」「ポインタの理解があやふや」「もっとC言語の理解を深めたい」という方には、下記の「C言語ポインタ完全制覇」がオススメです!

この本の主な内容は下記の通りで、通常の参考書では50ページくらいで解説するポインタを、この本では約 "360ページ" 使って幅広く・深く解説しています。

  • C言語でのメモリの使い方
  • 配列とポインタの関係性
  • ポインタのよくある使い方
  • ポインタの効果的な使い方

一通りC言語を学んだだけだと "理解があやふやになってしまいがち" "疑問に思いがち" な内容に対する明確な解説が多いため、特にポインタやC言語の理解があやふやという方にはオススメの本です。

また、C言語においてポインタはまさに "肝" となる機能ですので、ポインタについてより深く学ぶことでC言語全体の理解を深めることにもつながります。

ポインタ・C言語についてより深く理解するための本としては現状1番のオススメの本です。

ただし、他の入門書等で "一通りC言語を学んでいる" 方向けの解説になっているので、"C言語を始めるにあたっての最初の入門書" として利用すると難易度が高いので注意してください。

入門用のオススメ参考書は下記ページで紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示