C言語の動的メモリ、なんだか難しそう…って思っていませんか?
プログラムを動かしている最中にメモリを確保したり、解放したりする仕組み、それが動的メモリ割り当てです。今回は、その主役となる `malloc` や `free` といった関数たちの使い方から、うっかりやりがちな失敗(メモリリークとか!)まで、まるっと解説しちゃいます。
この記事を読み終わるころには、メモリ管理に自信が持てるようになっているはず!
この記事で学べること
- 動的メモリ割り当てって何? なんで便利なの?
- メモリ確保の基本! `malloc` 関数の使い方
- 初期化もおまかせ! `calloc` 関数って?
- メモリサイズ変更! `realloc` 関数の使い方
- 超大事! メモリ解放の `free` 関数とその必要性
- メモリリークって何? どう防ぐ?
- 動的メモリを使う上での注意点まとめ
C言語の動的メモリ割り当てとは? なぜ必要?
まず、基本からいきましょう! プログラムが動くためには、データや計算結果を一時的に置いておく場所、つまり「メモリ」が必要です。
普段、変数を宣言するとき、例えば `int score[100];` みたいに書きますよね。これは「静的メモリ割り当て」といって、プログラムを実行する前に「整数100個分の場所をください!」とコンパイラにお願いする方法です。
でも、プログラムによっては、実行してみないとどれくらいのメモリが必要になるか分からない場合があります。「ユーザーが入力した数だけデータを保存したい」とかね。
そんな時に活躍するのが「動的メモリ割り当て」! これは、プログラムの実行中に「今、これくらいのメモリが必要になったから貸して!」とOS(メモリの管理人さんみたいなもの)にお願いして、メモリを借りてくる仕組みなんです。
プログラム実行中にメモリを確保する仕組み
動的メモリ割り当てでは、プログラムが動いているまさにその瞬間に、必要なサイズのメモリ領域を確保します。
このメモリは、プログラムが自由に使える「ヒープ領域」と呼ばれる、だだっ広い空き地のような場所から借りてきます。
メモリのイメージ: +-------------------+ | プログラムコード | +-------------------+ | グローバル変数 | | 静的変数 | <-- 静的メモリ (コンパイル時にサイズ決定) +-------------------+ | ... | +-------------------+ | スタック領域 | <-- 関数呼び出しとかで使う (自動で管理) | (関数のローカル変数)| +-------------------+ | ↓ | | 空き | | ↑ | +-------------------+ | ヒープ領域 | <-- ★動的メモリはここから確保! | (mallocなどで確保) | +-------------------+
プログラムの実行状況に応じてメモリ量を調整できるので、無駄遣いが減ったり、大きなデータを扱えるようになったりするのが大きなメリットです。
静的メモリ割り当てとの違いと比較
ここで、静的メモリ割り当てと動的メモリ割り当ての違いを整理しておきましょう。
【静的メモリ割り当て】
- いつ決まる?
- プログラムを作る時(コンパイル時)にサイズが決まる。
- メリット
- 確保・解放の手間がいらない。処理が速いことが多い。
- デメリット
- サイズを後から変えられない。必要以上に確保すると無駄になる。
- 例
- `int data[100];`
【動的メモリ割り当て】
- いつ決まる?
- プログラムを実行している時(実行時)にサイズを決められる。
- メリット
- 必要な分だけメモリを使える。大きなデータも扱える。
- デメリット
- 自分で確保(`malloc`など)と解放(`free`)をする必要がある。少し処理が遅くなることがある。解放し忘れるとメモリリークになる!
- 例
- `malloc` や `calloc` を使う。
どっちが良い悪いではなく、状況によって使い分けるのが賢いやり方ですね!
C言語の動的メモリ確保の基本関数 `malloc`
さあ、いよいよ動的メモリ確保の主役登場! `malloc` 関数です。
`malloc` は "memory allocation" の略で、指定したサイズのメモリブロックをヒープ領域から確保してくれます。
関数の形: `void* malloc(size_t size);`
- 引数 `size`
- 確保したいメモリのバイト数を指定します。「〇〇バイト分の土地をください!」とお願いするイメージ。
- 戻り値
- 確保できたメモリ領域の先頭アドレスを指すポインタ (`void*` 型) を返します。もしメモリが足りなくて確保に失敗した場合は、`NULL` という特別な値を返します。この `NULL` チェックが超重要!
`void*` 型っていうのは、「どんな型のデータを入れる場所かまだ決まってないポインタ」という意味。使うときは、ちゃんと「これは整数を入れる場所だぞ!」みたいに型キャスト(型変換)してあげる必要があります。
`malloc` の使い方と書き方
`malloc` を使うときの基本的な流れはこんな感じです。
- ヘッダファイルのインクルード:まず、おまじないのように `#include <stdlib.h>` を書きます。`malloc` や `free` を使うにはこれが必要。
- メモリ確保とポインタへの代入:`malloc` を呼び出して、確保したいバイト数を指定。戻り値を適切な型のポインタ変数に代入します。この時、`sizeof` 演算子を使うと便利で間違いが少ないです。
- NULLチェック:メモリ確保が成功したか必ず確認します。もし戻り値が `NULL` なら、メモリが足りなかったということなので、エラー処理をします。
- メモリの使用:確保したメモリ領域をポインタ経由で使います。配列のようにアクセスしたりできます。
- メモリの解放:使い終わったら、必ず `free` 関数で解放します(後で詳しく説明します)。
例えば、整数(int)を5個格納できるメモリを確保する場合:
#include <stdio.h> #include <stdlib.h> // malloc, free を使うのに必要 int main(void) { int *ptr; // 確保したメモリのアドレスを入れるポインタ変数 int num = 5; // 1. メモリ確保 (int型 5個分のサイズをバイトで指定) // sizeof(int) * num で必要なバイト数を計算 // (int *) で void* 型を int* 型にキャスト ptr = (int *)malloc(sizeof(int) * num); // 2. NULLチェック (超重要!) if (ptr == NULL) { printf("メモリ確保に失敗しました。\n"); return 1; // エラー終了 } // 3. メモリの使用 (確保した領域に値を代入) printf("メモリ確保成功! アドレス: %p\n", ptr); for (int i = 0; i < num; i++) { ptr[i] = i * 10; // 配列のようにアクセスできる printf("ptr[%d] = %d\n", i, ptr[i]); } // 4. メモリの解放 (使い終わったら必ず!) free(ptr); printf("メモリを解放しました。\n"); // freeした後のポインタにアクセスするのは危険! // ptr = NULL; // 安全のためNULLを入れておくのも良い return 0; }
`malloc` のサンプルコードと実行結果
上のコードを少し改造して、ユーザーに入力してもらった数だけ整数を格納するメモリを確保するプログラムを作ってみましょう。
// --- ソースコード (malloc_sample.c) --- #include <stdio.h> #include <stdlib.h> int main(void) { int *scores; // 点数を入れるためのポインタ int num_students; printf("生徒の人数を入力してください: "); scanf("%d", &num_students); // 生徒の人数分のint型メモリを確保 scores = (int *)malloc(sizeof(int) * num_students); // NULLチェック if (scores == NULL) { printf("メモリ確保に失敗しました。\n"); return 1; } printf("%d人分のメモリを確保しました。\n", num_students); // 点数を入力してもらう for (int i = 0; i < num_students; i++) { printf("%d人目の点数: ", i + 1); scanf("%d", &scores[i]); // 配列のようにアクセス } // 合計点を計算して表示 int sum = 0; printf("\n--- 入力された点数 ---\n"); for (int i = 0; i < num_students; i++) { printf("%d人目: %d点\n", i + 1, scores[i]); sum += scores[i]; } printf("---------------------\n"); printf("合計点: %d点\n", sum); printf("平均点: %.2f点\n", (double)sum / num_students); // メモリ解放 free(scores); printf("メモリを解放しました。\n"); return 0; }
// --- 実行結果の例 --- 生徒の人数を入力してください: 3 3人分のメモリを確保しました。 1人目の点数: 85 2人目の点数: 92 3人目の点数: 78 --- 入力された点数 --- 1人目: 85点 2人目: 92点 3人目: 78点 --------------------- 合計点: 255点 平均点: 85.00点 メモリを解放しました。
こんな風に、プログラム実行時に入力された人数に合わせて、ピッタリのサイズのメモリを用意できるのが `malloc` の良いところですね!
初期化も行うメモリ確保関数 `calloc`
`calloc` 関数も `malloc` と同じようにメモリを確保するための関数ですが、ちょっとした違いがあります。
関数の形:`void* calloc(size_t num, size_t size);`
- 引数 `num`
- 確保したい要素の数。
- 引数 `size`
- 1要素あたりのバイト数。
- 戻り値
- `malloc` と同じく、確保したメモリへのポインタ (`void*`) か、失敗したら `NULL`。
`malloc` との大きな違いは2つ。
- 引数の指定方法:`malloc` は合計バイト数を1つの引数で指定しますが、`calloc` は「要素数」と「1要素のサイズ」を別々に指定します。内部的には `num * size` バイトのメモリが確保されます。
- メモリの初期化:`calloc` で確保されたメモリは、全てのビットが0で初期化されます。これが一番の違い! `malloc` は確保しただけで、中身は何が入っているか分かりません(ゴミデータが入っている可能性がある)。
整数(int)を5個確保する場合の比較:
// mallocの場合 ptr = (int *)malloc(sizeof(int) * 5); // 中身は不定 // callocの場合 ptr = (int *)calloc(5, sizeof(int)); // 中身は全て0で初期化される
確保したメモリをすぐに0クリアしたい場合には、`calloc` を使うとコードが少しスッキリしますね。
メモリサイズを変更する `realloc`
一度 `malloc` や `calloc` で確保したメモリのサイズを、後から変更したくなることもありますよね。
「思ったよりデータが増えたから、もっと広い場所が欲しい!」とか、「逆に、こんなに要らなかったから狭くしたい…」とか。
そんな時に使うのが `realloc` 関数です。
関数の形:`void* realloc(void* ptr, size_t new_size);`
- 引数 `ptr`
- サイズを変更したい、既に確保済みのメモリ領域へのポインタ。もし `NULL` を渡すと、`malloc(new_size)` と同じ動きになります。
- 引数 `new_size`
- 新しく確保したいメモリのバイト数。
- 戻り値
- サイズ変更後のメモリ領域へのポインタ (`void*`) か、失敗したら `NULL`。注意点として、サイズ変更のためにメモリ内の別の場所にデータが移動することがあります。その場合、元の `ptr` とは違うアドレスが返ってくる可能性があります!
【使い方のポイント】
- サイズを増やす場合
- 元のデータは維持されたまま、後ろに領域が追加されます(場所が移動する可能性あり)。
- サイズを減らす場合
- 元のデータの先頭から `new_size` バイト分だけが残り、はみ出した部分は失われます。
- 失敗した場合
- `realloc` が `NULL` を返した場合、元のメモリブロック (`ptr` が指す場所) は解放されずにそのまま残っています。なので、`realloc` の戻り値は、元のポインタ変数とは別の変数に一時的に格納して `NULL` チェックするのが安全です。
安全な `realloc` の書き方例:
int *ptr = (int *)malloc(sizeof(int) * 5); if (ptr == NULL) { /* エラー処理 */ } // サイズを10個分に増やしたい int new_size = 10; int *tmp_ptr; tmp_ptr = (int *)realloc(ptr, sizeof(int) * new_size); // NULLチェック (realloc失敗の確認) if (tmp_ptr == NULL) { printf("メモリの再確保に失敗しました。\n"); // この時点では ptr はまだ有効なので、必要なら解放する free(ptr); return 1; } // 成功したら、新しいポインタを元の変数に入れる ptr = tmp_ptr; printf("メモリサイズを%d個分に変更しました。\n", new_size); // ... ptr を使った処理 ... free(ptr); // 使い終わったら解放
`realloc` は便利ですが、挙動が少し複雑なので注意して使いましょう。
C言語で確保した動的メモリを解放する `free`
さて、`malloc`, `calloc`, `realloc` でメモリを「借りて」きましたが、借りたものは返すのが世の常、プログラムの世界でも同じです!
使い終わった動的メモリをシステム(OS)に「もう使わないので返します!」と伝えるのが `free` 関数の役割です。
関数の形:`void free(void* ptr);`
- 引数 `ptr`
- `malloc`, `calloc`, `realloc` で確保したメモリ領域へのポインタを指定します。
- 戻り値
- ありません (`void`)。
使い方はとってもシンプル。確保した時のポインタを `free` に渡すだけ!
`free` の重要性と使い方
なぜ `free` がそんなに重要なのか? それは、`free` しないと、そのメモリ領域はプログラムが終了するまで「使用中」として確保され続けてしまうからです。
これを繰り返すと、使えるメモリがどんどん減っていき、最終的には新しいメモリ確保ができなくなったり、システム全体の動作が不安定になったりします。これが悪名高い「メモリリーク」です。
`malloc`/`calloc`/`realloc` でメモリを確保したら、不要になった時点で必ず対応する `free` を呼び出す! これを鉄の掟として覚えてください。
使い方の例:
int *data = (int *)malloc(sizeof(int) * 10); // 確保 if (data == NULL) { /* エラー処理 */ } // ... data を使った処理 ... free(data); // 解放! これで一安心 // data = NULL; // 解放後にNULLを入れておくと、さらに安全 (後述)
`free` し忘れるとどうなる?(メモリリーク)
先ほども触れましたが、`free` のし忘れは「メモリリーク (memory leak)」を引き起こします。
蛇口から水がポタポタ漏れ続けるように、確保したメモリが解放されずに少しずつ溜まっていくイメージです。
メモリリークのイメージ: [確保] → [使う] → [freeし忘れ] → [また確保] → [使う] → [freeし忘れ] ... どんどんヒープ領域の空きが減っていく... || V 最終的にメモリ不足に! (mallocなどがNULLを返すようになる)
短いプログラムなら問題にならないこともありますが、長時間動作するプログラムや、何度もメモリ確保・解放を繰り返すプログラムでは、メモリリークは致命的なバグになります。
メモリを確保したら、どこで解放するかを常に意識する癖をつけましょう! デバッグツールなどを使ってメモリリークをチェックする方法もあります。
C言語の動的メモリ利用時の注意点
動的メモリは強力な武器ですが、使い方を間違えると足を撃ち抜くことにもなりかねません(痛い!)。
ここでは、特に注意したいポイントをいくつかまとめます。
NULLポインタチェックの徹底
これはもう、耳にタコができるくらい言いますが、本当に大事です!
`malloc`, `calloc`, `realloc` は、メモリ確保に失敗すると `NULL` を返します。
もし `NULL` が返ってきたのに、それに気づかずそのポインタを使おうとすると…? プログラムは十中八九、異常終了(クラッシュ)します。
メモリ確保関数の呼び出し直後には、必ず `if (ptr == NULL)` のようなチェックを入れて、失敗した場合の処理(エラーメッセージ表示、プログラム終了など)を書きましょう。
ptr = malloc(サイズ); if (ptr == NULL) { // メモリ確保失敗! perror("malloc failed"); // エラーメッセージ表示 exit(EXIT_FAILURE); // プログラム異常終了 } // ここに来たら、ptrは有効なメモリアドレスを指している
解放済みメモリへのアクセス(ダングリングポインタ)
一度 `free` したメモリ領域は、もうあなたの物ではありません。システムに返却済みです。
それなのに、`free` する前のポインタ変数(これをダングリングポインタ、宙ぶらりんのポインタと呼びます)を使って、解放済みの領域にアクセスしようとすると、何が起こるか分かりません。
運良く動くこともあるかもしれませんが、大抵は予期せぬバグやクラッシュの原因になります。そこにはもう別のデータが書き込まれているかもしれないのですから!
対策として、`free` した直後に、そのポインタ変数に `NULL` を代入しておくのが良い習慣です。
free(ptr); ptr = NULL; // これで、うっかりアクセスしようとしても NULL アクセスとなり、 // 多くの場合、すぐにエラーとして検出されやすい。
こうしておけば、もし間違って `ptr` を使おうとしても、NULL ポインタへのアクセスとなり、問題を発見しやすくなります。
他にも、確保したメモリサイズを超えて書き込みを行う「バッファオーバーフロー」も非常に危険な問題です。配列の範囲外アクセスと同じですね。確保したサイズを常に意識して、範囲内にアクセスするように注意しましょう。
【まとめ】C言語の動的メモリをマスターしよう
お疲れさまでした! C言語の動的メモリ割り当てについて、`malloc`, `calloc`, `realloc` でのメモリ確保から、`free` での解放、そして注意点まで一通り見てきました。
今回のポイントをまとめると、
- 動的メモリはプログラム実行中にサイズを決めてメモリを確保・解放できる仕組み。
- 確保には `malloc` (基本), `calloc` (初期化付き), `realloc` (サイズ変更)。
- 確保したら必ず `free` で解放する! メモリリークは大敵!
- `malloc` などの後は必ず `NULL` チェックを行う!
- `free` した後のポインタ (`ptr = NULL;`) や、範囲外アクセスに注意!
最初はちょっと戸惑うかもしれませんが、動的メモリを使いこなせるようになると、作れるプログラムの幅がぐっと広がります。
今回学んだことをしっかり押さえて、恐れずにどんどんコードを書いて試してみてください。
失敗しても大丈夫、そこから学べばいいんです! 自信を持って、C言語プログラミングを楽しんでいきましょう!
【関連記事】
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。