Nodachisoft Nodachi Sword Icon
  
@あまじ✎ 2021年2月23日に更新

第2章44 malloc関数、free関数で大きなメモリを扱う

イチからゲーム作りで覚えるC言語
第2章43プログラム起動時の引数を読む : PREV
NEXT : 第2章45 構造体を使う :

概要

C言語で大きな配列を使った場合に、具体的に問題となるようなケースと、その場その場で使いたいメモリサイズを指定してメモリを確保(=動的にメモリ確保)する方法と例を、malloc 関数、calloc関数、reallowc関数、free関数を使って確認していきます。

解決したい内容

C言語では、配列で確保した変数の配列のサイズは、一度初期化すると後から変えることができません。

たとえばchar name[32]; という変数であれば、変数の宣言をした ときに 32 文字まで変数 name に入れることができる、という制限があり、64文字を入れると思わぬエラーを引き起こすことがあります。

プログラムの後になって、やっぱりもっと大きなメモリサイズが欲しい! というケースはよくあります。

困ったときの例

例えば、画面に表示するマップサイズなど。 その時その時で必要となるメモリサイズが変わる場合があります。

例えば、下のような横64、縦64 サイズのマップデータを考えます。

exofarray.c
#include <stdio.h>

int main() {
    int map[ 6 * 6 ];
     :
    // あとから 6x6 以上のサイズのマップが欲しい!
    // けど、 6x6 以上は変数 map に確保されていない
    map[100 * 100] = 2; // 上手く動作しない
}

マップの (x, y) 座標は map[x成分 + y成分 * 6] に格納されているとかんがえると、 うまく 2 次元のマップデータとして扱うことができます。

スタック上での配列とマップ

ただし、後々に問題となることがあります。 マップデータのサイズが 6 x 6 で固定なら良いのですが、 マップをさらに x 方向に拡張して 12 x 6 としたいとき、 あとから、 int map[12 * 6]; などと再度初期化することはできません

では、最初から考えられる中で最大のメモリサイズを確保しておいてはどうでしょうか。

exofarray.c
#include <stdio.h>
int main() {
    int map[2000 * 2000];
    printf("無事に巨大なマップのメモリ確保!\n");
}

しかしこのプログラムをコンパイルして実行しても、 通常、正しくプログラムが動作しませんし、実際に使うとすると課題もあります。 理由を見ていきます。

スタック領域の上限サイズ

以前お話しした、メモリの確保される場所に関係があります。

関数の中で、下のように確保された変数はすべて、スタック領域とよばれる仮想メモリに保存されます。

exstack.c
int main(){
    char name[]="ぷらんく";
    int level = 5;
    int hp = 10;
    long gold = 100000L;
}

このスタック領域は通常、一時的に関数を実行している間だけ有効となるようなメモリ領域であり、 高速にメモリを確保することが可能です。

また、想定外に大きなメモリを確保することで、パソコン全体の空きメモリ領域が不足してパソコンを動作させているシステム全体が機能停止することを防ぐため、使えるサイズが制限されています。

スタック領域の使えるサイズ上限

このサイズ制限は使っている PC の OS によって異なります。例えば、64bit の Linux OS であれば 8MBytes、Windows10 64bit であれば、約4.3M~6.4MBなどが標準のサイズです。実際に開発で使っている端末の限界サイズを確認をするためのコードは後述の「補足」をご参照下さい。

メモリの無駄な確保を避ける

別の問題として、将来使うかもしれないという理由でメモリを無駄に大量に確保しておくことは、 パソコンの有限なリソースを無駄遣いすることになるので、避けた方がよいです。

今回であれば、int map[2000 * 2000]; は int 変数ひとつあたり 4バイトなので、 簡単に計算してみると、 4 × ( 2000 × 2000 ) で 8 MB のメモリが必要になってしまいます。

動的なメモリ確保

スタック領域のメモリ上限、メモリを無駄に確保しない、といった問題を解消するために、 ヒープ領域と呼ばれる仮想メモリ空間に、欲しいサイズだけメモリを確保することができる、 malloc 関数、calloc関数、realloc関数、free関数を使っていきましょう。

それぞれ下のような機能をもった関数です

名前 概要
malloc 指定したバイト数ぶんのまとまった空きメモリを探して確保してくれます。確保した空きメモリのアドレスをポインタとして返してくれます。確保されたメモリは初期化されていません。
calloc malloc同様に指定したサイズのメモリが確保できます。確保したスペースのメモリを決めた値で初期化できます。
realloc malloc関数 や calloc 関数で確保されたメモリサイズを後から変更します。
free malloc 関数や calloc 関数で確保したメモリの使用を終了します。

