【C言語】メモリの解放忘れ(メモリリーク)を自力で検出する方法

メモリ解放忘れの検出方法解説ページのアイキャッチ

C言語プログラミングでついついやってしまいがちなメモリの解放忘れ(malloc したメモリの free 忘れ)。

メモリの解放を忘れるとメモリリークが発生し、特に長時間稼働するようなシステムだと途中でメモリが取得できなくなって停止してしまう可能性があります。

僕もよく忘れちゃうんだよね…
誰でもそうだよ

多分今までにメモリの解放を忘れたことのないC言語プログラマーはいないんじゃないかな

みんな忘れてしまうからこそ、仕組みとして自動的に解放忘れを検出できる仕組みがあると便利だよね?

このメモリの解放忘れはツールなど使えば検出することができますし、自力でプログラム内に仕組みを仕込んで検出することも可能です。

このページでは後者の、メモリの解放忘れを自力で検出する方法について解説していきたいと思います。

メモリの解放忘れを検出する方法

それではまずはメモリの解放忘れを検出する考え方・方法について解説していきます。

解放していないメモリを管理

今回紹介するメモリの解放忘れを検出するための仕組みはシンプルです。

メモリを取得する malloc 関数を実行すると、取得したメモリのアドレスが戻り値として返却されますので、プログラム内で取得したメモリのアドレスを知ることができます。

ですので、取得中のアドレスのメモリを覚えておき、プログラム終了時にその情報を表示してやればメモリの解放忘れを検出することができます。

より具体的には下記のようなことを行えば良いです。

メモリ取得時にそのメモリの情報を記録

まずはメモリ取得時(malloc 関数実行時)に、そのメモリの情報を記録するようにします。

メモリの情報を記録する様子

メモリ取得される度に取得したメモリの情報が記録されていきますので、取得したメモリを管理することができるようになります。

メモリの情報としては、例えば下記のようなものが挙げられます。

  • メモリのアドレス
  • メモリのサイズ
  • メモリを取得した場所(ソースコードのファイル名や行など)

特にアドレスは、取得中のメモリを特定するための識別子のようなものですので、必ず記録しておきましょう。

これらの情報をメンバとする構造体を用意し、さらにその構造体の配列を作成すれば、複数のメモリを管理できるようになります(配列でなくリスト構造などを用いても良いです)。

メモリ管理配列

メモリの取得が行われる度に、取得したメモリの情報を配列の要素に格納して、記録を行なっていきます。

以降では、この構造体の配列のことを、メモリ管理配列と呼ばせていただきます。

メモリ解放時にそのメモリの情報を削除

次に、メモリ解放時(free 関数実行時)に、メモリ管理配列からその解放するメモリの情報を削除するようにします。

メモリの情報を削除する様子

メモリ管理配列の中から解放しようとしているメモリのアドレスが設定されている要素を探し出し、その要素の情報を初期化してやれば、メモリの情報を削除することができます。

プログラム終了時に残っているメモリの情報を表示

メモリを取得した時にメモリ管理配列にそのメモリの情報を記録、メモリを解放した時にメモリ管理配列からそのメモリの情報を削除しているので、メモリ管理配列に残っているのは、現在取得中のメモリの情報のみになります。

したがって、プログラム終了時にメモリ管理配列にメモリの情報が残っている場合は、そのメモリは解放忘れということになります。

ですので、プログラム終了直前にメモリ管理配列に残っている情報を表示してやれば、メモリの解放忘れがあることを通知することができるようになります。

解放忘れのメモリ

メモリの取得・解放前にメモリ管理配列を更新する関数を作成

では具体的にどのようにしてメモリ管理配列に対してメモリの取得・解放時にメモリ情報の記録・削除を行うかについて解説していきます。

一番単純なのは、malloc 関数実行後に取得したメモリの情報を記録・free 関数実行前にメモリの情報を削除する処理を逐一記述する方法です。

ptr1 = malloc(sizeof(int));
ptr1 の指すメモリの情報をメモリ管理配列に記録する処理;

ptr2 = malloc(sizeof(int));
ptr2 の指すメモリの情報を管理配列に記録する処理;

