メモリの動的な確保と解放
C言語では、グローバル変数、自動変数(ローカル変数)以外に動的に確保するメモリを使用します。malloc,calloc,allocaなどのC言語の標準ライブラリの関数でメモリを確保し、freeでメモリを解放できます。不要になったメモリは、解放しなければなりません。
読み方
- malloc
- まろっく、えむあろっく
- calloc
- かろっく、しーあろっく
- alloca
- あろか
- realloc
- りあろっく
- free
- ふりー
目次
- 1 概要
- 2 なぜメモリを動的に確保するのか
- 3 malloc系関数とは
- 4 malloc系以外のメモリを取得する関数
- 5 プロトタイプ
- 6 mallocで確保するメモリのサイズ
- 7 malloc, calloc, alloca の戻り値
- 8 malloc()のエラー
- 9 malloc, calloc, alloca の違い
- 10 メモリの再確保 realloc
- 11 不要なメモリは解放する
- 12 freeする前のフルチェックは不要
- 13 メモリリーク
- 14 スワップやスラッシング
- 15 メモリが足りない場合には
- 16 動的メモリ確保のアルゴリズム
- 17 プログラムが終了するときにメモリを解放するのか
- 18 サンプルコード
- 19 メモリの再確保
- 20 メモリの再利用防止
- 21 メモリリークを発見する
- 22 関連項目
概要
ここでは、以下の点について説明します。
- メモリを動的に確保するメリット
- メモリを操作する関数
- 動的メモリにおける注意点
- malloc,callocなどの使い方(サンプルコード)
C++ユーザのために簡単に説明を入れておきます。C++では、メモリの動的割り当てと解放にnewとdeleteを使用していますが、C言語では、mallocとfreeを使用します。
なぜメモリを動的に確保するのか
なぜ、メモリを動的に確保する必要があるのでしょうか? はじめから必要なメモリの量はわかりますか? プログラムの果たしたい目的によって、メモリを動的に確保しなければならないかどうか、決まります。
必ず、プログラムが起動してすぐに、10MBのメモリを使用して、プログラムがすぐに終了してしまうなら、メモリをmallocで確保する必要がないでしょう。自動変数、グローバル変数で十分でしょう。
malloc/freeを使うと何が嬉しいかというと、 必要な量を必要なときに確保することができます。 例えば、通信プログラムは、クライアントが接続してきたときに、クライアントとのメッセージ交換に使うメッセージ用のバッファを動的に確保し、クライアントが切断したら、そのメッセージバッファは不要になるため、解放することができます。
処理する最大サイズが10MBだけど、いつも10MB必要ではない、プログラムがあったとして、ずっと10MBのメモリを専有し続けるよりも、10MBが必要になったら、10MBのメモリを確保し、3MBのメモリでいいなら、10MBではなく、3MBのメモリだけ確保すればよいのです。
扱いデータの個数やサイズが不明なときや、状況に応じて異なる場合には、動的なメモリの確保と解放のほうが、柔軟にメモリを使用できます。
malloc系関数とは
malloc()系(memory allocation)の関数は、第1引数に必要なサイズを指定し、確保したメモリへのポインタを返します。 メモリのサイズの指定には、sizeof()を使用して「型」のサイズを調べて、必要な数を掛けあわて、サイズを決めることができます。malloc/free は、C言語の標準ライブラリで提供される関数です。
メモリは、いくらでも確保できるわけではありません。いろいろな制約によって使用できるメモリ量はかわってきます。
- OSの上限
- プロセスの上限(OSなどによって制限されます)
- 物理的な制限
- 物理メモリサイズ
- スワップのサイズ
C言語のmalloc/freeには、ガベージコレクション(GC)はありませんので、プログラムが自身でメモリを管理しなければなりません。
malloc系以外のメモリを取得する関数
malloc()以外にもメモリを取得する関数が用意されています。 メモリを確保して、文字列を格納する、といったよくある処理を1つの関数で行えます。
- strdup
- asprintf
これらの関数もmalloc()と同様にfree()で不要になったメモリを解放する必要があります。
プロトタイプ
メモリを操作する関数のプロトタイプは以下の通りです。
#include <stdlib.h> void * malloc(size_t size); void * calloc(size_t number, size_t size); void * realloc(void *ptr, size_t size); void free(void *ptr); void * alloca(size_t size);
mallocで確保するメモリのサイズ
malloc()系の関数は、引数で確保するメモリのサイズを指定します。
size_t size = 256; // char型を256個確保する char *sp = (char *)malloc( sizeof(char) * size ); // int型を256個確保する int *ip = (int *)malloc( sizeof(int) * size ); // double型を256個確保する double *dp = (double *)malloc( sizeof(double) *size ); // 256byte 確保する int *ip = (int *)malloc( size );
malloc, calloc, alloca の戻り値
malloc, calloc, realloc は、取得したメモリのアドレスのポインタを返します。 失敗したときに NULL (ヌルポインタ)を返します。
malloc()系関数の戻り値型は、void*です。各関数の戻り値は、ポインタの型に合わせて、キャストします。
size_t size = 256; char *sp = (char *)malloc( sizeof(char) * size ); int *ip = (int *)malloc( sizeof(int) * size ); double *dp = (double *)malloc( sizeof(double) *size );
malloc()のエラー
malloc()でエラーが発生した場合には、errnoにENOMEMが設定されます。 FreeBSD の /usr/src/lib/libc/gen/errlst.c では、ENOMEMは、以下のメッセージが定義されています。
Cannot allocate memory
malloc()がNULLを返した場合には、メモリが確保できなかったため、エラー処理を書きます。
size_t size = 256; char *sp = (char *)malloc( sizeof(char) * size ); if (NULL == sp) { // エラー時の処理 perror ("can not malloc"); } else { // OKな処理 }
malloc, calloc, alloca の違い
それぞれの関数の違いを説明します。
関数名 | 説明 |
---|---|
malloc | メモリを動的に確保します。メモリ領域は、ゼロクリアされません。 |
calloc | メモリを動的に確保します。確保したメモリを自動的にゼロクリアします。 |
alloca | メモリをスタックフレームから割り当てます。スコープから外れるときに自動的に解放されます。明示的にfreeで解放してはいけません。 |
realloc | メモリを動的に再確保します。渡されたポインタのメモリをコピーします。渡されたポインタの領域は解放します。 |
メモリの再確保 realloc
malloc()によって確保したメモリが足りなくなったときに、さらにメモリのサイズを大きくする必要があれば、realloc()を使って、メモリサイズを増やすことができます。 ただし、純粋に今確保している領域が後ろに伸びるわけではありません。 適当な場所にメモリを確保しなおし、そこにデータをコピーすることになります。 すでに確保しているメモリが大きいほど、コピーする量も多くなり、スピードが遅くなります。
realloc()の処理の内容を簡単に説明すると以下の流れになります。
- メモリの確保(旧領域)
- メモリの再確保
- メモリの確保(新領域)
- 旧領域から新領域へデータのコピー
- 旧領域の解放
realloc()は、確保した新しい領域へのアドレス(ポインタ)を返します。reallocは、必ず成功するとは限りません。realloc()は、失敗したとき NULL を返します。そのため、第一引数に渡す、ポインタで、reallocの戻り値を受け取るべきではありません。
以下は、ダメなコードの例です。
p = (int*)malloc(10 * sizeof(int)); // (1) p = (int*)realloc(p, 20 * sizeof(int)); // (2)
realloc()が失敗したとき、(1)で確保したメモリへのアドレスを失ってしまいます。そうしたとき、(1)のアドレスを操作できなくなってしまいます。当然、free()もできません。
正しいコードは、以下の通りです。
int *p; p = (int*)malloc(10 * sizeof(int)); if (! p) { err(EXIT_FAILURE, "can not malloc"); } int *tmp; tmp = (int*)realloc(p, 20 * sizeof(int)); if (! tmp) { free(p); err(EXIT_FAILURE, "can not realloc"); } p = tmp;
不要なメモリは解放する
確保したメモリは、そのメモリが不要になったときに明示的にfree()しなければなりません。 malloc()で確保したメモリが不要になっても確保しつつ、メモリが必要になったときに、すでに持っているメモリを再利用せずに、さらにmalloc()でメモリを確保していくと、そのうち、システム上のメモリを使い果たしてしまうか、プロセスのメモリ上限に到達してしまい、プログラムがそれ以上メモリが確保できなくなったり、OSのOOM Killer(Out of Memory Killer)にプログラム(プロセス)を終了させられてしまいます。
size_t size = 256; char *sp = (char *)malloc( sizeof(char) * size ); if (NULL == sp) { // エラー時の処理 perror ("can not malloc"); return -1; } // OKな処理を行う。 // もうここで sp のメモリは不要になった。 free(sp); // ここでメモリを解放する。 return 0;
freeする前のフルチェックは不要
下記のコードで示した通り、 free する前に ヌルチェックをする必要はありません。
if (ptr) { free(ptr); }
JIS X3010:2003 の 7.20.3.2 の free 関数の説明では、 ptr が空ポインタ(ヌルポインタ)であれば、何もしない、とされています。
そのため、以下のコードのように、ヌルチェックをせず、シンプルに free だけ書くことができます。
free(ptr);
メモリリーク
不要なメモリを解放せず、メモリを確保し続けて、システムのメモリをだんだん消費していく現象を「メモリリーク」と呼びます。 メモリリーク(Memory leak)は、プログラミングの一種のバグです。
スワップやスラッシング
OSの実装によるため、どのような条件で引き起こされるかは、環境によりますが、一般的なOSでは、実行中のプログラム達によってメモリを消費されていき、物理メモリが足りなくなると、OSはスワップを使いはじめます。OSは、ファイルシステムにメモリ上のデータを逃します。
スワップを使用するとOS、プログラムの動作が遅くなっていきます。 プログラムが必要なデータがスワップにある場合、一度、メモリに戻さなければなりません。メモリのデータをスワップにどかして、スワップからデータを読むといった動作が必要なります。 このメモリとスワップの入れ替えが激しく起きて、プログラムがほとんど停止状態に陥ることをスラッシング(thrashing)と呼びます。
メモリが足りない場合には
不要なメモリは解放し、それでも必要な物理メモリが足りず、スワップを使用してしまう場合には、サーバを増やして、処理を分散させるか(スケールアウト)、物理メモリを増設する(スケールアップ)などの対応が必要です。
動的メモリ確保のアルゴリズム
mallocなどの動的なメモリを確保するアルゴリズムには、いろいろなアルゴリズムがあります。 高速なアルゴリズムとして gperftoolsのtcmalloc があります。 簡単に既存のmallocと置き換えることができるため、パフォーマンスがメモリの動的確保にある場合には、tcmallocを利用することで、パフォーマンスの改善が期待できます。
プログラムが終了するときにメモリを解放するのか
「確保したメモリが不要になったら解放するべきである」と説明しました。 「プログラムが終了するときにメモリを解放する必要があるのか?」という疑問があるでしょうか?
OSの実装によりますが、たいていのOS(UnixやWindows)では、プログラムが終了するとき、OSによって、プログラムが使用していたリソースが自動的に解放されます。 そのため、プログラムが終了するとき、OSによってプログラムが持っていたメモリも一緒に解放されます。 だから、メモリを解放するためのロジックは、作成しておいたとしても、それを呼び出す必要はありません。
メモリを明示的に解放してもいいのですが、複雑なデータ構造をたどって、1つ1つのメモリを解放していくようなロジックを持っている場合、メモリの解放に時間が掛かります。OSに任せられるものは、任せてしまったほうが、OSのパフォーマンスやプログラムの終了処理が速くできる利点があります。
蛇足ですが、メモリの解放とは別に、OSがやってくれないメモリのデータをプログラムでディスクに書き出す必要がある場合には、メモリの解放ではなく、データの書き出しの処理はしなければなりません。
サンプルコード
ソースコード malloc.c
#include <stdio.h> #include <stdlib.h> #include <err.h> int main(int argc, char *argv[]) { int *p = (int*)malloc(sizeof(int)*4); if (NULL == p) { err(EXIT_FAILURE, "can not malloc"); } free(p); exit(EXIT_SUCCESS); }
ソースコード calloc.c
#include <stdio.h> #include <stdlib.h> #include <err.h> int main(int argc, char *argv[]) { int *p = (int*)calloc(4, sizeof(int)); if (NULL == p) { err(EXIT_FAILURE, "can not calloc"); } free(p); exit(EXIT_SUCCESS); }
ソースコード alloca.c
#include <stdio.h> #include <stdlib.h> #include <err.h> int main(int argc, char *argv[]) { int *p = (int*)alloca(sizeof(int)*4); if (NULL == p) { err(EXIT_FAILURE, "can not alloca"); } // alloca は、freeしない。 exit(EXIT_SUCCESS); }
コンパイル
$ cc malloc.c $ cc calloc.c $ cc alloca.c
メモリの再確保
#include <stdio.h> #include <stdlib.h> #include <err.h> int main(void) { int *p; int i; p = malloc(10 * sizeof(int)); if (! p) { err(EXIT_FAILURE, "can not malloc"); } for(i=0;i<10;i++)*(p+i)=i; int *tmp; tmp = realloc(p, 20 * sizeof(int)); // 一度テンポラリで受け取る if (! tmp) { free(p); // 失敗したら自分で解放する err(EXIT_FAILURE, "can not realloc"); } p = tmp; for(i=10;i<20;i++)*(p+i)=i; for(i=0;i<20;i++)printf("%3d\n",*(p+i)); free(p); // プログラムが終了するので、解放しないでもOSがしてくれる。 return 0; }
メモリの再利用防止
解放したアドレスを誤って、再利用するのを防止するため、解放したアドレスを持つポインタには、NULLを入れてしまうのが好ましいです。
#define FREE(p) {free(p);p=NULL} void f(){ int *p = (int*)malloc(sizeof(int)*3); // *p の使用 FREE(p); // p はNULLになってるので、解放されたアドレスを再利用するバグはなくなる。 }
メモリリークを発見する
万が一、メモリリークするようなプログラムのバグを探さなければならない場合は、Valgrindと呼ばれるメモリリークを検出できるプログラムを利用するのが良いでしょう。
ファジングでメモリリークを見つけることもできます。
関連項目
ツイート