C言語の動的配列のリサイズ方法

133, 2020-12-11

目次

C言語の動的配列のリサイズ方法

C言語で動的なメモリの確保で配列のメモリを確保すると、実行時に可変長な長さの配列を作ることが出来ます。
この配列を「動的配列」とか「可変長配列」などといいます。

高度なC言語のプログラミングでは、この動的配列を使う機会が非常に多いです。
モジュールを動的配列で実装することで、そのモジュールの設計における柔軟性が向上し、便利になります。

この記事ではC言語による動的配列の作り方と、その動的配列をリサイズする方法を解説します。
具体的には↓を見ていきます。

  • 動的配列の作り方

  • 動的配列のリサイズ方法

動的配列の作り方

動的な配列とは、「配列の長さ」が「動的に決まる」という意味の配列です。
普通はC言語の配列はサイズが固定です。要素数を指定したり、有限のデータを使って初期化したりします。

C言語で動的な配列を作るというのは「メモリを動的に確保する」ということになります。
C言語の標準ライブラリstdlib.hには動的なメモリを確保するための関数があります。
それはmalloc()calloc()などです。
この記事ではcalloc()を使った動的メモリの確保を解説します。

まず最初にcalloc()の仕様を見てみましょう。

calloc()の仕様

calloc()は↓のような作りになっています。

void *
calloc(size_t nmemb, size_t size);

calloc()は第1引数にメンバ数(要素数)をとります。
そして第2引数にメンバ1つ分のサイズを取ります。サイズはバイト数です。
たとえば第1引数に1を指定して、第2引数に100を指定した場合、確保されるメモリのバイト数は1 * 100100バイトになります。
第1引数が4で第2引数が100なら4 * 100400バイト分のメモリが確保されます。

calloc()はメモリの確保に成功すると確保したメモリへのポインタを返します。これはvoid *型です。
メモリの確保に失敗した場合はNULLを返します。また、errnoENOMEMをセットします。
確保したメモリはfree()関数で解放する必要があります。解放しない場合はメモリリークになります。

calloc()malloc()と違い、確保したメモリを0クリアします。

calloc()で動的配列を作る

ではcalloc()を使って動的配列を作ってみます。
↓のようにコードを書きます。

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

int
main(void) {
    int nmemb = 4;  // 配列の要素数
    int size = sizeof(int);  // 要素1つのバイト数
    int *arr = calloc(nmemb, size);  // 動的にメモリを確保
    if (!arr) {
        perror("calloc");
        return 1;
    }

    // 確保した配列の中身を見る
    for (int i = 0; i < nmemb; i++) {
        printf("[%d] = %d\n", i, arr[i]);
    }

    free(arr);  // メモリを解放
    return 0;
}

この↑のコードの実行結果は↓のようになります。

[0] = 0
[1] = 0
[2] = 0
[3] = 0

↓の部分を見てみます。

    int nmemb = 4;  // 配列の要素数
    int size = sizeof(int);  // 要素1つのバイト数
    int *arr = calloc(nmemb, size);  // 動的にメモリを確保
    if (!arr) {
        perror("calloc");
        return 1;
    }

変数nmembは確保する動的配列の要素数、変数sizeは要素1つ分のバイト数です。
それらの変数をcalloc()に渡しています。
calloc()の返り値をint *arrに入れていますが、void *型の返り値はこのように異なる型へ自由にキャストすることができます(void *じゃなくてもキャストはできます)。

arrNULLだった場合はメモリの確保に失敗しているので、↑のようにperror()errnoを参照し、return 1;します。

次に↓の部分を見てみます。

    // 確保した配列の中身を見る
    for (int i = 0; i < nmemb; i++) {
        printf("[%d] = %d\n", i, arr[i]);
    }

↑の部分では確保した配列arrの中身を出力しています。
nmembが要素数なのでその要素数分ループを回して、添え字でアクセスします。

最後に↓の部分です。

    free(arr);  // メモリを解放
    return 0;

↑のようにcalloc()で確保したメモリは解放するようにします。

動的配列のリサイズ方法

では本題の動的配列のリサイズ方法です。
C言語では動的配列のリサイズにはrealloc()関数を使います。

まず最初にrealloc()の仕様を見てみましょう。

realloc()の仕様

realloc()は↓のような作りになってます。

void *
realloc(void *ptr, size_t size);

第1引数のptrにはすでにある動的配列のポインタを渡します。
第2引数のsizeには確保するメモリのバイト数を渡します。このバイト数は「再確保する分のバイト数」ではなく、「確保するメモリ全体のバイト数」であることに注意してください。

realloc()は第1引数に渡されたポインタのメモリをsizeのバイト数で再確保し、その結果をvoid *で返します。
返り値のポインタに保存されているメモリのアドレスと、引数ptrに保存されているメモリのアドレスは異なっています。
再確保した場合、引数ptrのポインタをfree()する必要はなく、かわりに返り値のポインタをfree()する必要が出てきます。

realloc()はメモリの確保に成功した場合、先述のように確保したメモリのポインタを返します。
メモリの確保に失敗した場合はNULLを返します。また、errnoENOMEMをセットします。