malloc 関数

malloc はMemory allocation の略です。訳すと「メモリ割当」です。 読み方に決まりはないと思いますが、マロックという発音で読まれる事が多いです。

malloc 関数を使うと、指定したバイト数ぶんの空きメモリの塊を確保してくれます。確保した空きメモリの先頭メモリアドレスをポインタとして返してくれます。 空きメモリはスタック領域ではなく、ヒープ領域と呼ばれる場所に確保されますので、スタック領域の上限サイズは気にしなくても大丈夫です。

今回の 2000 x 2000 広大なマップデータを保存するメモリを確保することを考えてみましょう。

malloc_mapbuffer.c
#include <stdio.h>
int main() {
    // int map[2000 * 2000];
    int *map = (int*) malloc( sizeof(int) * 2000 * 2000);
    printf("無事に巨大なマップのメモリ確保!\n");
    free(map);
}

3 行目でメモリを確保していた行はコメントアウトし、4行目で、int型を 2000 × 2000 個ぶん 予約しています。

sizeof演算子は対象の型のサイズ(バイト数)を取得します。sizeof(int) であれば、 私の環境では int 型が 4 バイトなので整数の 4 が得られます。

sizeof(int) * A という書き型で、int型の数値が格納できるメモリ区間の A 個ぶんのサイズを指定していることになります。

malloc_mapbuffer.c
int *map = (int*) malloc( sizeof(int) * 2000 * 2000);

今回は、 malloc 関数でメモリを確保し、確保されたメモリアドレスの先頭を int 型のポインタ変数 map に格納しています。

これで、 map[0] ~ map[2000*2000 - 1] までの空間を使うことが出来ます。

なお、メモリ不足などでメモリが十分に確保できなかった場合など、 malloc 関数が失敗した場合は NULL が返ってきます

malloc 関数で確保したヒープ領域のメモリは、使い終わったら手動で解放という処理をする必要があります

解放には free 関数を使います。 free 関数を使うことで、ヒープ領域で確保しているメモリが他のプログラムから利用できる領域になります。逆に言うと、free 関数で解放処理を忘れると、そのぶんパソコンの空きメモリが少なくなってしまいます。

ガベージコレクション機能

free 関数で解放を忘れてしまったメモリは、プログラムが終了するまで他のプログラムから使用できません。 こういったメモリの解放忘れは C 言語のプログラムではよくあるバグの一つです。

Javaや Python、Go 言語などでは、わざわざプログラマが手動で free 関数を呼び出さなくとも、 ガベージコレクションと呼ばれる機能が自動的に使わなくなったメモリを解放する仕組みが 備わっていることが多いです。C 言語にはガベージコレクションの仕組みがないため、自分で メモリを解放するようにしていきましょう。

calloc 関数

calloc は contiguous allocation の略です。contiguous は「切れ目のない」「連続している」といった意味があります。

calloc は malloc と違い、メモリを確保するときにサイズをより指定しやすいことと、指定した値で確保したメモリの中身の全てのビットを 0 で初期化することが出来ます。

さきほどのmalloc 関数による、2000 * 2000 の int 型を格納する領域確保のプログラムを calloc 関数で置き換えてみると、下のような書き方になります。

calloc_mapbuffer.c
#include <stdio.h>
int main() {
    int *map = (int*) calloc( 2000 * 2000, sizeof(int) );
    printf("無事に巨大なマップのメモリ確保!\n");
    free(map);
}

このプログラムで取得したメモリの中身がちゃんと 0 で初期化されていることを確認してみます。 試に、下のようなプログラムを書いてみます。

calloc_mapbuffer.c
#include <stdio.h>
int main() {
    int* val = (int*)calloc(10, sizeof(int));
    for (int x = 0; x < 10; x++) {
        printf("%d ", val[x]);
    }
    free(val);
}

実行すると下のような結果になりました。

実行結果
0 0 0 0 0 0 0 0 0 0

実行すると、10 個ぶんの int型のメモリサイズが確保され、 全ての領域が 0 で初期化されていることがわかります。

calloc 関数で確保したメモリも忘れずに free 関数で解放処理をしましょう!

realloc 関数

realloc 関数をつかうと malloc 関数や calloc 関数で確保したメモリのサイズを、後から変更することが出来ます。

 
realloc_ex.c
#include <stdio.h>
int main() {
	int* map;
	map = (int*)calloc(5, sizeof(int));
	for (int x = 0; x < 5; x++) {
		printf("%d ", map[x]);
	}
    printf("\n");

	map = (int*)realloc(map , sizeof(int) * 10);
	for (int x = 0; x < 10; x++) {
		printf("%d ", map[x]);
	}
    printf("\n");
}

