続C言語教室 - 第13回 動的メモリの使い方(その1)
まだC言語教室の方では、ヒープからのメモリ割り当てについて触れていませんでしたが、C++再入門の方で取り上げるので、先にC言語の動的メモリについて簡単に触れました。
C++ 再入門 その11 ヒープからのメモリ割り当てと返却(1) - malloc と free
動的メモリというのは、変数などに割り当てるメモリの大きさが予めわかっていないような時に、実行時にその場で必要なメモリを割り当てて使う仕組みです。
動的メモリ確保
例えばファイルからデータを読み込んで処理を行う場合でも、前もってファイルにいくつのデータが入っているのかは読んでみなければわかりません。動的メモリが使えなければ、処理できる最大の個数というのを前もって決めておいて、その数を超えてしまったらエラーにするくらいしか出来ませんし、どんな少ないデータを処理する場合でも常に最大の個数の時に必要となるメモリを確保しなければなりません。そのパソコンでひとつのプログラムしか実行しなければ、それも悪くはないのですが、複数のプログラムが実行される時に、すべてのプログラムで最も大きい場合のメモリを確保しようとすれば、あっという間にどんなにたくさんのメモリを搭載していようとも足りなくなるのは目に見えています。
そこで必要になった時に必要になっただけメモリを確保しようというのが動的メモリというメモリの種類です。プログラムから見える変数で使うメモリというのは、関数の中だけで使えるローカル変数が割り当てられる「スタック」と、グローバル変数などが使う「静的メモリ」、そしてヒープと呼ばれるプログラム全体から都度、割り当てて使う「動的メモリ」の3種類があります。スタックと静的メモリに関しては変数で宣言すれば自動的に必要なメモリが確保されて、それが使われますが、動的メモリを使うには、実行時にメモリを割り当ててもらう操作を行い、不要になったら割り当てられたメモリを解放する操作が必要になります。
C言語の言語仕様の上では、動的メモリを使う構文はありません。動的メモリが必要になったら標準ライブラリ関数にあるメモリを割り当てる関数を呼び出してメモリを割り当ててもらいます。その戻り値をポインタ変数に格納してポインタを使って割り当てられたメモリを利用します。そして処理が進み割り当てられたメモリが不要となったら、今度は解放するためのライブラリ関数を呼んで割り当てられたメモリを返却し、再び割り当てられるようにします。ここで正しく返却しなければ割り当てられたメモリはプログラムが終了するまで確保されたままとなり、プログラムの他の場所で使うことが出来なくなります。同じ処理を繰り返すようなところで解放を忘れると、いずれメモリを使い尽くして動的メモリを割り当てることが出来なくなってしまい、処理を続けられなくなります。
具体的な動的メモリの使い方を見ていきましょう。代表的で一番良く使うのが malloc 関数です。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// VCで小うるさいことを言われるのでとりあえず無視しておく
#pragma warning(disable:4996)
#define MSG "MESSAGE"
void main() {
size_t len = strlen(MSG);
char *p = (char*)malloc(len + 1); // 末尾の'\0'の分を忘れずに
strcpy(p, MSG);
printf("%s\n", p);
free(p);
}
※あくまでサンプルコードなので、
もろもろのエラーチェックをサボっています。
これでマクロで定義された文字列を、その長さだけのメモリを動的に確保してから、その領域にコピーして出力することが出来ました。
mallocの引き数で渡すのは必要なバイト単位のメモリの大きさです。文字の場合は一つの要素の大きさが1バイトなのはわかっているのですが、必要なサイズは sizeof 演算子を使って取得するのが普通です(int などは一つの要素で何バイトつかうのか分からない)。そして戻り値の型は void * ですから、変数に代入する前に代入するポインタ型に明示的に型変換を行うのが流儀です。malloc はどんな型のポインタとしても使える適切にアライメントされたポインタを返すので安心して型キャストしてください。
これで得られたポインタを使って、割り当てられたメモリを使えるようになります。もし必要なメモリを割り当てることが出来なかった場合は、malloc は NULL を返します。ですから正しくは「常に」戻り値が NULL では無いかの確認が必要です。もしサンプルコードのようにチェックを怠った場合には、続くstrcpy の中で NULL ポインタの値を使ったというので実行時エラーが発生することもありますし、最近の処理系は、それをコンパイル時に見抜いて警告を発するのが普通です(サンプルコードでは#pragmaを使って回避している)。
そして処理が済んだら free に malloc で得られたポインタを渡して割り当てられたメモリを解放します。free でmalloc で割り当てられたのでは「ない」ポインタを渡したり、既に free された解放済みのポインタを渡すと、そこで実行時エラーになることが普通ですが、運が悪いとエラーが発生すること無くヒープが破壊されて、同じプログラムの他の場所で使っている割り当て済みのメモリが破壊されて、素知らぬ顔で実行を継続してしまうこともあります。このような場合、メモリが破壊されたまま他の処理の中で動的メモリを使ったところで、突然、実行時エラーが発生するので、その原因がなかなか発見できないことになるので大変です。
ところで free の引き数に NULL を渡すのは問題ありません。malloc が NULL を返した時にはメモリは「確保されていない」のですが、その場合に free を呼んではいけないのかといえば、NULL で呼び出す分には大丈夫です。同じポインタで何度も free を呼んではいけないのですが、free を呼んだらすぐにポインタに NULL を代入しておけば、間違って呼んでしまっても大丈夫だという技はよく使います。
なお、mallocの引き数に 0 を渡した場合、何が起こるのかというと処理系依存です。割り当てるメモリが無いので、NULLを返すこともあるようですが、大抵の処理系はちゃんと何らかの空っぽのメモリを割り当てて、そこへのポインタを返すようです。もっとも中身が無いので、そのポインタを使えばきっと問題は起きますけどね。
malloc で割り当てられたメモリは、単に使えるようになるだけで、その中身は何らかの初期化をされていません。初期化をサボっている分、大きなメモリを割り当ててもパフォーマンスに変わりは無いのですが、そのままうっかりポインタとして使おうものなら、どこを指しているポインタなのかわかったものではないので、くれぐれも値を入れる前に使わないようにしてください。
ところで、malloc の仲間たちとして、配列向けで初期化もしてくれる calloc と、既に割り当てられたメモリのサイズを変更する realloc、そしてスタックに対して動的にメモリを割り当てる(スタックなので有効範囲を抜ければ自動的に解放される)alloca があるのですが、長くなったので、これらは次の機会に回します。
ヘッダ画像は、AIで生成しました。
この記事が気に入ったらサポートをしてみませんか?