realloc()で動的配列をリサイズする

ではrealloc()で動的配列をリサイズしてみます。
↓のようにコードを書きます。

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

int
main(void) {
    int nmemb = 4;  // 配列の要素数
    int size = sizeof(int);  // 要素1つのバイト数
    int *arr = calloc(nmemb, size);  // 動的にメモリを確保
    if (!arr) {
        perror("calloc");
        return 1;
    }

    // 確保した配列の値を初期化
    for (int i = 0; i < nmemb; i++) {
        arr[i] = i * 10;
        printf("[%d] = %d\n", i, arr[i]);
    }
    puts("----");

    // 動的配列をリサイズする
    nmemb = 8;  // 要素数を倍にする
    size = nmemb * sizeof(int);  // 確保するバイト数
    int *tmp = realloc(arr, size);  // 配列をリサイズ
    if (!tmp) {
        free(arr);  // 元の配列を解放する
        perror("realloc");
        return 1;
    }
    arr = tmp;  // リサイズしたポインタをarrに保存

    // リサイズした配列の中身を見る
    for (int i = 0; i < nmemb; i++) {
        printf("[%d] = %d\n", i, arr[i]);
    }

    free(arr);  // メモリを解放
    return 0;
}

↑のコードを実行すると↓のような結果になります。

[0] = 0
[1] = 10
[2] = 20
[3] = 30
----
[0] = 0
[1] = 10
[2] = 20
[3] = 30
[4] = 0
[5] = 0
[6] = 0
[7] = 0

valgrindなどのメモリチェックツールによっては「Conditional jump or move depends on uninitialised value」などのエラーが出ますが、これは初期化されていない配列の要素をprintf()で参照しているからです。
今回は簡便さのためにリサイズした要素は未初期化にしてあります。

↑の出力を見る限りは初期化されているように見えるけどな

パソコンの7不思議さ

まず↓の部分を見てみます。

    int nmemb = 4;  // 配列の要素数
    int size = sizeof(int);  // 要素1つ分のバイト数
    int *arr = calloc(nmemb, size);  // 動的にメモリを確保
    if (!arr) {
        perror("calloc");
        return 1;
    }

↑の部分はcalloc()の解説の通りです。
次に↓の部分です。

    // 確保した配列の値を初期化
    for (int i = 0; i < nmemb; i++) {
        arr[i] = i * 10;
        printf("[%d] = %d\n", i, arr[i]);
    }
    puts("----");

今回は結果の配列を見やすくするため、↑のようにarr[i] = i * 10;とやって配列の要素を初期化しています。
あとはcalloc()の解説と同じです。
次に↓の部分です。

    // 動的配列をリサイズする
    nmemb = 8;  // 要素数を倍にする
    size = nmemb * sizeof(int);  // 確保するバイト数
    int *tmp = realloc(arr, size);  // 配列をリサイズ
    if (!tmp) {
        free(arr);  // 元の配列を解放する
        perror("realloc");
        return 1;
    }
    arr = tmp;  // リサイズしたポインタをarrに保存

realloc()で配列をリサイズしています。
まず変数nmembに確保したい配列の要素数を保存しています。今回は8です。
それから変数sizeに配列全体のサイズを指定します。これは要素数と要素1つのサイズから計算します。

今回作っているのはint型の配列なので、要素1つのサイズはsizeof(int)で得ることが出来ます。
そのサイズをnmemb, つまり要素数にかければ配列全体のサイズが求まります。

    int *tmp = realloc(arr, size);
    if (!tmp) {
        free(arr);  // 元の配列を解放する
        perror("realloc");
        return 1;
    }

↑の部分では、realloc()の第1引数にすでにあるarrを渡し、第2引数に計算したメモリのサイズを渡します。
そしてその返り値をint *tmpに入れています。
わざわざint *tmpに保存している理由ですが、仮にarrで返り値を受けてしまうと、返り値がNULLだった場合にarrのメモリを解放できなくなります。そのため↑のように一時的なtmpという変数を作って、そこに保存しています。

tmpNULLだった場合はrealloc()に失敗しているので、元の配列arrを解放し、errnoperror()で参照したあとにreturn 1;としています。

    arr = tmp;  // リサイズしたポインタをarrに保存

メモリの確保に成功した場合は、↑のようにtmpを忘れずにarrに代入しておきます。
こうすることでarrはリサイズされた配列として機能します。

次に↓の部分です。

    // リサイズした配列の中身を見る
    for (int i = 0; i < nmemb; i++) {
        printf("[%d] = %d\n", i, arr[i]);
    }

リサイズした配列の中身を出力しています。
最後に↓の部分です。

    free(arr);  // メモリを解放
    return 0;

arrfree()しています。
途中のtmpは解放する必要はありません。解放した場合はダブルフリーになります。

出力結果をみると、配列がリサイズされ拡張されているのがわかると思います。
このようにrealloc()を使うと動的配列をリサイズすることができます。

おわりに

realloc()の使い方はちょっと注意が必要ですが、慣れればストレスなく使えるようになります。
動的配列を使えるようになると便利なモジュールを作れるようになります。

配列が伸びるよどこまでも

キノピオの鼻みたいだぁ