実行結果は下のようになりました。

実行結果
0 0 0 0 0
0 0 0 0 0 -842150451 -842150451 -842150451 -842150451 -842150451

4行目で calloc 関数で int型のメモリサイズ × 5 個ぶんのメモリ領域を確保しています。 このとき、 確保されたメモリの中身はすべて 0 で初期化されています。

続いて 10 行目で realloc 関数を使い、ポインタ変数 map が指し示すメモリ領域のサイズを int型メモリサイズ × 5 個ぶん から、10個ぶんに拡張しています。

拡張した部分は 0 で初期化されていないので、ゴミデータが入っており、 私の環境では「-842150451」という想定外の値が表示されています。

realloc関数の使い方は以下の通りです。

realloc関数
void *realloc(void *変更したいメモリアドレス, size_t 新しいサイズ)

戻り値は、サイズが変更された後のメモリドレスです。 もしメモリサイズの変更に失敗したら NULL が返ってきます。

補足

ここはスタック領域で扱えるサイズがどれくらいなのかを具体的に確認したいという レアな方向けへの補足です。

スタック領域で使えるメモリサイズ

スタック領域で扱えるサイズを確認するための C言語のコードを書いておきます。

Windows 環境での確認

今まで出てきたことのない、ヘッダファイルや関数GetCurrentThreadStackLimits() が登場していますが、 ここでは詳細な説明は割愛します。

 
check_win_stack.c
#include <stdio.h>
#include <basetsd.h>
#include <processthreadsapi.h>
int main() {
    ULONG_PTR lowLimit = 0UL;
    ULONG_PTR highLimit = 0UL;
    GetCurrentThreadStackLimits( &lowLimit, &highLimit);
    printf("The lower boundary of the current thread stack = %llu\n", lowLimit);
    printf("The upper boundary of the current thread stack = %llu\n", highLimit);
}

Windows10 64bit の環境で gcc でコンパイルして実行すると下のような結果になります。

result
The lower boundary of the current thread stack = 4325376
The upper boundary of the current thread stack = 6422528

これで、スタック領域のサイズが確認できました。

MacOS,Linux,Unix 環境での確認

MacOS や Linux、Unix では Windows 用の GetCurrentThreadStackLimits() 関数は使えません。 別のプログラムを書く必要があります。

ですが、MacOS や Linux、Unix であれば手軽にコマンド ulimit -s で確認できるので、 こちらのご紹介のみとします。 ご参考までに私が確認した Amazon Linux 2(Debian 10.7)での実行結果です。

ulimit
> ulimit -s
10240

単位はキロバイトですので、10 MB ですね!

参考

イチからゲーム作りで覚えるC言語
第2章43プログラム起動時の引数を読む : PREV
NEXT : 第2章45 構造体を使う :
 
 
送信しました!

コメント、ありがとうございます。

なんかエラーでした

ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。

Twitter:@NodachiSoft_jp
お名前:
 
連絡先:
 
メッセージ:
 
戻る
内容の確認!

以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください

お名前:
 
連絡先:
 
メッセージ:
 
Roboto からの操作ではないという確認のため確認キーを入れてください。
確認キー=95
戻る
 / 
送信確認へ
コメント欄
コメント送信確認へ

関連ありそうな記事(5件)です!

第4章3 C言語コンソール上で迷路脱出プログラム・迷路の自動生成

#C11仕様#C言語#ゲームプログラミング✎ 2021-05-15
C言語のコンソールゲームで迷路脱出ゲームプログラムの作り方を確認します。迷路は自動生成されます
広告領域
追従 広告領域
目次
第2章44 malloc関数、free関数で大きなメモリを扱う
第2章44 malloc関数、free関数で大きなメモリを扱う
概要
概要
解決したい内容
解決したい内容
困ったときの例
困ったときの例
スタック領域の上限サイズ
スタック領域の上限サイズ
メモリの無駄な確保を避ける
メモリの無駄な確保を避ける
動的なメモリ確保
動的なメモリ確保
malloc 関数
malloc 関数
calloc 関数
calloc 関数
realloc 関数
realloc 関数
補足
補足
スタック領域で使えるメモリサイズ
スタック領域で使えるメモリサイズ
Windows 環境での確認
Windows 環境での確認
MacOS,Linux,Unix 環境での確認
MacOS,Linux,Unix 環境での確認
参考
参考
Nodachisoft © 2021