C言語のメモリ管理 安全装置のない拳銃
C言語はAT&Tベル研究所のリッチーが中心になって作りました。
1972年ころに作られましたので、非常に古いプログラミング言語です。
高級言語はほぼない時代だったからか、扱いを間違うと非常に危険です。
ちゃんと理解していなければ、すぐに致命的なバグを作り出してしまいます。
今回の記事では、C言語のメモリ管理の危険さを説明します。
C言語を使わざるをえなくなった方は、使い方をしっかりと覚えてなるべくバグを作らないようにしてください。
ローカル変数はスタック領域
スタック領域とか意識していますか?
プログラムを実行するときにプログラム自体をメモリに載せます。その領域をスタック領域といいます。
スタック領域は小さいです。エンタプライズ用途でも数MBしかありません。
void funcA(){ int a = 0; }
と関数内で宣言した場合、int型が4バイトの場合、4バイトがスタック領域に確保されます。
※以下、intは4バイトとします。大昔は2バイトだったこともあり、今では8バイトのOSもあるようです
これは配列を宣言した場合でも同様です。
void funcA(){ int i; int a[10]; for (i=0; i<10; ++i){ a[i] = i; } }
a[i], i=0,,,9の10個分、4バイト*10 = 40バイトがスタック領域に確保されます。
配列の大きさはどんなに大きくしてもコンパイルは通りますが、実行時にエラーになります。
void funcA(){ int i; int a[1000000000]; for (i=0; i<1000000000; ++i){ a[i] = i; } }
1G個分、4バイト*1G = 4GBがスタック領域に確保されます。
コンパイラはこれに対してエラー通告しません。※困ったことです
しかし、実行してみるとスタックオーバーフローという悲しいやつになります。
ということで、C言語ではローカル変数で大きいサイズの変数を宣言してはいけません。
大きさは各プロジェクトで決められると思いますが、組み込み系では数KB、アプリ系でも数十KB~1MB以上はダメです。
組み込みの人は1KBを超えるメモリをローカル変数に宣言されると動悸がしてきますので、そういうコードは見せないようにしてください。
ヒープ領域 malloc()とfree()
ローカル変数で大きいメモリは禁止なんですが、大きい配列を使いたい、というときはどうしたらいいか?
スタック領域ではなくヒープ領域に動的に確保します。
スタック領域がプログラム自体が格納される小さい領域ですが、ヒープ領域はプログラム実行時動的に確保される大きい領域です。
malloc()という関数でメモリを確保しfree()で解放します。
void funcA(){ int i; int* a; a = (int*)malloc(1000000000 * sizeof(int));// ヒープ領域にメモリ確保 for (i=0; i<1000000000; ++i){ a[i] = i; } free(a); // メモリ解放 }
※4GB分のメモリを確保して触ったんですが、もちろん物理的にメモリが1GBしかないようなPCで実行するとまずいと思います。PCがフリーズしたりするので、物理メモリより小さい数字で試して下さい。
malloc()の使い方は慣れないと難しいかもです。
まず、確保したい変数はポインタ型で宣言します(int* a;)。上記のaは「intのポインタ型」です。
malloc()の引数は確保したいバイト数を入れます。sizeof(int)がint型のサイズなので、それに1Gをかけます。
malloc()はvoid*が返りなので、(int*)でキャストしなければなりません。
そして、使い終わったらfree()をして確保したメモリを解放する必要があります。
使い方を把握する リークと二重フリーとメモリ破壊
「malloc()でメモリを確保して、使い終わったらfree()する」
というのがルールですが、注意点があります。いずれもコンパイルエラーになりません。
「実行時エラー」か「実行してしばらくして気付く」ものです。C言語のメモリ管理が最悪と言われるゆえんです。
メモリリーク
以下のようにfree()を忘れると、確保したメモリが解放されずに残ります。これをメモリリークと言います。
void funcA(){ int i; int* a; a = (int*)malloc(1000000000 * sizeof(int));// ヒープ領域にメモリ確保 for (i=0; i<1000000000; ++i){ a[i] = i; } // メモリ解放してない }
4GBものメモリのfree()を忘れたらさすがにすぐに気づくと思いますが、数十KBのfree()忘れなど、数年して初めて気づくというきついものになります。
二重free
メモリ確保していないポインタをfree()してはいけません。
void funcA(){ int i; int* a; free(a);//メモリ確保していないのにfree }
free()済のポインタもfree()してはいけません。
void funcA(){ int i; int* a; a = (int*)malloc(1000000000 * sizeof(int));// ヒープ領域にメモリ確保 for (i=0; i<1000000000; ++i){ a[i] = i; } free(a)// メモリ解放 free(a)// free()済のaをまたfree() }
メモリ破壊
確保していないメモリを触るのをメモリ破壊となります。
void funcA(){ int i; int* a; // メモリ確保を忘れる for (i=0; i<1000000000; ++i){ a[i] = i; } free(a)// メモリ解放 }
解放した後に触るのもメモリ破壊となります。
void funcA(){ int i; int* a; a = (int*)malloc(1000000000 * sizeof(int));// ヒープ領域にメモリ確保 for (i=0; i<1000000000; ++i){ a[i] = i; } free(a)// メモリ解放 a[10] = 12; }
メモリ破壊は非常に分かりにくいバグになります。
少しくらい破壊したところでプログラムの動きが変になるわけではないです。
たまに変な動きをするというホラーが待っています。
C言語のメモリ管理 コーディングルール
コンパイラが守ってくれないので以下のようなコーディングルールを作ることになります。それでも完全ではないです。メモリリークを防ぐルールはありません。気を付けるしかありません。
- 10KB以上のローカル変数禁止
- ポインタ宣言するときNULLを代入
- free()はNULLでないときのみ使用
- free()したらすぐにNULLを代入
まとめ
C言語のメモリ管理について解説しました。
コメント