このページでは、C言語の標準関数である realloc
について解説していきます。
この realloc
関数を使いこなすためには、まずは malloc
関数について理解しておいた方が良いと思います。
malloc
関数については下記ページで解説しておりますので、malloc
関数について詳しく知りたい方は下記ページをご参照いただければと思います。
Contents
realloc
関数とは
まず前提として、C言語でメモリを使用する際には、あらかじめ使用するメモリを確保する必要があります。
そのメモリを確保する関数が malloc
関数や calloc
関数になります。
例えば addr = malloc(size)
を実行した場合、malloc
が NULL
を返却しなければ、malloc
関数の中で addr
から size
バイトのメモリが確保され、この確保されたメモリはプログラム内で自由自在に扱うことができます。
realloc
関数は、この malloc
関数や calloc
関数によって確保されたメモリを “新たなサイズ” で再度確保し直す関数です(realloc
関数によって再度確保し直したメモリに対して実行することも可能)。
捉え方によっては、単純に malloc
関数等によって確保されたメモリのサイズを変更する関数とも考えることができます。
また、引数の指定の仕方によっては malloc
と同じ動作をさせることも可能です。
realloc
関数の書式
realloc
関数の書式は下記のようになります。使用する際には stdlib.h
をインクルードする必要があります。
#include <stdlib.h>
void *
realloc(void *ptr, size_t size);
スポンサーリンク
realloc
関数の引数
realloc
関数の引数は下記の2つになります。
ptr
:事前にmalloc
関数等で確保したメモリのアドレスを指定(型はvoid *
)NULL
も指定可能
size
:再確保したいメモリのサイズ(変更後のメモリのサイズ)をバイト単位で指定(型はsize_t
)
第1引数の ptr
の型は void *
ですので、ポインタ型であればどの型の変数でも指定可能です(int *
でも char *
でも void *
でも…)。
void *
については下記ページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。
また、第1引数のptr
には NULL
も指定可能であり、この場合の realloc
関数の動作は malloc
関数と同じです。ですので、このページでは “第1引数の ptr
が NULL
でない場合” の使い方を中心に解説していきたいと思います。
realloc
関数の返却値
realloc
関数は、メモリの再確保に成功した場合、その再確保したメモリの先頭アドレスを返却します。
この場合、realloc
関数実行後、プログラムはこの返却されたアドレスから size
バイトのメモリを自由自在に扱うことができます(size
は第2引数で指定する値)。
もし事前に確保していたメモリのサイズよりも size
の方が大きいのであれば、realloc
関数により自由自在に扱えるメモリが増えることになりますし、size
の方が小さいのであれば、realloc
関数により自由自在に扱えるメモリが減ることになります(システムに返却される)。
また、メモリの再確保に成功した場合、realloc
関数は NULL
を返却します。
基本的には、この返却値を受け取る変数の型は、第1引数 ptr
に指定するポインタ変数と同じ型を選択します。
realloc
関数の動作
続いて realloc
関数がどのような動作を行うのかについて解説していきます。
メモリの再確保に成功した場合
引数 ptr
に「事前に malloc
関数等で確保したメモリの先頭アドレス」を指定した場合、引数 size
で指定したサイズのメモリの再確保が行われます。
この時の realloc
関数では、下記のような処理が行われます。
まず、引数 size
で指定したサイズのメモリが新たに確保されます(下の図はメモリ空間を2次元的に表した模式図となります)。
また、新たに確保したメモリに、引数 ptr
を先頭アドレスとするメモリのデータ(つまり malloc
関数等で事前に確保されていたメモリのデータ)がコピーされます。
さらに、引数 ptr
を先頭アドレスとするメモリが解放されます。このメモリは解放されますので、メモリの再確保に成功した場合、realloc
関数実行後は引数 ptr
にアクセスしてはいけません。この引数 ptr
を先頭アドレスとするメモリが解放されるというところが、realloc
関数を使用する上でのポイントの1つになります。
上記が行われた後、最後に新たに確保したメモリの先頭アドレスが返却されます。
つまり、メモリの再確保に成功した場合、realloc
関数実行後は realloc
関数の返却アドレスから size
バイト分のメモリをプログラム内で自由自在に扱えるようになります。
また、データのコピーに対して補足をしておくと、事前に確保していたメモリのサイズ < size
の場合、つまり realloc
関数によりメモリサイズが大きくなる場合、事前に確保していたメモリのサイズ
分のデータのみがコピーされます。
拡張された分の領域にはデータのコピーは行われません。したがってその領域のデータは不定値となります。
逆に 事前に確保していたメモリのサイズ > size
の場合、つまり realloc
関数によりメモリサイズが小さくなる場合、size
分のデータのみがコピーがされます。
前述の通り、事前に確保していたメモリは解放されますので、他のメモリに退避しておかない限り、そのコピーされなかったデータは今後使用することができなくなるので注意が必要です。
基本的には上記のような動作になるのですが、新たに確保したメモリのアドレス、すなわち realloc 関数の返却アドレス
と引数 ptr
に指定したアドレスが一致する場合もあります。この場合は単純に、ptr
を先頭アドレスとするメモリがサイズ変更されただけと考えることができます。
つまり、例えば下記のように realloc
関数を実行した場合、
/* ptr:事前に確保したメモリを指すポインタ変数 */
ret = realloc(ptr, size);
if (エラーかどうかの判断) {
エラー処理;
}
realloc
関数実行直後にポインタ変数 ptr
が解放されたメモリを指した状態の場合もありますし、
realloc
関数実行直後にポインタ変数 ptr
が再確保されたメモリを指した状態の場合もあることになります。
なんだか2つの状態があってややこしいですね…。
ただしこれは、realloc
関数実行後に必ずポインタ変数 ptr
をポインタ変数 ret
の値で上書きし、さらにポインタ変数 ret
を NULL
で上書きするようにすることで下の図のような1つの状態にまとめることができます。
これにより、その後再確保したメモリを扱ったり解放したりする際の処理もシンプルに記述することができるようになりますし、メモリの二重解放や解放済みのメモリのアクセスも防ぎやすくなります。
より具体的には、次のようにコードを記述すれば、上の図のようにポインタ変数 ptr
のみが再確保したメモリを指し、ポインタ変数 ret
が NULL
を指す状態にまとめることができます。
/* ptr:事前に確保したメモリを指すポインタ変数 */
ret = realloc(ptr, size);
if (エラーかどうかの判断) {
エラー処理;
}
/* 再確保したメモリをptrに指させる */
ptr = ret;
ret = NULL;
上記のコードを見て、次のように書いてしまえば良いのでは?と思った方もおられるかもしれません。鋭いです!
/* ptr:事前に確保したメモリを指すポインタ変数 */
ptr = realloc(ptr, size);
if (エラーかどうかの判断) {
エラー処理;
}
ですが、realloc
関数を扱う際には上記のように記述するとメモリの解放漏れが発生する可能性があります(もちろん前後の処理の書き方にもよるのですが…)。
その理由を次に説明していきたいと思います。
メモリの再確保に失敗した場合
ここまでの話は realloc
関数でメモリの再確保に成功した場合の話になります。
続いてrealloc
関数でメモリの再確保に失敗した場合について解説していきます。
realloc
関数では、メモリの再確保に失敗した場合 NULL
を返却します。
この場合、事前に確保していた引数 ptr
を先頭アドレスとするメモリは解放されません。
したがって、realloc
関数で NULL
が返却された際には、第1引数の ptr
の指すメモリの解放を行わないとメモリリークになるので注意してください。
これはつまり、realloc
関数で NULL
が返却した場合に備えて、第1引数 ptr
に指定するアドレスは保持しておかなければならないことになります。
したがって、前述でも紹介したように、下記のように処理を記述してしまうと realloc
関数が NULL
を返却した際に ptr
に NULL
が格納され、元々 ptr
が指していた “事前に確保していたメモリ” のアドレスがわからなくなってしまいます。そうなると、事前に確保していたメモリの解放ができなくなり、メモリの解放漏れが発生します。
/* ptr:事前に確保したメモリを指すポインタ変数 */
ptr = realloc(ptr, size);
if (ptr == NULL) {
free(ptr); /* 必ずfree(NULL)になる */
return -1;
}
ですので、realloc
関数の返却値は第1引数に指定するポインタ変数とは異なるポインタ変数で受け取る方が良いです。
/* ptr:事前に確保したメモリを指すポインタ変数 */
ret = realloc(ptr, size);
if (ret == NULL) {
free(ptr); /* 事前に確保していたメモリが解放される */
return -1;
}
もしくは、下記のように、事前にrealloc
関数の第1引数に指定するアドレスを他のポインタ変数に退避しておくのでも良いです。大事なのは、realloc
関数失敗時に備えて事前に確保したメモリのアドレス(realloc
の第1引数に指定するアドレス)を保持しておくことです。
/* ptr:事前に確保したメモリを指すポインタ変数 */
old = ptr; /* 事前に確保したメモリのアドレスを退避 */
ptr = realloc(ptr, size);
if (ptr == NULL) {
free(old); /* 事前に確保していたメモリが解放される */
return -1;
}
realloc
関数で NULL
以外を返却した場合は、事前に確保したメモリのアドレスは不要ですので、このアドレスは忘れてしまって問題ありません。
スポンサーリンク
realloc
関数の使用例
ここまでの realloc
関数の動作の解説を踏まえ、次は realloc
関数の簡単な使用例を参照しながら realloc
関数の使い方を解説していきます。
realloc
関数の基本的な使用例
下記は、まず最初に malloc
関数で int
型の変数 OLD_SIZE
個分のサイズのメモリを確保し、さらにその後に realloc
関数で int
型の変数 NEW_SIZE
個分のサイズのメモリを再確保する関数の例となります。
realloc 関数の動作 で解説した内容のポイントになるところにコメントを記載しています。
#include <stdio.h>
#include <stdlib.h>
#define OLD_SIZE 10
#define NEW_SIZE 16
int realloc_use(void) {
int *ptr = NULL;
int *ret = NULL;
int i;
/* 事前にmalloc関数でメモリを確保 */
ptr = malloc(sizeof(int) * OLD_SIZE);
if (ptr == NULL) {
return -1;
}
printf("ptr = %p\n", ptr);
for (i = 0; i < OLD_SIZE; i++) {
ptr[i] = i;
}
/* reallocで新しいサイズでメモリを再確保 */
ret = realloc(ptr, sizeof(int) * NEW_SIZE);
if (ret == NULL) {
/* 事前に確保したメモリは未解放の状態なのでここで解放 */
free(ptr);
return - 1;
}
/* 再確保したメモリのアドレスを確認 */
printf("ret = %p\n", ret);
/* ptrを再確保したメモリのアドレスで上書き */
ptr = ret;
ret = NULL;
/* 再確保したメモリにデータがコピーされているかを確認 */
for (i = 0; i < NEW_SIZE; i++) {
printf("%d, ", ptr[i]);
}
printf("\n");
for (i = OLD_SIZE; i < NEW_SIZE; i++) {
ptr[i] = i;
}
for (i = 0; i < NEW_SIZE; i++) {
printf("%d, ", ptr[i]);
}
printf("\n");
/* 再確保したメモリを解放(事前に確保したメモリの解放は不要) */
free(ptr);
return 0;
}
私の環境で上記の関数を実行した際の表示結果は次の通りになりました。
ptr = 0x100205dc0 ret = 0x100205dc0 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1836016431, 1886413102, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
特にこの表示結果の1行目から3行目が、realloc
関数の動作のポイントを示してくれていると思います。
順番前後しますが、まず3行目は、再確保したメモリの中身を全て realloc
関数実行直後に printf
で出力した結果になります。この出力を行なっているのが下記部分になります(int
型の変数として NEW_SIZE
個分の値を表示)。
/* 再確保したメモリにデータがコピーされているかを確認 */
for (i = 0; i < NEW_SIZE; i++) {
printf("%d, ", ret[i]);
}
printf("\n");
事前に下記部分で malloc
関数で確保したメモリに OLD_SIZE
個分のint
型の値を格納しており、上記の3行目の表示結果より、このメモリのデータが再確保したメモリに realloc
関数内でコピーされていることが確認できると思います。
for (i = 0; i < OLD_SIZE; i++) {
ptr[i] = i;
}
realloc 関数の動作 でも解説しましたが、このように、realloc
関数で再確保したメモリには事前に確保したメモリ(引数 ptr
を先頭アドレスとするメモリ)のデータがコピーされます。
今回は事前に確保したメモリのサイズが再確保したメモリのサイズよりも小さいので、コピーされるのは事前に確保したメモリのサイズ分のみとなります(上記の関数の場合は sizeof(int) * OLD_SIZE
分のみコピーされる)。
ですので、拡張された領域のメモリのデータは不定値となっています。上記の3行目の場合、0
〜 9
のみはコピーされた値ですが、それ以降の値は全て不定値です。
上記関数を実行した際に、3行目の結果で 0
〜 9
よりも後ろ側の値が全て 0
になる場合もあります
ただしそれは、たまたまそのメモリに 0
が格納されていただけであり、毎回全て 0
が格納されることは保証されないので注意してください
また上記の表示結果の1行目は、 malloc
関数によって事前に確保したメモリの先頭アドレスを表示した結果であり、さらに2行目は、realloc
関数によって再確保したメモリの先頭アドレスを表示した結果となります。
これらの2つのアドレスは全く同じであり、単にメモリのサイズが変更されただけであることが確認できます。
ただし、realloc 関数の動作 でも解説したように、事前に確保したメモリの先頭アドレスと再確保したメモリの先頭アドレスは異なる場合もあります。実際に私の環境で試した際には、OLD_SIZE
と NEW_SIZE
を大きく変更してみたところ、表示結果は下記のようになりました(3行目以降は省略)。
ptr = 0x101008200 ret = 0x101808200
こんな感じで、事前に確保したメモリの先頭アドレスと再確保したメモリの先頭アドレスが一致しない場合もあります。
もちろんこのような場合でも、再確保したメモリには事前に確保したメモリの内容はコピーされますので、あたかも事前に確保したメモリのサイズの変更のみが行われたかのように再確保したメモリを扱うことができます。
また、これも realloc 関数の動作 で解説した通り、確保したメモリの先頭アドレスと再確保したメモリの先頭アドレスが一致する場合、realloc
関数の第1引数に指定するポインタ変数 ptr
は再確保したメモリの先頭を指すことになり、一致しない場合は ptr
は解放済みのメモリの先頭を指すことになります。
ただし、下記を実行することで、両者のどちらの場合であっても ptr
が再確保したメモリを指すようになり、それ以降は、両者のどちらの場合であったかを意識することなく処理を記述することができています。
/* ptrを再確保したメモリのアドレスで上書き */
ptr = ret;
ret = NULL;
realloc
関数を用いたファイルの読み込み
realloc
関数の使用例の最後として、realloc
関数を用いてファイル読み込みを行う関数 readFile1
の紹介を行います。
第1引数に読み込みたいファイルのファイルポインタ、さらに第2引数にファイルサイズの格納先のメモリのアドレスを指定して readFile1
関数を実行すれば、ファイルの中身全体がコピーされたメモリの先頭アドレスを readFile1
関数の返却値として得ることができます(ファイルサイズは第2引数に指定したアドレスに格納される)。
#include <stdio.h>
#include <stdlib.h>
#define SIZE 1024
unsigned char *readFile1(FILE *fi, size_t *p_file_size) {
unsigned char *ptr = NULL;
unsigned char *ret = NULL;
size_t read_size; /* freadで読み込んだサイズ */
size_t file_size; /* freadで読み込んだサイズの合計 */
/* freadで読み込んだサイズの合計を0に初期化 */
file_size = 0;
*p_file_size = 0;
/* freadで読み込めるサイズがSIZE未満になるまでファイル読み込み */
do {
/* メモリサイズをSIZE増やしてメモリを再確保 */
ret = realloc(ptr, sizeof(unsigned char) * (file_size + SIZE));
if (ret == NULL) {
free(ptr);
return NULL;
}
/* 再確保したメモリをptrに指させる */
ptr = ret;
ret = NULL;
/* ファイルから新たにSIZEバイトのデータを読み込み */
read_size = fread(ptr + file_size, 1, SIZE, fi);
file_size += read_size;
} while (read_size == SIZE);
if (file_size == 0) {
/* 読み込んだサイズの合計が0ならNULLを返却 */
free(ptr);
return NULL;
}
/* ファイルサイズに合わせてメモリを再確保 */
ret = realloc(ptr, sizeof(unsigned char) * file_size);
if (ret == NULL) {
free(ptr);
return NULL;
}
/* 再確保したメモリをptrに指させる */
ptr = ret;
ret = NULL;
/* ファイルサイズを設定 */
*p_file_size = file_size;
return ptr;
}
int main(void) {
unsigned char *data;
FILE *fi;
size_t file_size;
fi = fopen("inputfilename", "rb");
if (fi == NULL) {
return -1;
}
/* ファイルの読み込みを実行 */
data = readFile1(fi, &file_size);
fclose(fi);
if (data == NULL) {
return -1;
}
FILE *fo = fopen("outputfilename", "wb");
if (fo == NULL) {
free(data);
return -1;
}
/* ファイルの書き込みを実行 */
if (fwrite(data, 1, file_size, fo) != file_size) {
free(data);
fclose(fo);
return -1;
}
fclose(fo);
/* readFile1内で確保したメモリを解放 */
free(data);
return 0;
}
ファイル全体を読み込む際は、まずファイルサイズを調べてから必要なメモリを確保し、それからファイルを読み込むというやり方が多いと思います。ですが、realloc
関数を利用すれば、上記の関数のように事前にファイルサイズを調べることなくファイルの読み込みを行うこともできます(ファイルの終端まで読みこんだかどうかは fread
の返却値から判断できる)。
一回目の realloc
関数実行時には、ptr
は NULL
で file_size
は 0
となりますので、この時の realloc
関数の動作は malloc(sizeof(unsigned char) * SIZE)
と同様になります。
それ以降は realloc
関数で sizeof(unsigned char) * SIZE
ずつメモリのサイズを増やしながらファイルの読み込みを繰り返しています。
また、読み込んだファイルサイズが 0
の場合に NULL
を返却するため、下記の処理を行なっています。
if (file_size == 0) {
/* 読み込んだサイズの合計が0ならNULLを返却 */
free(ptr);
return NULL;
}
この処理は環境によっては不要な場合もあると思います。
私の環境だと realloc(ptr, 0)
を実行しても realloc
関数の返却値が NULL
になってくれないのですが、環境によっては realloc(ptr, 0)
を実行した場合に NULL
が返却される場合もあり、その場合は上記の処理を削除してもファイルサイズが 0
の時に NULL
を返却することができると思います。
スポンサーリンク
realloc
関数使用時の注意点
最後に、ここまでの解説のまとめの意味も含めて、realloc
関数使用時の注意点について解説しておきます。
メモリの解放漏れに注意
realloc
関数では、メモリの再確保に成功した際には第1引数で指定したアドレスのメモリを関数内で解放してくれますが、メモリの再確保に失敗した場合は解放されません。
メモリの再確保に失敗した場合、第1引数で指定したアドレスのメモリは別途解放してやる必要があるので注意してください。これを怠るとメモリの解放漏れになります。
メモリの解放漏れが発生するコードの例や修正方法等は メモリの再確保に失敗した場合 で説明していますので、詳しく知りたい方は メモリの再確保に失敗した場合 を参照いただければと思います。
メモリの二重解放や解放済みのメモリへのアクセスに注意
また、realloc
関数でメモリの再確保に成功した場合、第1引数に指定したポインタ変数が解放済みのメモリを指している場合と再確保したメモリを指している場合とがあります。
前者の場合、第1引数に指定したポインタ変数を利用してメモリにアクセスすると解放済みのメモリへのアクセスになりますし、そのポインタ変数に格納されているアドレスのメモリを解放すると二重解放になります。
これらを行うとメモリ破壊が発生する可能性がありますので注意してください。
これらの注意点に関しては、realloc 関数の動作 でも解説したように、realloc
関数実行後に下記のように第1引数に指定したポインタ変数を再確保したメモリのアドレスで上書きしてやることで避けやすくなると思います。
/* ptr:事前に確保したメモリを指すポインタ変数 */
ret = realloc(ptr, size);
if (ptr == NULL) {
free(ptr);
return -1;
}
/* 再確保したメモリをptrに指させる */
ptr = ret;
ret = NULL;
ポインタ変数で解放済みのメモリのアドレスを覚えていると、間違ってそのポインタ変数を利用して解放済みのメモリにアクセスしてしまったり、間違ってそのポインタ変数でメモリの解放を行ってしまったりする可能性があります。
なので、解放済みのメモリのアドレスは忘れてしまった方が良いです。その忘れる処理が、上記の ptr = ret
となります。
また、ret = NULL
をしておくことで、間違って free(ptr)
と free(ret)
の両方を実行してしまった場合でも二重解放を避けることができるようになります(free(NULL)
自体は実行しても良い。無駄な処理になるけど)。
スポンサーリンク
処理効率やメモリ効率の低下に注意
また realloc
関数では、第1引数で指定したアドレスのメモリのデータが再確保したメモリにコピーされます。
このコピーが頻繁に行われると処理効率が低下するので注意してください。
例えば realloc 関数を用いたファイルの読み込み では、ループの中で realloc
関数を実行しているので realloc
関数が大量に実行され、処理効率が低下する可能性があります。
特に realloc
関数により “少しずつ” メモリのサイズを大きくしながら処理を行うような場合、処理効率が著しく低下する可能性があります。ですので、realloc
関数の第2引数のサイズを大きくする or 必要なサイズを確定してから一度に必要なメモリを malloc
関数で確保するようにした方が処理効率の点では良いです。
また、realloc
関数を何回も実行するとメモリのフラグメンテーションも起こりやすくなりますので、この点についても注意が必要です。
まとめ
このページでは、C言語における realloc
関数について解説しました!
realloc
関数は、malloc
関数等で事前に確保したメモリをサイズを変更した状態で再確保する関数です。メモリのアドレスに拘らないのであれば、単に、事前に確保したメモリをサイズ変更する関数と捉えても問題ありません。
また、事前に確保していたメモリの先頭アドレスと、realloc
関数で再確保したメモリの先頭アドレスが一致する場合としない場合がありますが、どちらの場合であるかを意識することなく、再確保したメモリを使用することができます。
さらに、realloc
関数ではメモリの再確保に成功した場合、事前に確保していたメモリは解放されますが、失敗した場合は事前に確保していたメモリは解放されません。失敗した場合に事前に確保していたメモリの解放を忘れるとメモリリークになるので注意してください。
もちろん後からメモリのサイズを変更できるという点では realloc
関数は便利なのですが、このページを読んで、使い方が難しい関数であると感じた方も多いのではないかと思います。
malloc
関数の方がシンプルに扱えますので、可能であれば malloc
関数を利用する方が良いのではないかと思います。realloc
関数の利用が必要な場合は、ぜひこのページで紹介した使用例や注意点を参考にして使用していただければと思います!