【C言語】realloc関数の使い方・注意点を解説

このページでは、C言語の標準関数である realloc について解説していきます。

この realloc 関数を使いこなすためには、まずは malloc 関数について理解しておいた方が良いと思います。

malloc 関数については下記ページで解説しておりますので、malloc 関数について詳しく知りたい方は下記ページをご参照いただければと思います。

malloc解説ページのアイキャッチ【C言語】malloc関数(メモリの動的確保)について分かりやすく解説

realloc 関数とは

まず前提として、C言語でメモリを使用する際には、あらかじめ使用するメモリを確保する必要があります。

そのメモリを確保する関数が malloc 関数や calloc 関数になります。

例えば addr = malloc(size) を実行した場合、mallocNULL を返却しなければ、malloc 関数の中で addr から size バイトのメモリが確保され、この確保されたメモリはプログラム内で自由自在に扱うことができます。

malloc関数で確保されるメモリの説明図

realloc 関数は、この malloc 関数や calloc 関数によって確保されたメモリを “新たなサイズ” で再度確保し直す関数です(realloc 関数によって再度確保し直したメモリに対して実行することも可能)。

捉え方によっては、単純に malloc 関数等によって確保されたメモリのサイズを変更する関数とも考えることができます。

また、引数の指定の仕方によっては malloc と同じ動作をさせることも可能です。

realloc 関数の書式

realloc 関数の書式は下記のようになります。使用する際には stdlib.h をインクルードする必要があります。

realloc
#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 * については下記ページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。

voidとvoid*型の解説ページのアイキャッチ【C言語】void型とvoid*型(void型ポインタ)について解説

また、第1引数のptr には NULL も指定可能であり、この場合の realloc 関数の動作は malloc 関数と同じです。ですので、このページでは “第1引数の ptrNULL でない場合” の使い方を中心に解説していきたいと思います。

realloc 関数の返却値

realloc 関数は、メモリの再確保に成功した場合、その再確保したメモリの先頭アドレスを返却します。

この場合、realloc 関数実行後、プログラムはこの返却されたアドレスから size バイトのメモリを自由自在に扱うことができます(size は第2引数で指定する値)。

realloc関数の返却アドレスの意味を示す図

もし事前に確保していたメモリのサイズよりも size の方が大きいのであれば、realloc 関数により自由自在に扱えるメモリが増えることになりますし、size の方が小さいのであれば、realloc 関数により自由自在に扱えるメモリが減ることになります(システムに返却される)。

また、メモリの再確保に成功した場合、realloc 関数は NULL を返却します。

基本的には、この返却値を受け取る変数の型は、第1引数 ptr に指定するポインタ変数と同じ型を選択します。

realloc 関数の動作

続いて realloc 関数がどのような動作を行うのかについて解説していきます。

メモリの再確保に成功した場合

引数 ptr に「事前に malloc 関数等で確保したメモリの先頭アドレス」を指定した場合、引数 size で指定したサイズのメモリの再確保が行われます。

この時の realloc 関数では、下記のような処理が行われます。

まず、引数 size で指定したサイズのメモリが新たに確保されます(下の図はメモリ空間を2次元的に表した模式図となります)。

realloc関数の動作の説明図1

また、新たに確保したメモリに、引数 ptr を先頭アドレスとするメモリのデータ(つまり malloc 関数等で事前に確保されていたメモリのデータ)がコピーされます。

realloc関数の動作の説明図2

さらに、引数 ptr を先頭アドレスとするメモリが解放されます。このメモリは解放されますので、メモリの再確保に成功した場合、realloc 関数実行後は引数 ptr にアクセスしてはいけません。この引数 ptr を先頭アドレスとするメモリが解放されるというところが、realloc 関数を使用する上でのポイントの1つになります。

realloc関数の動作の説明図3

上記が行われた後、最後に新たに確保したメモリの先頭アドレスが返却されます。

realloc関数の動作の説明図4

つまり、メモリの再確保に成功した場合、realloc 関数実行後は realloc 関数の返却アドレスから size バイト分のメモリをプログラム内で自由自在に扱えるようになります。

また、データのコピーに対して補足をしておくと、事前に確保していたメモリのサイズ < size の場合、つまり realloc 関数によりメモリサイズが大きくなる場合、事前に確保していたメモリのサイズ 分のデータのみがコピーされます。

拡張された分の領域にはデータのコピーは行われません。したがってその領域のデータは不定値となります。

realloc関数内で行われるデータのコピーの説明図1

逆に 事前に確保していたメモリのサイズ > size の場合、つまり realloc 関数によりメモリサイズが小さくなる場合、size 分のデータのみがコピーがされます。

前述の通り、事前に確保していたメモリは解放されますので、他のメモリに退避しておかない限り、そのコピーされなかったデータは今後使用することができなくなるので注意が必要です。

realloc関数内で行われるデータのコピーの説明図2

基本的には上記のような動作になるのですが、新たに確保したメモリのアドレス、すなわち realloc 関数の返却アドレス と引数 ptr に指定したアドレスが一致する場合もあります。この場合は単純に、ptr を先頭アドレスとするメモリがサイズ変更されただけと考えることができます。

realloc関数の返却アドレスと第1引数が一致する場合の説明図

つまり、例えば下記のように realloc 関数を実行した場合、

