🙆
C言語でのメモリ管理入門
はじめに
C は開発者自身がメモリを確保し、使い終えたら開放する責任を負う言語です
ガベージコレクションは存在しないため、メモリリークや二重解放などのバグが発生しやすく、しばしば重大な脆弱性やクラッシュを引き起こします
本稿では スタック と ヒープ の違いから malloc
/free
の正しい使い方、デバッグツール、そして小さな動的配列ライブラリを実装するちょっとしたハンズオンまでを網羅し、実際に手を動かして C のメモリ管理を完全に理解します
メモリ領域の全体像
- スタック : 自動変数を置く高速な領域(関数呼び出し単位で自動割り当て・自動解除)
- ヒープ :
malloc
系で動的に確保する領域(サイズを実行時に決められる) - 静的 / グローバル領域 : プログラム起動時に 1 度だけ確保され、終了まで残る
- リテラル領域 : 文字列定数など読み取り専用データを保持する
スタックの特徴と注意点
- 割り当ても解除もコンパイラが自動生成するので高速
- 通常はコンパイル時にフレームサイズが決定する
- フレーム外参照(ダングリング)は未定義動作
int *danger(void) {
int x = 42; /* スタック上 */
return &x; /* x は関数終了で消え、返却ポインタは無効 */
}
ヒープの特徴と四関数
関数 | 役割 |
---|---|
void* malloc(size_t n) |
任意バイト数を確保(初期化なし) |
void* calloc(size_t k, size_t n) |
k 個 × n バイトを確保しゼロ初期化 |
void* realloc(void* p, size_t n) |
既存領域 p を n バイトに拡張/縮小 |
void free(void* p) |
確保した領域を OS へ返却 |
ヒープは明示的に free
しない限り保持され続けるため、対応表を必ず守る
malloc / free の基本パターン
- 要素数と型サイズを計算する
- ポインタに代入し直後に NULL チェックする
- 使い終わったら必ず
free
し、ポインタを NULL にリセット
size_t n = 100;
int *arr = malloc(sizeof(int) * n);
if (!arr) { /* 失敗処理 */ }
/* …利用… */
free(arr);
arr = NULL; /* ダングリング防止 */
realloc と calloc の安全な使い方
-
calloc
はゼロクリア済みなので構造体配列で便利 -
realloc(p, new)
が失敗するとp
は無改変で残る- 先に一時変数に結果を受け取ってから上書きするのが鉄則
void *tmp = realloc(ptr, bigger);
if (!tmp) { /* 失敗時は ptr が生きている */ }
ptr = tmp;
典型的バグと防止策
バグ | 説明 | 予防法 |
---|---|---|
メモリリーク |
malloc に対し free 不足 |
所有権のはっきりした設計・ツール検出 |
ダングリングポインタ | 解放後にポインタ経由でアクセス |
free 後に NULL 代入 |
二重解放 | 同じアドレスを 2 回 free
|
ポインタを唯一に保ち NULL 化 |
未確保アクセス | 配列境界外や NULL 参照 | 境界チェック・NULL チェック徹底 |
ハンズオン: 可変長 int_vector の実装
- 構造体定義
typedef struct {
int *data; /* 連続領域の先頭アドレス(ヒープ)*/
size_t len; /* 現在の要素数 (length) */
size_t cap; /* 確保している最大要素数 (capacity) */
} int_vector;
- 初期化
void iv_init(int_vector *v) {
v->data = NULL; /* まだメモリを持たない */
v->len = 0; /* 要素なし */
v->cap = 0; /* 容量ゼロ */
}
-
要素追加 (push)
- 容量が足りなければ 2 倍拡張
-
realloc
失敗なら即終了(本番ではエラー返却)
void iv_push(int_vector *v, int value) {
if (v->len == v->cap) { /* 容量不足? */
size_t new_cap = v->cap ? v->cap * 2 : 4; /* 0→4→8→16… */
int *tmp = realloc(v->data, sizeof(int) * new_cap);
if (!tmp) { perror("realloc"); exit(1); } /* メモリ不足 */
v->data = tmp; /* 新アドレス */
v->cap = new_cap; /* 容量更新 */
}
v->data[v->len++] = value; /* 末尾に格納して len++ */
}
- 破棄
void iv_free(int_vector *v) {
free(v->data); /* ヒープを OS へ返却 */
v->data = NULL; /* ダングリング防止 */
v->len = 0; /* 使わないので 0 */
v->cap = 0; /* 容量も 0 */
}
- 使ってみる
int main(void) {
int_vector v;
iv_init(&v); /* 初期化 */
for (int i = 0; i < 10; ++i) /* 0‒9 を格納 */
iv_push(&v, i);
for (size_t i = 0; i < v.len; ++i) /* len だけ回す */
printf("%d\n", v.data[i]);
iv_free(&v); /* 必ず解放 */
return 0;
}
デバッグツール活用
-
Valgrind (Linux) :
valgrind --leak-check=full ./a.out
でリークと不正アクセス検出 -
AddressSanitizer (GCC/Clang) :
-fsanitize=address
を付けてビルドすると実行時に即 abort - Dr.Memory / mtrace / Electric Fence : OS や目的に合わせて選択
まとめ
- スタックは自動、ヒープは手動――この違いをまず頭に入れる
-
malloc
系 API は NULL チェック → 利用 → 対応するfree
の三点セット -
realloc
失敗時のロールバックを忘れない - 開発中は Valgrind や ASan を常時オンにして早期にバグを潰す
- 小さな動的配列や文字列ライブラリを書いてみると実戦的な理解が深まる
このハンズオンを通じて、C 言語でのメモリ管理を安全かつ効率的に行う基礎体力が身に付きます
Discussion