動的メモリ管理

動的メモリはアパートの賃貸のようなものです。必要な時に申し込み(malloc)、使い終わったら退去します(free)。退去しなければずっと資源を占有し続けます。それがメモリリークです。

なぜ動的メモリが必要なのか

スタック上の局所変数は関数が戻ると消え、配列のサイズはコンパイル時に決定する必要があります。データサイズが実行時にならないと分からない場合や、関数呼び出しをまたいでデータを維持したい場合は、ヒープ上の動的メモリが必要です。

C
int n;
scanf("%d", &n);
int arr[n];
⚠️ 注意: 可変長配列(VLA)はC99の機能であり、すべてのコンパイラでサポートされているわけではなく、大きな配列には推奨されません。正しい方法は malloc を使用することです。

mallocとfree

malloc はヒープ上に指定バイト数のメモリを割り当て、void * を返します。使い終わったら free で解放する必要があります。

C
int *p = (int *)malloc(sizeof(int) * 5);
if (p == NULL) {
    printf("Memory allocation failed\n");
    return 1;
}
for (int i = 0; i < 5; i++) {
    p[i] = i * 10;
}
for (int i = 0; i < 5; i++) {
    printf("%d ", p[i]);
}
free(p);
p = NULL;
TEXT
0 10 20 30 40
💡 ヒント: malloc はメモリを初期化しません。内容はゴミ値です。割り当て後は必ず戻り値の NULL チェックを行ってください。free 後にポインタを NULL に設定するのは、ダングリングポインタを防ぐ良い習慣です。

calloc

calloc はメモリを割り当て、すべてのビットを0に初期化します。引数は要素数と各要素のサイズです。

C
int *p = (int *)calloc(5, sizeof(int));
if (p) {
    for (int i = 0; i < 5; i++) {
        printf("%d ", p[i]);
    }
    free(p);
}
TEXT
0 0 0 0 0

malloccalloc の比較:

関数 初期化 引数
malloc 初期化なし 総バイト数
calloc ゼロ埋め 要素数、各要素のサイズ

realloc

realloc は以前に割り当てたメモリのサイズを変更します。拡張も縮小も可能です。

C
int *p = (int *)malloc(sizeof(int) * 3);
p[0] = 10; p[1] = 20; p[2] = 30;

int *tmp = (int *)realloc(p, sizeof(int) * 5);
if (tmp) {
    p = tmp;
    p[3] = 40;
    p[4] = 50;
    for (int i = 0; i < 5; i++) {
        printf("%d ", p[i]);
    }
    free(p);
}
TEXT
10 20 30 40 50
⚠️ 注意: realloc は新しいアドレスを返す場合(元のデータは自動コピーされます)と同じアドレスを返す場合があります。p = realloc(p, ...) と書いてはいけません。失敗して NULL が返った場合、元のポインタが失われます。一時変数で結果を受け取ってください。

動的1次元配列

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

int main(void) {
    int n;
    printf("Enter number of elements: ");
    scanf("%d", &n);

    int *arr = (int *)calloc(n, sizeof(int));
    if (!arr) return 1;

    for (int i = 0; i < n; i++) {
        arr[i] = (i + 1) * (i + 1);
    }

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

    free(arr);
    arr = NULL;
    return 0;
}
▶ 試してみよう
TEXT
Enter number of elements: 5
1 4 9 16 25

動的2次元配列

動的2次元配列の実装には2つの一般的な方法があります。

方法1:ポインタ配列(各行を独立して割り当て)

C
int rows = 3, cols = 4;
int matrix = (int )malloc(sizeof(int *) * rows);
for (int i = 0; i < rows; i++) {
    matrix[i] = (int *)calloc(cols, sizeof(int));
}

matrix[1][2] = 99;
printf("%d\n", matrix[1][2]);

for (int i = 0; i < rows; i++) {
    free(matrix[i]);
}
free(matrix);
TEXT
99

方法2:連続メモリ(一括割り当て)

C
int rows = 3, cols = 4;
int *buf = (int *)calloc(rows * cols, sizeof(int));
int matrix = (int )malloc(sizeof(int *) * rows);
for (int i = 0; i < rows; i++) {
    matrix[i] = buf + i * cols;
}

