見出し画像

C++ 再入門 その11 ヒープからのメモリ割り当てと返却(1)

C言語やC++に限った話ではないのですが、変数として割り当てられるメモリには大きく分けて3つの種類があります。一つは一般的なローカル変数として使われるスタック。固定的なメモリが割り当てられるグローバル変数や静的な変数。そしてヒープと言われる大きなメモリから要求に応じて動的に割り当てたり解放される変数です。

C言語の場合、ヒープから割り当てられる変数に対する言語的なサポートは無く、標準ライブラリ関数として用意されている malloc (など)を呼び出してヒープからメモリを割り当ててもらい、メモリが不要になったら free を呼び出して返却します。

#include <stdlib.h>
void *malloc(size_t size);
#include <stdlib.h>
void free(void *p);

昔はインクルードするファイルは <malloc.h> だった気がする。もしそんなソースを見かけても黙って <stdlib.h> になおして使ってね、もう malloc.h というファイルは無いかもしれない。

お爺さんの独り言

mallocの引き数に指定するのは割り当てて欲しい領域のバイト単位の大きさです。戻り値には、割り当てた領域の先頭へのポインタが戻ります。もし NULL だったら何らかの理由(大抵は充分なヒープ領域が残っていなかった)で割り当てができなかったことを示します。この NULLチェック、本当なら必ずやらないといけないのですが、メモリ不足の場合に出来ることがないことが殆どなので、敢えてスルーしているコードも多い(そして、このポインタを使うところで落ちる)ので気をつけましょう。そして処理が進んでもう領域が不要になったら、mallocで戻ってきたポインタを引き数に入れて free を呼び出します。ところで、この関数に戻り値は無いのですが、ここに「正しい」ポインタを渡さないとライブラリ関数内でエラーとなりプログラムが停止することがあります。何をもって「正しい」のかは意外と奥が深くメモリ割り当て処理の内部を把握しないと正確には理解できないのですが、一度 free した領域をもう一度 free してしまって落ちるということが殆どです。そのため free 直後に渡したポインタの値を NULL にしておくというコードを見かけることもあります(NULL を free に渡しても何もしない、これは仕様)。

ところでヒープから割り当てられた領域の有効範囲はプログラム(プロセス)が終了するまでです。それに対し malloc の戻り値を格納するポインタ変数は必ずしも同じではなくローカル変数に割り当てた場合、そのまま free せずに関数を抜けると割り当てられた領域が残ったままポインタを失うことになります(そうなると free する方法が無い)。コードの上では対応する free を書いたつもりになっていても、そのコードを実行せずにポインタ変数の有効範囲を抜けてしまえば割り当てられたメモリは残ったままになってしまう訳です。こうして「メモリ・リーク」と呼ばれる状態が発生して、そのままプログラムの実行が続くと、いずれヒープを使い尽くしてしまい最終的にメモリ不足になってプログラムが動かなくなってしまうことがあるんですね。メモリが潤沢にあったりリークしたメモリが極めて小さいとリークを見つけることは意外と難しく、あまり長い間動き続けない一般的なプログラムならともかくサーバとしてとても長い間、動き続けるプログラムだと、これが大きな問題になることが多いです。

C言語のプログラミングも基本的な書き方に慣れて、だんだん大きなコードを書くようになると、もう嫌になるほどリーク問題と戦うようになります。ですからいろいろなところに注意点が書かれていますし、他の人から指摘も受けますし、痛い目にあう経験も積むようになります。メモリ・リークについて語れるようになればC言語も一人前ということなのかもしれません。

さて話を戻しましょう。malloc で割り当てられた領域へのポインタの「型」は void * です。この型の意味することは void * という型の変数は宣言することは出来ず、戻って来たポインタはポインタ変数に代入する時には必ず()で囲った型キャストを行いポインタ変数の型を指定しなければならないという意味です。100バイト分の文字変数の領域を割り当てるのであれば、

char *p;
p = (char*)malloc(100);

と書くわけです。これで配列としてアクセスすれば p[0]~p[99]までを使うことが出来るわけです。mallocに渡すのはあくまでバイト数なので、整数型で100個分の領域を確保するのであれば、

int *p:
p = (int*)malloc(sizeof(int)*100);

としてp[0]~p[99]までの領域を押さえます(最初の例ではsizeof(char)*100と書くのが本当は正しい)。

そして free の引き数の型が void* なのは、どんなポインタ型の変数であっても構わないという意味で、char* であっても int* であっても型キャストする必要はなく、そのまま渡して良いという意味です(void* に型キャストしてはいけません)。

free(p);

プロトタイプに同じ void* と書いてあっても、渡す時と渡されるときでは、このように扱いが異なることに注意してくださいね。

ヒープからの割り当てと返却に関しては、まだ留意点が山ほどあるのですが、今回はこのくらいにして、次回に続けましょう。

注意点:
* 確保したメモリは必ず解放しましょう。
* 同じメモリを複数回解放する(二重解放)のは危険です。
* 解放済みのメモリにアクセスしないよう注意が必要です。

ヘッダ画像は、以下のものを使わせていただきました。

https://commons.wikimedia.org/wiki/File:ISO_C%2B%2B_Logo.svg
Jeremy Kratz - https://github.com/isocpp/logos , パブリック・ドメイン,
https://commons.wikimedia.org/w/index.php?curid=62851110による

#プログラミング #プログラミング言語 #プログラミング講座 #CPP #C言語 #ヒープ #動的メモリ割り当て #malloc #free #voidポインタ #メモリリーク

この記事が気に入ったらサポートをしてみませんか?