メモリブロックの割付け

こんにちは、めのんです!

お待たせしました!
何日も前に予告していたのにずっと先送りにしてきた割付け記憶域期間の解説をようやく行います。
とても1回では終わりませんので何回かに分けて解説することになります。

今回はざっくりとした概要だけにとどめて、順を追って細かい話をしていけたらと考えています。

malloc関数とfree関数

割付け記憶域期間を持つオブジェクトというのは、主に「malloc」関数によって割り付けられたメモリブロックです。
メモリブロックは文字通りメモリのかたまりで、それだけでは何の意味も持ちません。

malloc関数はポインタを返しますので、それを必要な型へのポインタに格納してください。
以後はそのポインタを使って割り付けたメモリブロックにアクセスします。

メモリブロックの割付け

具体例を挙げますね。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
  int *p = malloc(sizeof(int));
  *p = 123;
  printf("%d\n", *p);
  return 0;
}

上のサンプルコードでは、6行目でmalloc関数を使ってint型に必要な大きさのメモリブロックを割り付けています。
int型に必要な大きさなのでsizeof演算子でint型のサイズを調べて渡しています。

割り付けられたメモリブロックは以後int型のオブジェクトとして振る舞う必要がありますので、int型へのポインタpに格納しています。
これでメモリブロックはint型のオブジェクトとして扱えるようになりました。

malloc関数が割り付けたばかりのメモリブロックの内容は不定ですので、必ず値を明示的に格納してから使うようにしてください。

ところで、malloc関数を使うには「stdlib.h」ヘッダをインクルードする必要があります。
忘れないようにしてくださいね。

メモリブロックの解放

さきほどのサンプルコードでは、malloc関数で割り付けたメモリブロックを解放せずにそのまま放置していました。
ですが、こういうプログラムは悪い例です。
割り付けたメモリブロックが必要なくなれば必ず解放してあげなければなりません。

メモリブロックの解放に使うのは「free」関数です。
さきほどのサンプルコードをfree関数を使ってメモリブロックを解放するように修正してみます。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
  int *p = malloc(sizeof(int));
  *p = 123;
  printf("%d\n", *p);
  free(p);
  return 0;
}

free関数にmalloc関数など(malloc関数以外は別の機会に解説します)で割り付けたポインタ以外を渡した場合は未定義の動作になります。
また、すでに解放済みのメモリブロックを指すポインタを渡した場合も未定義の動作になります。

空ポインタ(NULL)を渡した場合は単に無視されるだけですので、free関数でメモリブロックを解放したあとはポインタにNULLを代入しておく方が安全だと思います。

  ...
  printf("%d\n", *p);
  free(p);
  p = NULL;   // 安全のためにNULLを代入
  return 0;
}

配列の割付け

ここまでの解説では、int型1要素のオブジェクトに相当するメモリブロックを割り付ける場合でした。
これはint型に限らず、double型でも構造体型でも同じことがいえます。

もちろん配列型でも同じで、次のように書くことができます。

  int (*p)[10] = malloc(sizeof(int[10])); // 配列を割り付ける

  for (int i = 0; i < 10; i++)
    (*p)[i] = 123;

これでもいいんですが、毎回(*p)のように丸括弧で囲まないといけないのは面倒ですし、間違いのもとになります。
できればもう少し簡単に扱いたいですね。

そんなときはこのようにします。

  int *p = malloc(sizeof(int) * 10); // int型10要素分の大きさを割り付ける

  for (int i = 0; i < 10; i++)
    p[i] = 123;

これで普通の配列同じように扱えるようになりました。

ポイントはsizeof(int) * 10のように要素数をsizeof(int)に掛けているところです。
簡単ですね。

このように複数要素分のメモリブロックを割り付けた場合でも、解放するときは普通にfree関数にポインタを渡すだけでかまいません。

割付け記憶域期間

malloc関数などで割り付けたオブジェクトは「割付け記憶域期間」を持ちます。
割付け記憶域期間を持つオブジェクトはfree関数で解放するまでが生存期間になります。

ソースコード上のブロックなどとは完全に独立した記憶域機関を持ちますので、静的記憶域間と同じく関数やブロックから抜けても勝手になくなることはありません。
その代わり自分で明示的に解放しないといけないので、解放忘れや二重解放といった不具合につながりやすい欠点もあります。

よく「Cは自分でメモリ管理をしないといけない」という話を耳にすると思います。
それはこの割付け記憶域期間を持つオブジェクトのことです。

割付け記憶域期間を持つオブジェクトは必ずしも使う必要はありませんし、使うとしても場所を最小限に絞ればそれほど扱いが難しいわけではありません。
ただ、PHPのようなスクリプト言語と同じ感覚で設計すると、どうしてもmallocやfree関数に頼るしかなくなってしまいます。

高い信頼性が必要なソフトウェアでは、いっそmallocやfree関数を一切使わないという選択もありだと思いますし、実際そのようなソフトウェアもたくさんあります。
必要に応じて使い機能を取捨選択するのが一番だと思います。

void型へのポインタ

ここまであえて触れませんでしたが、malloc関数の返却値はどんな型へのポインタにでも代入できてしまいます。
また、free関数はどんな型へのポインタでも渡せてしまいます。

普通はそんなことはなくて、違う型へのポインタに代入しようとすると、コンパイラは警告を出すことが多いですし、未定義の動作を起こす原因にもなります。
では、malloc関数やfree関数はどうなっているのでしょうか?

その答えを知るために、stdlibヘッダでの中にある
malloc関数やfree関数の宣言を見てみましょう。

void *malloc(size_t);
void free(void*);

どちらにも「void*」という型が使われていますね。
これがvoidへのポインタです。

voidへのポインタは、どんな型へのポインタにも暗黙的に型変換できますし、どんな型へのポインタからもvoidへのポインタに暗黙に型変換できます。

「どんな型へのポインタ」と書きましたが、実際には少し制限があります。

相互に型変換できるのは、オブジェクト型か不完全型へのポインタだけです。
不完全型については別の機会に詳しく解説しようと思います。
簡単にいうと関数型へのポインタ以外ならOKということです。

もうひとつ制約があって、const修飾子などの型修飾子が付く方向には暗黙的に型変換できるのですが、外す方向には暗黙に型変換できません。
具体的にはこんな感じです。

  void *p = malloc(10);
const int *p2 = p;   // OK
const void* p3 = p2; // これもOK
int *p4 = p3:        // これはダメ

今回の解説は以上となります。
これだけの内容であれば難しい要素はほとんどないんじゃないでしょうか?

簡単なことでもそれを応用するとなれば難しいということはよくあります。
応用についても折を見て解説していきますね。

次回は境界調整について解説したいと思います。
これも割付け記憶域期間を持つオブジェクトとそれなりに深い関係があります。
どうかご期待ください!