動的メモリ管理

1. C言語でのメモリの割り当て

プログラム実行までの過程

C言語におけるソースファイルから実行に至るまでの過程を復習しておこう(図1).

コンパイル処理により,C言語で記述されたソースファイルはオブジェクトファイルに変換される.オブジェクトファイルには機械語に変換された命令列や,変数を格納するメモリの情報が含まれている.

次に,リンク処理により,オブジェクトファイルはランタイムライブラリと結合され,実行ファイルが作成される.
ランタイムライブラリには,printf などのランタイム関数や,プログラムの起動時に実行されるランタイム関数のための初期化を行うスタートアップルーチンなどが含まれている.オブジェクトファイルは,これらのランタイムライブラリと結合されて,初めて実行可能(ロード可能)なイメージとなる.

作成された実行ファイルを起動すると,オペレーティングシステムはプログラムのロード処理を行う.オペレーティングシステムによりプログラムのためのメモリ空間に,実行ファイルのイメージが割り当てられる.
プログラムのコードや変数の置かれる位置(アドレス)は,ロード時に最終的に決定される.

ロード処理が終了すると,プログラムコードの特定の位置(エントリポイント)からプログラムの実行が開始される.

動的(dynamic)な処理と静的(static)な処理

これらの一連の処理の中で,ロード時までに決定する処理を静的(static)な処理とよび,プログラムの実行が開始されてから決定するものを動的(dynamic)な処理,あるいは実行時処理と呼ぶ.

int g_i = 100;         // グローバル変数

int main()
{
    int i[3] = {1,2,3};//ローカル変数 (動的ローカル変数・自動変数)
    static int s_i = 10;//静的ローカル変数

    ・・・・
    return 0; 
}

上記のプログラムコード中の変数が,どのように割り当てられるのかを見てみよう.

変数は,その変数が宣言された位置によりグローバル変数ローカル変数に分類できる.

関数の外部で宣言された変数をグローバル変数といい,変数が宣言された後は,ソースファイル中のどこからでも参照できる.
これに対し,関数内部で宣言されたローカル変数は,変数が宣言されたブロック(中括弧でくくられた部分)の中からのみ参照できる.

グローバル変数と静的ローカル変数は,プログラムのロード時に(つまり静的に)アドレスが決定され,初期化も行われる.

動的ローカル変数(自動変数)は,スタックと呼ばれるメモリ空間に割り付けられる.動的ローカル変数は,関数が呼び出された時に(つまり動的に)メモリに割り付けられ,初期化が行われる.

これらの変数に割り当てるサイズ(大きさ)はすべて静的に決定されている.つまり,一度ソースコード中に記述した配列の大きさなどは,変更することは出来ない.

2. 動的メモリ管理

動的メモリ管理の必要性

ソースコードを記述する段階では,メモリに割り当てる変数の大きさを決定できない場合がある.たとえば,テキストエディタプログラムなどでは,実際にテキストをオープンするまで,その大きさを知ることはできない.あらかじめ,可能な限りのメモリを確保してソースファイル中に記述しておくことも可能であるが,無駄が多いばかりでなく,複数のプログラムが限られたメモリを分け合って使用している Windowsのような環境では,他のプログラムの実行にも影響を及ぼしてしまうことがある.

このようなプログラムを記述する場合には,プログラムの実行時に(動的に)メモリ空間を確保して使用する必要があり,このことを動的メモリ管理と呼ぶ.

プログラムは,メモリ領域が必要になった時点でシステムにメモリを要求し,利用可能なメモリ領域を確保する(アロケート).また,不要となったメモリ領域は解放し,次回以降のアロケート処理で利用できるようにする.

動的メモリ管理で使用されるメモリ空間をヒープと呼ぶ.

動的メモリ管理のためのライブラリ関数

C言語では,動的メモリ管理のためのライブラリ関数が用意されている.これらの関数を使用する場合には stdlib.h をインクルードする.

void *malloc(size_t size)

size バイトのメモリをアロケートし,そのアドレスを返す.メモリの確保に失敗した場合には NULL を返す.

malloc 関数は,sizeバイトの領域を確保し,そのアドレスを返す.

size_t 型は stdio.h ヘッダファイル中で typedef 宣言された型で, unsigned int 型と同じである.(typedef の詳細については後ほど学習する)

malloc 関数の戻り値のプロトタイプ宣言は void* 型となっているが,これはどのような型のポインタでも格納できる「万能なポインタ」という意味をもつ.malloc 関数で確保したメモリ領域にどのような型のデータを格納するかは,プログラマが管理する必要がある(呼び出されたmalloc 関数は知る由もない). プログラマは,malloc関数の戻り値を,適切なポインタ変数に代入して使用することとなる.

