C言語編 目次

 関数設計

 

・関数名の命名規則
・プログラミングに出る!英単語

 ポインタ

 

・データ型とポインタ

 データ型

 

・char *とconst char *は違う
・符号付きと符号なし

 演算子

 

・三項演算子とデータ型の問題

 制御構文

 

・条件式で代入する
・三項演算子を使ったswitch

 構造体

 

・構造体のサイズとアライメント
・構造体メンバのサイズを知る

 配列

 

・配列使用時の注意
・配列の要素数を知る

 メモリ管理

 

・メモリスタック
・動的メモリ確保とメモリリーク

 モジュール設計

 

・モジュール分割
・汎用モジュールとアプリ依存モジュール

 パフォーマンス
  徹底チューニング

 


・どんな処理に時間がかかるのか
・ファイル入出力の効率化
・アルゴリズムを考える1
・アルゴリズムを考える2

 プリプロセッサの便利機能


・2重インクルード防止

 


トップページへ戻る

動的メモリ確保とメモリリーク

 今回は、動的なメモリの確保とメモリリークについてです。動的なメモリというのは、malloc()関数などで確保するメモリです。Win32の場合、スタックサイズはデフォルトで1MBですが、動的なメモリは2GBまで使うことができます。

 スタックは、あくまで関数内の作業用の変数領域として使い、サイズの大きいデータや可変サイズのデータは動的に確保するのが一般的です。

 さて、メモリを動的に確保する場合、メモリリークには常に気をつけないといけません。malloc()などで動的に確保したメモリは、free()で開放するまでずっと確保されたままになります。

 free()で開放しなくても、プロセスが終了すれば、確保したメモリはすべて開放されますが、バックグラウンドで動き続けるサービスのようなプログラムの場合は、一箇所でも開放し忘れがあると、徐々にメモリを食いつぶしていき、最後にはプログラムが動作不能になってしまいます。

 メモリリークのやっかいなところは、起動したあとしばらくは普通に動作しているように見えるので、見つけるのが難しい点です。

 メモリリークはCプログラムの典型的なバグの一つです。直ちに致命的なエラーとなることはありませんが、徐々にリソースを消費していくので、時限爆弾を仕込んでいるようなものですね。

 基本的には、「確保したものは、必ず開放する」ということを守ればメモリリークはおきないはずなので、これは、プログラムの作り次第でほとんどは排除することができます。メモリリークがおきるということは、プログラムの作りがまずいと思ってよいと思います。

 では、ここからはメモリリークのおきないプログラムの作り方を説明していきます。

(1)確保・開放を1つのペアとして考える

 まずは、基本中の基本ですが、malloc()で確保したメモリは、必ずfree()で開放します。この2つの関数は、2つで1つだと思ってください。malloc()を書いたら、必ず対応するfree()を記述します。malloc()だけ書いてfree()を書かないなんてことは、どんな場合でもありえません。

 「プログラムが終了したら自動的に開放されるから、開放しなくてもいいや」なんて考えてはいけません。自動的に開放されるかされないかに関わらず、「自分で確保したものは、必ず自分で開放する」という考えを持つことが大切です。自分のプログラム内で完結するように作れば、少なくとも自分の責任でメモリリークがおきることがないことが保証できるからです。

(2)原則として、関数内で確保・開放を完結させる

 動的なメモリを確保する場合、大きく分けると、プログラム全体で使うようなグローバルなデータで、サイズの大きいものを作る場合と、関数内で一時的に大きなサイズのデータを使いたい場合の2通りがあります。

 関数内で確保・開放する場合は、関数の先頭で確保し、関数の最後で開放します。そして、関数からのreturnは関数の最後だけに書きます。具体的には次のような形式になります。

int func()
{
    int    err = 0;
    int    *ptr = NULL;
    
    // 関数の先頭で確保
    ptr = (int *)malloc(sizeof(int) *1024);
    if (!ptr) err = 1;

    // 関数の内容1
    if (!err)
    {
       // ...
    }
    // 関数の内容2
    if (!err)
    {
       // ...
    }

    // 関数の最後で開放
    if (ptr) {free(ptr), ptr = NULL;}

    return err;
}

 メモリの空き容量不足で、動的なメモリが確保できない場合、malloc()関数はNULLを返すので、必ずmalloc()の戻り値に対してNULLチェックをします。NULLが返った場合はエラーとし、以降の処理はスキップさせます。

 関数内でエラーが発生した場合は、以降の処理をスキップするようにします。よくありがちですが、エラーが発生した時点でreturnしてはいけません。returnすると、最後の開放処理が実行されなくなってしまいます。

 ここで、「すべてのreturnの直前に開放処理を入れればいいんじゃないの?」と思ったでしょうか?そのような考え方はかなり苦しいです。動的に確保するメモリが後から追加されたら、すべてのreturnの前に開放処理を追加しないといけません。もし一箇所でも開放処理を入れ忘れたらどうしますか?「確保したものは、必ず開放する」ことが保証できますか?

 入口と出口を1つずつにし、「入ったら即確保、出る直前で開放」とすることによって、全ての抜け道をふさぐことができるのです。これは、どんな関数でも同じです。returnを2つ以上書かないとできない処理なんてものは一切ありません。

 メモリを開放したら、ポインタをNULLで初期化し直します。

 「関数の最後なのに、初期化する必要なんてあるの?」

 プログラム上は確かに省略可能な処理ですが、これは「使えなくなったメモリ領域をプログラム上から切り離す」という意味のある大切な処理です。このように、「意味を持たせるために、省略可能な処理をあえて書く」というのは、整然としたプログラムを書くために必要なことです。

 もう一つの、関数をまたいで使うような動的メモリの場合は、細心の注意が必要です。こちらも原則として、1つの関数内に確保・開放処理をペアで記述します。プログラム全体にわたって共通で使うようなデータの場合は、main()関数の先頭で確保して、main()関数の最後で開放します。

 malloc()とfree()の記述を、別々の関数に分けてはいけません。呼ばれるのか呼ばれないのかわからないような関数にfree()だけ記述するのは、あまりに危険です。これはメモリリークの元凶となります。