realloc関数の実行
/* ptr:事前に確保したメモリを指すポインタ変数 */
ret = realloc(ptr, size);
if (エラーかどうかの判断) {
    エラー処理;
}

realloc 関数実行直後にポインタ変数 ptr が解放されたメモリを指した状態の場合もありますし、

realloc実行直後のretとptrとメモリの関係図1

realloc 関数実行直後にポインタ変数 ptr が再確保されたメモリを指した状態の場合もあることになります。

realloc実行直後のretとptrとメモリの関係図2

なんだか2つの状態があってややこしいですね…。

ただしこれは、realloc 関数実行後に必ずポインタ変数 ptr をポインタ変数 ret の値で上書きし、さらにポインタ変数 retNULL で上書きするようにすることで下の図のような1つの状態にまとめることができます。

2つの状態を1つの状態にまとめた時のptrとretとメモリの関係図

これにより、その後再確保したメモリを扱ったり解放したりする際の処理もシンプルに記述することができるようになりますし、メモリの二重解放や解放済みのメモリのアクセスも防ぎやすくなります。

より具体的には、次のようにコードを記述すれば、上の図のようにポインタ変数 ptr のみが再確保したメモリを指し、ポインタ変数 retNULL を指す状態にまとめることができます。

ptrに再確保したメモリを指させる
/* ptr:事前に確保したメモリを指すポインタ変数 */
ret = realloc(ptr, size);
if (エラーかどうかの判断) {
    エラー処理;
}

/* 再確保したメモリをptrに指させる */
ptr = ret;
ret = NULL;

上記のコードを見て、次のように書いてしまえば良いのでは?と思った方もおられるかもしれません。鋭いです!

直接reallocの返却値をptrに格納する
/* ptr:事前に確保したメモリを指すポインタ変数 */
ptr = realloc(ptr, size);
if (エラーかどうかの判断) {
    エラー処理;
}

ですが、realloc 関数を扱う際には上記のように記述するとメモリの解放漏れが発生する可能性があります(もちろん前後の処理の書き方にもよるのですが…)。

その理由を次に説明していきたいと思います。

メモリの再確保に失敗した場合

ここまでの話は realloc 関数でメモリの再確保に成功した場合の話になります。

続いてrealloc 関数でメモリの再確保に失敗した場合について解説していきます。

realloc 関数では、メモリの再確保に失敗した場合 NULL を返却します。

この場合、事前に確保していた引数 ptr を先頭アドレスとするメモリは解放されません。

reallocでメモリの再確保に失敗した場合にptrのメモリが残ってしまうことを示す図

したがって、realloc 関数で NULL が返却された際には、第1引数の ptr の指すメモリの解放を行わないとメモリリークになるので注意してください。

これはつまり、realloc 関数で NULL が返却した場合に備えて、第1引数 ptr に指定するアドレスは保持しておかなければならないことになります。

したがって、前述でも紹介したように、下記のように処理を記述してしまうと realloc 関数が NULL を返却した際に ptrNULLが格納され、元々 ptr が指していた “事前に確保していたメモリ” のアドレスがわからなくなってしまいます。そうなると、事前に確保していたメモリの解放ができなくなり、メモリの解放漏れが発生します。

直接reallocの返却値をptrに格納する
/* ptr:事前に確保したメモリを指すポインタ変数 */
ptr = realloc(ptr, size);
if (ptr == NULL) {
    free(ptr); /* 必ずfree(NULL)になる */
    return -1;
}

ですので、realloc 関数の返却値は第1引数に指定するポインタ変数とは異なるポインタ変数で受け取る方が良いです。

メモリ解放漏れを考慮した処理1
/* ptr:事前に確保したメモリを指すポインタ変数 */
ret = realloc(ptr, size);
if (ret == NULL) {
    free(ptr); /* 事前に確保していたメモリが解放される */
    return -1;
}

もしくは、下記のように、事前にrealloc 関数の第1引数に指定するアドレスを他のポインタ変数に退避しておくのでも良いです。大事なのは、realloc 関数失敗時に備えて事前に確保したメモリのアドレス(realloc の第1引数に指定するアドレス)を保持しておくことです。

メモリ解放漏れを考慮した処理2
/* 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 関数の動作 で解説した内容のポイントになるところにコメントを記載しています。

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 個分の値を表示)。

3行目の出力
/* 再確保したメモリにデータがコピーされているかを確認 */
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行目の場合、09 のみはコピーされた値ですが、それ以降の値は全て不定値です。

MEMO

上記関数を実行した際に、3行目の結果で 09 よりも後ろ側の値が全て 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を再確保したメモリのアドレスで上書き */
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 関数実行時には、ptrNULLfile_size0 となりますので、この時の realloc 関数の動作は malloc(sizeof(unsigned char) * SIZE) と同様になります。

それ以降は realloc 関数で sizeof(unsigned char) * SIZE ずつメモリのサイズを増やしながらファイルの読み込みを繰り返しています。

また、読み込んだファイルサイズが 0 の場合に NULL を返却するため、下記の処理を行なっています。

ファイルサイズが0の時の処理
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に再確保したメモリを指させる
/* 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 関数の利用が必要な場合は、ぜひこのページで紹介した使用例や注意点を参考にして使用していただければと思います!