要求されたメモリ量を割り当てるのに十分な空間がシステムに残されていない場合には,malloc関数は NULL を返す.NULLは stdio.h で ((void *)0) と define されたシンボルである.

malloc 関数で割り当てられた領域は,初期化されておらず,どのような値が格納されているかは不定である.(注:Visual C++のデバッグバージョンの malloc では,各バイトは 0xcd に初期化される.)

void *calloc(size_t num, size_t size)

size バイトの要素を num 個分アロケートし,そのアドレスを返す.メモリの確保に失敗した場合には NULL を返す.それぞれの要素は0で初期化されている.

calloc 関数は malloc 関数と同様の処理を行うが,確保された領域は0で初期化されている.

void free(void* memblock)

memblock で示された領域を解放する.

free 関数は,malloc,calloc 関数でアロケートした領域を解放し,次回以降のアロケート要求時に使用できるようにする.

引数には,malloc,calloc 呼び出しで返された値を渡すこと.

引数に NULL を渡した場合には,free関数はなにも処理せずにリターンする.

ライブラリ関数の使用例

ライブラリ関数の使用例を以下に示す.

1) int 型整数をひとつ確保

int *p;    // int型を指すポインタ

p = malloc(sizeof(int));
if (p == NULL) {
    // アロケートに失敗
} else {
    *p = 100;  //間接参照演算子を用いてアクセス

    free(p);  //使い終わったら解放
}

2) int 型整数の配列を確保

int *p;    // int型を指すポインタ

p = malloc(sizeof(int) * 10);  // 10個分を確保
if (p == NULL) {
    // アロケートに失敗
} else {
    p[0] = 10;
    p[1] = 20;     // 配列と同様に添え字でアクセスできる
   
*(p + 1) = 20; // p[1] = 20 と同じ意味
    ...
    free(p);
}

3) int 型整数の配列を確保(callocの例)

int *p;    // int型を指すポインタ

p = calloc(10, sizeof(int));  // 10個分を確保
if (p == NULL) {
    // アロケートに失敗
} else {
    p[0] = 10;
    p[1] = 20;     // 配列と同様に添え字でアクセスできる
    ...
    free(p);
}

 

3. メモリリーク

動的なメモリの確保,解放を繰り返すリスト処理のようなプログラムにおいては,メモリ領域の解放処理を適切に行わないと,利用可能なメモリ領域が減っていくメモリリークと呼ばれる現象が発生する.メモリリークが発生するとプログラムそのものが正常に動作しなくなったり,他のプログラムにも悪影響を及ぼすことがある.

プログラムのバグによるメモリリークを発見することは困難であることが多い.Visual C++ では,_CrtDumpMemoryLeaks() 関数を用いて解放しわすれているメモリ領域をデバッガ上に表示することができる.

4.実習 動的メモリ確保

以下のサンプルプログラムを用いて実習を行う.サンプルプログラムは P:\学部授業関連\2023年度前期\プログラミング演習II\課題2\allocsample に置いてあるのでコピーして使用すること (allocsample.c).

 

1) デバッガ上で実行し,デバッグウインドウに表示されるメッセージを確認しなさい.次に,free() の行をコメントアウトし同様に実行し,デバッグウインドウの表示を確認し,_CrtDumpMemoryLeaks()関数により モリリークが検知されることを確認しなさい.

2) malloc の行にブレークポイントを置き,ステップ実行しながら p の値がどのように変化するか調べなさい.

3) タスクマネージャで,メモリの使用量を確認しながらステップ実行しなさい.

タスクバーの空いている部分を右クリックして[タスクマネージャ]を選択するか,Ctrl+Alt+Del キーを押して表示される[タスクマネージャ]を選択する.

[詳細]タブを選択.

列の見出し部分で右クリックし、表示されたメニューの[列を選択]をクリック.[列の選択ダイアログ]から[コミットサイズ]にチェックを入れ[OK]ボタンを押す.

ソースコードの malloc の行にブレークポイントを置き,デバッガ上でプログラムを実行する.

タスクマネージャのイメージ名から allocsample.exe を探し,コミットサイズの値を確認しながらステップ実行を行う.

4) 3)と同様の操作を,malloc で確保するサイズを以下のように変化させて実行する.

malloc(1024*1024*1000);  (約1Gバイト)

malloc(1024*1024*100);

malloc(1024*1024*500);