【C言語】realloc関数|正しい使い方と注意点 メモリ断片化など

realloc関数

reallocはメモリを再割り当て/再確保するための関数です。既存のメモリ領域のサイズを拡張/縮小する用途に利用します。

#inlcude <stdlib.h>
void *realloc(void *ptr, size_t size);
第一引数ptrメモリオブジェクトへのポインタ(NULL可)
第二引数size変更後のメモリサイズ(バイト単位)
戻り値──再確保後のアドレス(失敗時NULL)

基本動作

第一引数に指定されたメモリブロックのサイズを、第二引数に指定されたメモリサイズへと拡張/縮小します。なお第一引数にNULLポインタが渡された場合には、malloc関数と同等の働きをします(malloc(size)相当)。

void *a = realloc(NULL, 1); // `malloc(1)`と同等(メモリ領域の確保)
void *b = realloc(a, 2);    // メモリ領域の拡張

仕様

reallocは第一引数に与えられたメモリオブジェクトを解放し、新たなオブジェクトへのポインタを戻り値として返します。その際、元オブジェクトの値は新たなオブジェクトへとコピーされます。

ただし、第一引数に指定したポインタのアドレスと戻り値のアドレスが同一のアドレスになる場合もあります。また、メモリ確保の失敗時にはNULLが返されるため、reallocの使い方にはちょっとしたコツや注意が必要となります。

void *a = realloc(NULL, 1); // メモリ確保
void *b = realloc(a, 2);    // メモリ拡張

if (b == NULL) {      /* 失敗時 */
  free(a);            // 必要に応じて元のアドレスを手動で解放
  exit(EXIT_FAILURE); // 必要に応じてプログラムを終了
}

if (a != b) {         /* アドレスが変化した場合 */
  // memcpy(b, a, 1); // コピーは不要(既にコピー済み)
  // free(a);         // 元オブジェクトは解放済みのため不要
  a = b;              // 必要に応じてポインタ変数を更新
}

free(a);

アドレス変化時の挙動

ポインタのアドレスが変化した場合には、元のオブジェクトは自動的に解放されます。その際、元オブジェクトのデータは移動先のオブジェクトへと自動的にコピーされます。

拡張時/縮小時の挙動

reallocによって拡張された分の領域には、初期値は指定されず、不定な値が充てられます。逆にサイズが縮小された場合には、縮んだ分の領域は不定なものとなります。

失敗時の挙動

再割り当てが失敗した場合には、元オブジェクトの値が変化することは無く、またオブジェクトが解放されることもありません。なおrealloc関数の戻り値は空ポインタ(NULL)となります。

メモリリークの発生に注意する

これはrealloc関数利用時に陥りがちな罠なのですが、realloc関数の戻り値を元オブジェクトのポインタ変数に対して即時代入してはいけません。

void *ptr = malloc(1);
ptr = realloc(ptr, 2); /* 再確保が失敗したと仮定 */
ptr; // ptrはNULL
     // mallocで確保したアドレスにはもうアクセスできない
     // そのため元オブジェクトは解放されないままとなる
     // よってこのような記述はメモリリークの原因となる
free(ptr); // free(NULL)はエラーにならない
           // そのためメモリリークの発見も遅れやすい

realloc関数では、再割り当てが失敗した際には元オブジェクトのポインタが解放されないままとなります。上記の例ではポインタ変数ptrのアドレスは空ポインタとなるため、元オブジェクトへのアドレスが失われてしまいます。そのため上記のmalloc関数で確保したオブジェクトは解放されないままとなり、メモリリークが発生することになります。

realloc関数利用時には必ず、一時変数を利用して空ポインタのチェックを行うようにし、再確保の失敗時には必要に応じて元オブジェクトを解放する必要があります(解放せずにそのまま使い続けることも可能です)

void *ptr = malloc(1);
void *tmp = realloc(ptr, 2);
if (tmp == NULL) { /* 失敗時 */
  free(ptr); // 必要に応じて元オブジェクトを解放
  return NULL;
} else {
  ptr = tmp;
}

メモリの断片化を引き起こしやすい

reallocでは、元のメモリのアドレスと、再確保後のメモリのアドレスが異なるアドレスになる場合があります。これは、realloc時にメモリの空きブロックが足りず、元のメモリブロックを十分な長さに拡張できないような場合に、より広いメモリ領域にメモリブロックが移動されるためです。

その際、メモリブロックの余計な移動作業やコピーが発生するため、余計な処理コストの発生やパフォーマンスの低下に繋がるほか、頻繁な移動によってメモリの断片化(フラグメンテーション)を引き起こしやすくなる場合があります。また頻繁なreallocの利用は断片化の影響を受けやすいという懸念もあります。

これらの問題を回避するには、事前に大きめのメモリサイズを確保し、再確保の回数を極力減らすような工夫が必要となります。

realloc(ptr, 0)について

realloc関数の第一引数に指定された元オブジェクトが空ポインタ以外の場合でかつ、第二引数のサイズに0が指定された場合の処理(realloc(ptr, 0))について、以前は元オブジェクトに対する解放処理(free(ptr))が働くと言われていましたが、現在では異なる挙動をとる場合があるため、誤って利用しないよう注意する必要があります。

実際に、現在では処理系によって異なる挙動がとられています。手元のClangコンパイラでは同処理系におけるmalloc(0)と同等の挙動を取るようです。元オブジェクトは解放されますが、戻り値はNULLではなく、新たに割り当てられたオブジェクトとなります。

void *a = malloc(1);
void *b = realloc(a, 0);
assert(b != NULL && a != b);
free(b);
free(b); // error for object 0x0000000100403ab0
free(a); // error for object 0x0000000100402170

コードの移植性を考慮する場合、reallocの解放処理としての利用は極力避ける必要があります。

広告
広告