本文へスキップ
BecomeCoder

C言語コース · 第8章 動的メモリ管理 · レッスン34

よくあるバグ ― リーク・ダングリング・ダブルフリー

ブラウザで完結

導入

malloc/free を手で対応させるルールは単純ですが、破ると3つの典型的なバグが起こります。メモリリーク(free忘れ)、ダングリングポインタ(解放済みの場所を使う)、ダブルフリー(同じ場所を2回解放する)です。それぞれ「悪い例」と「直した例」を見比べて、なぜ危険か・どう防ぐかを押さえましょう。

説明

まずはメモリリークです。確保したのに free を忘れると、そのメモリは二度と使えないまま残ってしまいます(プログラムが終了すればOSが回収しますが、長時間動くプログラムでは確保のたびに使えるメモリが減っていきます)。

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));   // 確保
    *p = 42;
    printf("%d\n", *p);
    // free(p); を書き忘れると、このメモリは二度と解放されない → メモリリーク
    return 0;
}

直した例は、使い終わったら必ず free を呼ぶだけです。

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 42;
    printf("%d\n", *p);
    free(p);      // 使い終わったら必ず解放する
    return 0;
}

次はダングリングポインタです。free した後もポインタ自体(住所を覚えた変数)は残っているので、うっかりそのまま使ってしまうことがあります。解放後の領域は「もう自分のものではない」ため、何が入っているかは保証されません。この例は危険なので実際には実行しないでください。

// ❌ 危険な例(実行しない)
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 10;
    free(p);              // ここで p の指す場所は「解放済み」になる
    printf("%d\n", *p);   // 解放済みの場所を読んでいる(ダングリングポインタ)。結果は保証されない
    return 0;
}

直し方は「解放したら、その場でポインタに NULL を入れておく」「使う前に NULL チェックする」の2つです。

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 10;
    free(p);
    p = NULL;              // 解放したらすぐ NULL にしておく

    if (p != NULL) {       // 使う前に必ずチェックする
        printf("%d\n", *p);
    } else {
        printf("解放済み\n");
    }
    return 0;
}

最後はダブルフリーです。同じポインタを2回 free すると、内部の管理情報が壊れ、以降の malloc/free がおかしくなることがあります。この例も危険なので実際には実行しないでください。

// ❌ 危険な例(実行しない)
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 5;
    free(p);
    free(p);   // 同じポインタをもう一度 free している(ダブルフリー)
    return 0;
}

ここでも「解放したら NULL を入れる」が効きます。free(NULL) は「何もしない」と規格で決められた安全な呼び出しなので、誤って2回目を呼んでも壊れません。

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 5;
    free(p);
    p = NULL;
    free(p);         // NULL の free は安全(何もしない)
    printf("解放済み\n");
    return 0;
}
flowchart TB
  a["malloc"] --> b["使う"]
  b -->|"free を忘れる"| leak["メモリリーク"]
  b -->|"free した後もpを使う"| dang["ダングリングポインタ"]
  b -->|"free を2回呼ぶ"| dbl["ダブルフリー"]
  b -->|"free の後 p = NULL"| safe["安全(3つとも防げる)"]

やってみよう

上の「直した例」(リーク対策・ダングリング対策・ダブルフリー対策)を実行して、いずれも問題なく動くことを確かめましょう。「危険な例」とコメントされたコードは、コードを読んで理解するだけにして、実際には書き換えて実行しないでください。

演習

int *p = malloc(sizeof(int)); で確保し、*p = 100; を入れて printf("%d\n", *p); で表示したあと、free(p); で解放し、続けて printf("解放済み\n"); を表示してください(p を使い回さない、正しい後始末の形です)。

ヒント1を見る

printffree(p);printf("解放済み\n"); の順に書きます。

ヒント2を見る

free の後に p を読み書きするコードを書かないのが正解です。

実際に動かしてみよう

下のエディタにCを書いて「コンパイル & 実行」を押すと、ブラウザ内で本物のCコンパイラ(clang + dlmalloc)がその場でコンパイルして実行し、出力が表示されます。構造体・共用体・malloc/free・関数ポインタ・ファイル入出力などがそのまま動きます。本文の例を書き換えて試しながら進めましょう(初回だけコンパイラの読み込みに時間がかかります。以降はキャッシュされます。この回は標準入力(scanf)は使わず、値はコードに直接書きます)。

C/C++ — ブラウザ内で本物のコンパイラで実行

ブラウザ内で本物のC/C++コンパイラ(clang + libc++ / WebAssembly)を動かします。構造体・malloc/free・std::string・vector・クラス・テンプレート・STL などがそのまま実行できます(初回のみコンパイラの読み込みに時間がかかります。以降はブラウザにキャッシュされます)。
スクロールして表示された時点でも自動で読み込まれます。