メモリの動的な確保と解放

提供: C言語入門
2018年2月11日 (日) 13:06時点におけるDaemon (トーク | 投稿記録)による版

移動: 案内検索
スポンサーリンク

C言語では、グローバル変数、自動変数(ローカル変数)以外に動的に確保するメモリを使用します。malloc,calloc,allocaなどのC言語の標準ライブラリの関数でメモリを確保し、freeでメモリを解放できます。不要になったメモリは、解放しなければなりません。

読み方

malloc
まろっく、えむあろっく
calloc
かろっく、しーあろっく
alloca
あろか
realloc
りあろっく
free
ふりー

概要

ここでは、以下の点について説明します。

  • メモリを動的に確保するメリット
  • メモリを操作する関数
  • 動的メモリにおける注意点
  • 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の違い
関数名 説明
malloc メモリを動的に確保します。メモリ領域は、ゼロクリアされません。
calloc メモリを動的に確保します。確保したメモリを自動的にゼロクリアします。
alloca メモリをスタックフレームから割り当てます。スコープから外れるときに自動的に解放されます。明示的にfreeで解放してはいけません。
realloc メモリを動的に再確保します。渡されたポインタのメモリをコピーします。渡されたポインタの領域は解放します。

メモリの再確保 realloc

malloc()によって確保したメモリが足りなくなったときに、さらにメモリのサイズを大きくする必要があれば、realloc()を使って、メモリサイズを増やすことができます。 ただし、純粋に今確保している領域が後ろに伸びるわけではありません。 適当な場所にメモリを確保しなおし、そこにデータをコピーすることになります。 すでに確保しているメモリが大きいほど、コピーする量も多くなり、スピードが遅くなります。

realloc()の処理の内容を簡単に説明すると以下の流れになります。

  1. メモリの確保(旧領域)
  2. メモリの再確保
    1. メモリの確保(新領域)
    2. 旧領域から新領域へデータのコピー
    3. 旧領域の解放

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;

メモリリーク

不要なメモリを解放せず、メモリを確保し続けて、システムのメモリをだんだん消費していく現象を「メモリリーク」と呼びます。 メモリリーク(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と呼ばれるメモリリークを検出できるプログラムを利用するのが良いでしょう。

ファジングでメモリリークを見つけることもできます。

関連項目




スポンサーリンク