matrix[2][3] = 77;
printf("%d\n", matrix[2][3]);

free(matrix);
free(buf);
TEXT
77
💡 ヒント: 方法2はメモリが連続しており、キャッシュに優れ、解放も簡単です。方法1は各行で異なる列数を持てます(ジャグ配列)。

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

int main(void) {
    int rows = 3, cols = 4;
    int m = (int )malloc(sizeof(int *) * rows);
    int *buf = (int *)calloc(rows * cols, sizeof(int));
    for (int i = 0; i < rows; i++) {
        m[i] = buf + i * cols;
    }

    int val = 1;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            m[i][j] = val++;
        }
    }

    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%3d", m[i][j]);
        }
        printf("\n");
    }

    free(m);
    free(buf);
    return 0;
}
▶ 試してみよう
TEXT
  1  2  3  4
  5  6  7  8
  9 10 11 12

動的メモリのよくあるエラー

freeの忘れ — メモリリーク

C
void leak(void) {
    int *p = (int *)malloc(sizeof(int) * 100);
}

関数が終了すると局所変数 p は消えますが、ヒープ上の100個分のintメモリはまだ占有されたままで、二度と解放できません。

二重解放

C
int *p = (int *)malloc(sizeof(int));
free(p);
free(p);
⚠️ 注意: 同じメモリを2回 free するのは未定義動作であり、プログラムがクラッシュする可能性があります。free 後にすぐポインタを NULL に設定してください。free(NULL) は安全です。

解放済みメモリの使用

C
int *p = (int *)malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p);
⚠️ 注意: 解放後のメモリにアクセスするのはダングリングポインタであり、結果は予測不能です。

範囲外アクセス

C
int *p = (int *)malloc(sizeof(int) * 5);
p[5] = 100;

5つの要素がインデックス0〜4で割り当てられているため、p[5] は範囲外です。

valgrind入門

ValgrindはLinux上のメモリチェックツールで、メモリリーク、範囲外アクセス、未初期化読み取りを検出できます。

BASH
gcc -g -o myapp myapp.c
valgrind --leak-check=full ./myapp

代表的な出力:

TEXT
==12345== HEAP SUMMARY:
==12345==     in use at exit: 400 bytes in 1 blocks
==12345==   total heap usage: 2 allocs, 1 frees, 800 bytes allocated
==12345==
==12345== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345==    by 0x10915E: leak (myapp.c:5)
💡 ヒント: WindowsではDr. MemoryやVisual Studioのデバッグヒープが使えます。macOSではInstrumentsのLeaksツールを使用してください。

❓ よくある質問

Q mallocが返すメモリにはどんな値が入っていますか?
A 未定義です。以前の使用残りのゴミデータです。ゼロ埋めが必要な場合はcallocを使うか、手動でmemsetを呼び出してください。
Q freeした後もポインタには値が残りますか?
A freeはメモリを解放するだけで、ポインタの値は変更しません。そのアドレスを保持したままです(ダングリングポインタ)。そのため、free直後にNULLに設定すべきです。
Q reallocで拡張すると元のデータは失われますか?
A いいえ。reallocは元のデータが保存されることを保証します(少なくともmin(旧サイズ, 新サイズ)バイト)。新しいアドレスに移動した場合も自動的にコピーされます。
Q スタック変数をfreeできますか?
A 絶対にできません。freeはmalloc/calloc/reallocが返したヒープメモリのみ解放できます。スタックアドレスに対してfreeを呼び出すのは未定義動作です。

📖 まとめ

📝 練習問題

  1. ユーザーがnを入力し、n個のintを動的割り当てし、n個の数値を読み込んでソート・出力し、メモリを解放するプログラムを作成してください
  2. 動的2次元配列関数 int create_matrix(int rows, int cols)void free_matrix(int m, int rows) を作成してください。main関数で4x5の行列を作成し、値を代入して出力してください
  3. 意図的にメモリリークを作り、valgrind(またはDr. Memory)で検出し、出力を確認してください
100%