Menu

C言語の calloc と realloc:ゼロ初期化されたメモリと配列の拡張

calloc はゼロ初期化されたメモリを返し、サイズの掛け算を安全に行います。realloc はすでに中身を詰めたブロックを拡張します。このページでは両方と、失敗した realloc がメモリを漏らさないための一時ポインタのイディオム、そして malloc + memset より calloc が勝る場面を扱います。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

malloc が答えるのは 1 つの問いだけです。「これだけのバイト数をください」。<stdlib.h> にある 2 つの相棒が、その次に来る問いに答えます。これだけのバイト数を、消去した状態でくださいcalloc)と、いま持っているブロックをもっと大きくしたいrealloc)です。

calloc:個数、サイズ、そしてゼロ

void *calloc(size_t count, size_t size);

引数は 1 つではなく 2 つで、結果はゼロバイトで埋められています。

これが典型的な使い方です。すべての枠がゼロから始まらなければならない、ヒストグラムや集計用の配列です。malloc ならループが必要ですが、calloc は確保の一部としてそれを行います。しかもオペレーティングシステムが元々ゼロ埋めされた新しいページを渡してくるため、多くの場合そのコストはゼロです。

オーバーフローという論拠

引数が 2 つに分かれているのは見た目の問題ではありません。ファイルから読み込んだ個数を考えてみましょう:

size_t n = huge_value_from_input;

int *a = malloc(n * sizeof *a);     /* the product can wrap around */
int *b = calloc(n, sizeof *b);      /* required to detect the overflow */

n * sizeof *asize_t に収まる範囲を超えると小さな数に巻き戻り、malloc はごく小さなブロックで成功し、その後の書き込みはすべて末尾のはるか先へ走ります。calloc は規格により、代わりに失敗して NULL を返すことが要求されています。サイズがプログラムの外から来るとき、このチェックには持っておく価値があります。

「ゼロ初期化」が実際に意味すること

calloc が書き込むのはゼロバイトです。整数と文字にとってそれは値ゼロであり、まさに望むものです。ポインタや浮動小数点数の場合、全ビットゼロは主要なプラットフォームのすべてでヌルポインタと 0.0 になりますが、C言語の規格がそれを約束しているわけではありません。厳密な移植性が必要なコードでは、それらは明示的に代入します。

calloc と malloc + memset

int *a = calloc(n, sizeof *a);

int *b = malloc(n * sizeof *b);
if (b != NULL) {
    memset(b, 0, n * sizeof *b);
}

結果は同じです。calloc を選びましょう。1 行で済み、オーバーフローのチェックをしてくれ、大きなブロックではメモリにまったく触れずに済ませることもできます。どのみち全バイトを上書きするつもりなら、素の malloc を選びましょう。すぐに埋めるメガバイト単位の領域をゼロ埋めするのは、純粋な無駄です。

realloc:サイズを変える

void *realloc(void *p, size_t newsize);

reallocnewsize バイトのブロックを返し、その内容は新旧のうち小さいほうのサイズまで元のブロックと一致します。その場でブロックを延ばすこともあれば、新しく確保してコピーし、古いほうを解放することもあります。どちらが起きたかは分かりませんので、以後使ってよいのは返ってきたポインタだけです。

tmp ポインタのイディオム

tmp のまわりの 3 行こそ、この関数が悪名を得ている理由のすべてです。省略したくなる書き方は壊れています:

/* BUG: leaks the original block when realloc fails */
p = realloc(p, newsize);
if (p == NULL) {
    return;
}

realloc は失敗すると NULL を返し、元のブロックは確保されたまま、そのまま残ります。結果を直接 p に代入すると、そのブロックを指す唯一のポインタを上書きしてしまうので、二度と解放できません。これはリークであり、しかもメモリがすでに逼迫しているまさにそのときに起きます。tmp に代入し、tmp を判定し、それから確定させましょう:

void *tmp = realloc(p, newsize);
if (tmp == NULL) {
    /* p is still valid; handle the failure however suits the caller */
    return 0;
}
p = tmp;

知っておく価値のある挙動があと 2 つあります。realloc(NULL, n)malloc(n) とまったく同じように振る舞うので、拡張用の関数は最初の呼び出しを特別扱いせずに済みます。そして、拡張しても新しいバイトは未初期化のままです。保たれるのは古い内容だけです。