ptr2 の指すメモリの情報を管理配列から削除する処理;
free(ptr2);

ptr1 の指すメモリの情報を管理配列から削除する処理;
free(ptr1);

でもこれだと malloc 関数や free 関数を実行する度に処理を記述する必要があるので面倒です。

ですので、下記のような関数を別途作成し、メモリの情報の追加・削除を行う方が良いです。

leak_detect_malloc 関数
  • malloc 関数を実行
  • malloc 関数実行後に malloc で取得したメモリの情報をメモリ管理配列に記録
  • malloc 関数の戻り値を返却
leak_detect_free 関数
  • free 関数実行前に free で解放するメモリの情報をメモリ管理配列から削除
  • free 関数を実行

あとは malloc や free の代わりに上記の leak_detect_malloc や leak_detect_free を実行するようにすれば、メモリの情報を管理し、メモリの解放忘れを検出できるようになります。

#define を利用して関数の置き換え

うーん、理屈は分かった

でも malloc や free を使用していた箇所を leak_detect_malloc や leak_detect_free に書き換えるの面倒じゃない?

面倒だね!

なので、#define を利用して関数の置き換えを行う方法について解説するよ!

次に、わざわざ malloc や free を使用している箇所の書き換えを行わなくても良いように #define を利用して関数の置き換えを行います。

#define を一番利用する場面は定数の定義だと思います。

#define N 100
for(i = 0; i < N; i++){
    /* ループ処理 */
}

これを記述することで、ソースコードがコンパイルされる前に、第一項で記述した文字列が、第二項で記述した文字列にそのまま置き換えられ、置き換えた後にコンパイルが行われることになります。

#define N 100
for(i = 0; i < 100; i++){
    /* ループ処理 */
}

ポイントは文字列がそのまま置き換えられる点です。

これは関数名にも当てはまりますので、#define に関数名を指定してやればソースコード上の関数名が置き換わり、実行される関数の置き換えをすることが可能です。

ここではこれを利用して、malloc 実行時には leak_detect_malloc を、free 実行時には leak_detect_free を実行するようにします。

具体的にはソースコードの冒頭付近で下記のような記述を行います。

#define malloc(s) leak_detelc_malloc(s, __FILE__, __LINE__) 
#define free leak_detect_free

2行目の #define では free を単純に leak_detect_free に置き換えています。1行目の #define では malloc を leak_detect_malloc に置き換え、引数に追加で  __FILE__ と __LINE__ を渡すようにしています。

__FILE__ は現在のソースコードのファイル名、__LINE__ は現在のソースコードの行番号を表すマクロですので、leak_detect_malloc にメモリ取得が行われた場所の情報を渡すことができます。

以上により、ソースコードの malloc や free を使用している箇所を書き換えることなく、leak_detect_malloc や leak_detect_free が実行されるようになります。

メモリの解放忘れを検出するプログラム

それでは実際のメモリの解放忘れを検出するプログラムを紹介していきたいと思います。

ソースコード

ソースコードは下記のようになります。main.c と leakdetect.c と leakdetect.h の3つのファイルから構成しています。

main.c
#include <stdio.h>
#include <stdlib.h>

#define LEAK_DETECT
#ifdef LEAK_DETECT
#include "leakdetect.h"
#define init leak_detect_init
#define malloc(s) leak_detelc_malloc(s, __FILE__, __LINE__) 
#define free leak_detect_free
#define check leak_detect_check
#else
#define init() 
#define check()
#endif

int main(void){
    int *p[8];

    /* メモリ管理配列の初期化 */
    init();

    /* メモリの確保 */
    p[0] = (int*)malloc(sizeof(int));
    p[1] = (int*)malloc(sizeof(int));
    p[2] = (int*)malloc(sizeof(int));
    p[3] = (int*)malloc(sizeof(int));
    p[4] = (int*)malloc(sizeof(int));
    p[5] = (int*)malloc(sizeof(int));
    p[6] = (int*)malloc(sizeof(int));
    p[7] = (int*)malloc(sizeof(int));

    /* メモリの解放 */
    free(p[4]);
    free(p[7]);

    /* メモリの解放忘れを表示 */
    check();

    return 0;
}
leakdetect.c
#include <stdio.h>
#include <stdlib.h>
#include "leakdetect.h"

