メモリ周りの基本的操作とテクニック

このエントリーをはてなブックマークに追加

メモリの基本操作

さて、今回はメモリ周りの基礎から解説していきます。
ただmalloc(3)などの解説だけでは面白くもないですので、ある程度テクニックの紹介もさせて頂きます。

基本的な使い方

メモリの操作の基本となる関数はmalloc(3)、calloc(3)、realloc(3)、free(3)です。
関数とその意味は次の通りです。

関数名説明
mallocメモリ取得
calloc初期化したメモリ取得
reallocメモリ長変更
freeメモリ解放

簡単な使い方はこのようになります。
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *p;
    char *rp;

    //必要な領域を取得
    p = malloc(128);
    if (p == NULL) {
        return 1;
    }

    //取得したメモリ領域を操作
    snprintf(p, 128, "abcdefg"); //文字列を書き込んでみよう
    printf("%s\n", p);

    //メモリサイズの変更
    rp = realloc(p, 256);
    if (rp == NULL) {
        free(p);
        return 1;
    }
    p = rp;

    //不必要になったら開放
    free(p);

    return 0;
}

非常に簡単ですね。実際に使う場合には構造体と共に使う場合が多いでしょう。
エラー処理さえきちんとしていれば特に難しいものでもありません。


エラーチェックを楽にする

エラー処理さえきちんとしていれば問題は無い、とはいっても人間ミスをするものです。
そもそも良いプログラマはDRY原理を実行しています。ミスの発生しやすしエラーチェックは自動で行いましょう

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

void *xmalloc(size_t size)
{
        void *ret;

        ret = malloc(size);
        if (ret == NULL) {
                exit(EXIT_FAILURE);
        }

        return ret;
}

void xfree(void *ptr)
{
        free(ptr);
}

void *xrealloc(void *ptr, size_t size)
{
        void *ret;

        ret = realloc(ptr, size);
        if (ret == NULL) {
                exit(EXIT_FAILURE);
        }

        return ret;
}


int main(void)
{
    char *p;

    //必要な領域を取得
    p = xmalloc(128);

    //取得したメモリ領域を操作
    snprintf(p, 128, "abcdefg"); //文字列を書き込んでみよう
    printf("%s\n", p);

    //メモリサイズの変更
    p = xrealloc(p, 256);

    //不必要になったら開放
    xfree(p);

    return 0;
}

malloc/realloc/freeを新規作成のxmalloc/xrealloc/xfreeに置き換えています。これはUnix系では良く見られるテクニックで、関数名、実装も大体同じです。
どうでしょうか? 簡単な実装ですが手間が一気に減ったと思います。
さて、エラー処理にexit(3)を使用していますが理由が分かりますか? これはmalloc系の関数が失敗した場合はシステムのメモリ不足等の原因でそれ以上処理が続けられない状態だからです。これ以上プログラムを実行する事自体が無意味、もしくは非常に危険な状態なのです。
さらに、エラーメッセージを表示すればデバッグが楽になります。
実はさらに便利な一面もあります。それはメモリ系のデバッグで役に立つという点です。通常のPC上であればメモリ系のバグチェックツールもあるのですが、組み込み等の特定のケースではそういったツールを使う事自体が非常に厳しい場面もあります。


ラッパ関数でエラーチェック!

メモリ周りでのバグはいくつかパターンがあります。オーバーフロー、ダブルフリー、メモリリーク、危険領域への書き込み、初期化していないメモリの読み書き等。 すべてのエラーチェックは難しいですが、ここではfree(3)の簡易エラーチェックを行いましょう。
例えば次のようなエラーを考えてみます。これはfreeするポインタが間違っています。この場合実行すればプログラムはエラーで終了します。
簡単な例ですが、実際にこのような事は珍しくありません。

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

int main(void)
{
        char *p;

        p = malloc(128);
        p+=1;

        free(p);

        return 0;
}


さて、では先ほどのプログラムを次にように変更してみましょう。

#include <stdio.h>
#include <stdlib.h>
#define CHECK_ARRAY_SIZE 128
void *malloc_pointer[CHECK_ARRAY_SIZE] = {};

void set_memory(void *ptr)
{
    int i;

    for (i=0; i<CHECK_ARRAY_SIZE; i++) {
        if (malloc_pointer[i] == NULL) {
           malloc_pointer[i] = ptr;
           break;
        }
    }
}

int exist_memory(void *ptr)
{
    int i;

    for (i=0; i<CHECK_ARRAY_SIZE; i++) {
        if (malloc_pointer[i] == ptr) {
           return 1;
        }
    }

    return -1;
}

void unset_memory(void *ptr)
{
    int i;

    for (i=0; i<CHECK_ARRAY_SIZE; i++) {
        if (malloc_pointer[i] == ptr) {
            malloc_pointer[i] = NULL;
            break;
        }
    }
    return 0;
}

void *xmalloc(size_t size)
{
    void *ret;

    ret = malloc(size);
    if (ret == NULL) {
       exit(EXIT_FAILURE);
    }
    set_memory(ret);

    return ret;
}

void xfree(void *ptr)
{
    if (exist_memory(ptr) < 0) {
       exit(EXIT_FAILURE);
    }
    free(ptr);
    unset_memory(ret);
}

int main(void)
{
    char *p;

    p = xmalloc(128);
    p+=1;

    xfree(p);

    return 0;
}

malloc_pointerが固定長ですので汎用的ではありませんが、簡易チェックとしては十分でしょう。
これならばダブルフリーもチェックできます。


まとめ

いかがでしたでしょうか? メモリ周りは本当にバグが多い部分です。
エラーチェックは当然必須ですが、こういったテクニックを利用してバグを減らす努力は大切です。
さて、次回はLinuxでのメモリ周りをくわしく解説をしていきます。


メモリ処理にはポインタの理解は必須です。苦手な方はこういった書籍が有効でしょう。