配列を拡張する:標準的なパターン

ほとんどすべての C言語のコンテナが使っているパターンを示します。長さ、容量、そして両者が並んだときの倍増です。

ここで重みを担っている設計上の判断が 2 つあります。

1 ずつ足すのではなく倍にする。 固定量ずつ増やすと push のたびに配列全体をコピーすることになり、n 要素を詰めるのにおよそ バイトのコピーがかかります。倍にすればコピーは十分まれになり、push 1 回あたりのコストは平均して定数時間になります。cap == 0 ? 4 : cap * 2 という出だしは、分岐を別に設けずに最初の push を扱えます。realloc(NULL, n) がただの malloc だからです。

構造体は確保領域を 1 つだけ所有する。 v.data は、その構造体を所有する者によってちょうど 1 回だけ解放されます。この所有のルールを書き残すこと - コメントにするか、すべての vec_initvec_free を対にすること - が、伸びるコンテナを関数のあいだで受け渡してもリークさせないための鍵です。

縮める

サイズを小さくした realloc はブロックを切り詰めます。大きめのバッファに不明な量のデータを読み込んだあとに便利です:

縮小の失敗は、中断するに値するエラーではありません。古い、より大きなブロックはいまだ有効で、いまだ正しいのです。失敗を無視するのが正解である唯一の場面ですが、それでも tmp を経由している点に注目してください。生きているポインタを上書きしないという規則は変わらないからです。

どれに手を伸ばすか

やりたいこと使うもの
バイト数だけ欲しい、中身は問わないmalloc(n * sizeof *p)
要素の個数分を、ゼロに消去して欲しいcalloc(n, sizeof *p)
信頼できない入力から来た個数calloc(オーバーフローのチェックのため)
手持ちのブロックを大きく、または小さくtmp ポインタを経由した realloc
拡張ループでの最初の確保realloc(NULL, n) - 特別扱い不要

4 つとも、同じ free でちょうど 1 回だけ解放されます。

よくある間違い

  • p = realloc(p, n) - 失敗時にリークします。必ず tmp を経由しましょう。
  • ブロック内を指す古いポインタを持ち続ける。 realloc のあとデータは移動しているかもしれないので、保存しておいた内部ポインタやインデックスから求めたアドレスはすべて古くなります。インデックスは移動に耐えますが、ポインタは耐えません。
  • 新しいバイトがゼロだと思い込む。 ゼロにするのは calloc だけです。realloc は拡張された領域を未初期化のままにします。
  • calloc(n * size, 1) - 引数を 2 つに分けた形が存在する理由、すなわちオーバーフローのチェックを捨てています。
  • realloc が成功したあとに古いポインタを解放する。 realloc はすでにそれを解放しています。2 度目の free は二重解放です。

よくある質問

C言語の calloc と malloc の違いは何ですか?

違いは 2 つです。calloc(n, size) は個数と要素サイズを別々に受け取り、それらを掛けてもオーバーフローしないことを確認します。一方 malloc(n * size) は黙って桁あふれし得ます。そして calloc はブロックをゼロで埋めますが、malloc は以前そこにあったものをそのまま残します。

C言語の realloc はどう動きますか?

realloc(p, newsize) は新しいサイズのブロックを返し、既存の内容は新旧のうち小さいほうのサイズまで保たれます。その場で広げることも、移動させることもあるので、以後使ってよいのは返ってきたポインタだけです。古いほうはすでに解放されているかもしれません。

なぜ realloc の結果を一時ポインタに代入すべきなのですか?

p = realloc(p, n) は呼び出しが失敗したときに pNULL で上書きしてしまい、元のブロックは確保されたまま何からも指されなくなる - つまりリークするからです。まず tmp に代入し、NULL でないか確認し、そのうえで p = tmp と書きましょう。

malloc ではなく calloc を使うべきなのはどんなときですか?

メモリがゼロであることが本当に必要なとき(カウンタ配列、フィールドを空から始めたい構造体、部分的にしか埋めないバッファ)か、サイズが「個数 × 要素サイズ」で大きくなり得るときです。どうせ全バイトを上書きするなら、malloc のほうがゼロ埋めの手間を省けます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める