/* 管理するメモリの情報の最大数 */
#define N 500

/* メモリ情報を格納する構造体の配列 */
MEM_T mem_info[N];

/* メモリ管理配列を初期化する関数 */
void leak_detect_init(void){
    int i = 0;

    /* 各メモリの情報を初期化 */
    for(i = 0; i < N; i++){
        mem_info[i].ptr = NULL;
        mem_info[i].size = 0;
        mem_info[i].file = NULL;
        mem_info[i].line = 0;
    }
}

/* メモリ確保とそのメモリの情報を記録する関数 */
void *leak_detelc_malloc(size_t size, const char *file, unsigned int line){
    int i = 0;
    void *ptr = NULL;

    /* まずはメモリを確保 */
    ptr = malloc(size);

    /* メモリ確保に失敗した場合はメモリ管理構造体には情報を格納しない */
    if(ptr == NULL){
        return NULL;
    }

    for(i = 0; i < N; i++){
        /* 情報が格納されていない要素にメモリの情報を格納 */
        if(mem_info[i].ptr == NULL){
            mem_info[i].ptr = ptr;
            mem_info[i].size = size;
            mem_info[i].file = file;
            mem_info[i].line = line;
            break;
        }
    }

    /* 確保したメモリのアドレスを返却 */
    return ptr;
}

/* メモリの確保とそのアドレスのメモリ情報を記録から削除する関数 */
void leak_detect_free(void *ptr){
    int i = 0;

    for(i = 0; i < N; i++){
        /* free しようとしているアドレスのメモリ構造体を探す */
        if(mem_info[i].ptr == ptr){
            /* そのメモリはちゃんと解放されるのでその構造体の情報を消す */
            mem_info[i].ptr = NULL;
            mem_info[i].size = 0;
            mem_info[i].file = NULL;
            mem_info[i].line = 0;
            break;
        }
    }

    /* メモリを解放 */
    free(ptr);
}

/* 解放が行われていないメモリの情報を表示する関数 */
void leak_detect_check(void){
    int i = 0;

    for(i = 0; i < N; i++){
        /* 情報が消されていないメモリ管理構造体を探す */
        if(mem_info[i].ptr != NULL){
            /* 情報が消されていない構造体の ptr が指すメモリは解放忘れ */

            /* その解放忘れのメモリの情報を出力 */
            printf("メモリリークを検出!!!!!\n");
            printf(" アドレス:%p\n", mem_info[i].ptr);
            printf(" サイズ:%u\n", (unsigned int)mem_info[i].size);
            printf(" 場所:%s:%u\n", mem_info[i].file, mem_info[i].line);
            printf("\n");
        }
    }
}
leakdetect.h
#ifndef LEAKDETECT_H
#define LEAKDETECT_H

#include <stdlib.h>

/* 管理数の上限 */
#define MAX_NUM 500

/* メモリ管理構造体 */
typedef struct {
    void *ptr;
    size_t size;
    const char *file;
    unsigned int line;
} MEM_T;

/* 関数のプロトタイプ宣言 */
void leak_detect_init(void);
void *leak_detelc_malloc(size_t, const char*, unsigned int);
void leak_detect_free(void*);
void leak_detect_check(void);

#endif

コンパイル方法

main.c と leakdetect.c の2つのファイルをコンパイルしてリンクする必要がありますので、下記のように分割コンパイルを行います。

gcc main.c leakdetect.c -o main.exe

実行結果

実行すると、下記のようにメモリの解放が行われていないメモリの情報が表示されます。

bash-3.2$ ./main.exe 
メモリリークを検出!!!!!
 アドレス:0x7f889f500080
 サイズ:4
 場所:main.c:23

メモリリークを検出!!!!!
 アドレス:0x7f889f500090
 サイズ:4
 場所:main.c:24

メモリリークを検出!!!!!
 アドレス:0x7f889f5000a0
 サイズ:4
 場所:main.c:25

メモリリークを検出!!!!!
 アドレス:0x7f889f5000b0
 サイズ:4
 場所:main.c:26

メモリリークを検出!!!!!
 アドレス:0x7f889f5000d0
 サイズ:4
 場所:main.c:28

メモリリークを検出!!!!!
 アドレス:0x7f889f5000e0
 サイズ:4
 場所:main.c:29

スポンサーリンク

ソースコードの解説

それでは紹介したソースコードを簡単に解説していきたいと思います。

メモリ情報の構造体の定義

メモリ情報の構造体は leakdetect.h の下記で定義しています。

/* メモリ管理構造体 */
typedef struct {
    void *ptr;
    size_t size;
    const char *file;
    unsigned int line;
} MEM_T;

それぞれのメンバには下記の情報を管理するようにしています。

  • ptr:取得したメモリのアドレス
  • size:取得したメモリのサイズ
  • file:メモリを取得したファイルの名前
  • line:メモリを取得したファイルの行番号

メモリ管理配列の作成

さらに leakdetect.c の下記部分で、この構造体の配列であるメモリ管理配列を作成しています。

/* 管理するメモリの情報の最大数 */
#define N 500

/* メモリ情報を格納する構造体の配列 */
MEM_T mem_info[N];

N は “500” として定義していますので、500 個までのメモリの情報を管理することができます(それ以上管理したいのであれば N を増やせば良いです)。

メモリ情報の記録

メモリ情報の記録は下記の leak_detect_malloc 関数の中で行っています。

/* メモリ管理配列を初期化する関数 */
void leak_detect_init(void){
    int i = 0;

    /* 各メモリの情報を初期化 */
    for(i = 0; i < N; i++){
        mem_info[i].ptr = NULL;
        mem_info[i].size = 0;
        mem_info[i].file = NULL;
        mem_info[i].line = 0;
    }
}

まず malloc を行い、取得したメモリの情報を記録した後に、malloc の戻り値をこの関数の戻り値として返却しています。

すでに管理されているメモリの情報を上書きしないように、メモリ管理配列の各要素の ptr メンバが NULL であるかどうかを確認し、NULL である要素にメモリの情報を格納するようにしています。

これによりメモリの確保が行われるたびにメモリ管理配列にそのメモリの情報がどんどん格納されていくことになります。

メモリ情報の削除

メモリ管理情報の削除は下記の leak_detect_free 関数の中で行っています。

/* メモリの確保とそのアドレスのメモリ情報を記録から削除する関数 */
void leak_detect_free(void *ptr){
    int i = 0;

    for(i = 0; i < N; i++){
        /* free しようとしているアドレスのメモリ構造体を探す */
        if(mem_info[i].ptr == ptr){
            /* そのメモリはちゃんと解放されるのでその構造体の情報を消す */
            mem_info[i].ptr = NULL;
            mem_info[i].size = 0;
            mem_info[i].file = NULL;
            mem_info[i].line = 0;
            break;
        }
    }

    /* メモリを解放 */
    free(ptr);
}

free しようとしているアドレスに対するメモリ情報をメモリ管理配列の中から探し出し、見つけた要素の情報を初期化することでメモリ情報の削除を行なっています。

後は実際に free 関数でメモリを解放して終了です。

メモリ解放忘れの情報表示

メモリ解放忘れの情報表示は下記の leak_detect_check 関数で行っています。

/* 解放が行われていないメモリの情報を表示する関数 */
void leak_detect_check(void){
    int i = 0;

    for(i = 0; i < N; i++){
        /* 情報が消されていないメモリ管理構造体を探す */
        if(mem_info[i].ptr != NULL){
            /* 情報が消されていない構造体の ptr が指すメモリは解放忘れ */

            /* その解放忘れのメモリの情報を出力 */
            printf("メモリリークを検出!!!!!\n");
            printf(" アドレス:%p\n", mem_info[i].ptr);
            printf(" サイズ:%u\n", (unsigned int)mem_info[i].size);
            printf(" 場所:%s:%u\n", mem_info[i].file, mem_info[i].line);
            printf("\n");
        }
    }
}

単純にメモリ管理配列に残っているメモリの情報(ptr が NULL でないメモリの情報)を表示しているだけです。

この関数を main 関数の最後で実行することで、プログラム内で発生したメモリ解放忘れを全て表示することができます。

#define による関数の置き換え

#define による関数の置き換えは main.c の下記部分で行っています。

#define LEAK_DETECT
#ifdef LEAK_DETECT
#include "leakdetect.h"
#define init leak_detect_init
#define malloc(s) leak_detelc_malloc(s, __FILE__, __LINE__) 
#define free leak_detect_free
#define check leak_detect_check
#else
#define init() 
#define check()
#endif

LEAK_DETECT が定義されている場合のみ、メモリ管理を行うようにしていますので、下記の行をコメントアウトすれば leak_detect_malloc 関数や leak_detect_free 関数が実行されず、通常の malloc 関数と free 関数が実行されるようになります。

具体的には、上記の行をコメントアウトするかどうかで次のようにコンパイルされるソースコードが変化することになります。

LEAK_DETECT 定義ありの場合の main.c
#include <stdio.h>
#include <stdlib.h>

#define LEAK_DETECT
#ifdef LEAK_DETECT
#include "leakdetect.h"
#define init leak_detect_init
#define malloc(s) leak_detelc_malloc(s, __FILE__, __LINE__) 
#define free leak_detect_free
#define check leak_detect_check
#else
#define init() 
#define check()
#endif

int main(void){
    int *p[8];

    /* メモリ管理配列の初期化 */
    leak_detect_init();

    /* メモリの確保 */
    p[0] = (int*)leak_detelc_malloc(sizeof(int), __FILE__, __LINE__);
    p[1] = (int*)leak_detelc_malloc(sizeof(int), __FILE__, __LINE__);
    p[2] = (int*)leak_detelc_malloc(sizeof(int), __FILE__, __LINE__);
    p[3] = (int*)leak_detelc_malloc(sizeof(int), __FILE__, __LINE__);
    p[4] = (int*)leak_detelc_malloc(sizeof(int), __FILE__, __LINE__);
    p[5] = (int*)leak_detelc_malloc(sizeof(int), __FILE__, __LINE__);
    p[6] = (int*)leak_detelc_malloc(sizeof(int), __FILE__, __LINE__);
    p[7] = (int*)leak_detelc_malloc(sizeof(int), __FILE__, __LINE__);

    /* メモリの解放 */
    leak_detect_free(p[4]);
    leak_detect_free(p[7]);

    /* メモリの解放忘れを表示 */
    leak_detect_check();

    return 0;
}
LEAK_DETECT 定義なしの場合の main.c
#include <stdio.h>
#include <stdlib.h>

#define LEAK_DETECT
#ifdef LEAK_DETECT
#include "leakdetect.h"
#define init leak_detect_init
#define malloc(s) leak_detelc_malloc(s, __FILE__, __LINE__) 
#define free leak_detect_free
#define check leak_detect_check
#else
#define init() 
#define check()
#endif

int main(void){
    int *p[8];

    /* メモリ管理配列の初期化 */
    ;

    /* メモリの確保 */
    p[0] = (int*)malloc(sizeof(int));
    p[1] = (int*)malloc(sizeof(int));
    p[2] = (int*)malloc(sizeof(int));
    p[3] = (int*)malloc(sizeof(int));
    p[4] = (int*)malloc(sizeof(int));
    p[5] = (int*)malloc(sizeof(int));
    p[6] = (int*)malloc(sizeof(int));
    p[7] = (int*)malloc(sizeof(int));

    /* メモリの解放 */
    free(p[4]);
    free(p[7]);

    /* メモリの解放忘れを表示 */
    ;

    return 0;
}

メモリの管理を行うと、余分にメモリが必要になったりプログラムが遅くなったりしてしまいます。

ですので、動作確認時は LEAK_DETECT を定義してメモリの解放忘れをチェックし、問題なくなったら LEAK_DETECT の定義をコメントアウトして公開や提出をするようにすると良いと思います。

まとめ

このページではメモリの解放忘れを自力で検出する方法について解説しました。

ついつい忘れてしまいがちなメモリの解放ですが、こんな感じで検出することができます。

またこのプログラムや解説を通じて、メモリの管理や関数の置き換え等の知識が深まれば幸いです!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です