C/C++ FAQ #2 (構造体内の可変長配列)

構造体に可変長配列を定義したい場合があります。

struct foo {
    size_t size;
    char *array;
};

こんな感じの構造体で、char 配列の長さは動的に決定される、というようなケースです。

この場合以下のように使用します。

int main(int argc, char *argv[]) {
    struct foo f;
    char i;

    f.size = 100;
    f.array = (char *)malloc(f.size);

    for (i = 0; i < f.size; i++) {
        f.array[i] = i;
    }

    free(f.array);
    return 0;
}

普通にやるとこんな感じですね。え、これに何の不満もない?

そうですかー、じゃあサイズが 0 の場合はどうですか?

    f.size = 0;
    f.array = (char *)malloc(f.size); // 必要ないのにメモリ確保?

あるいは

    f.size = 0;
    if (f.size != 0) { // 条件分岐?面倒じゃない?
        f.array = (char *)malloc(f.size);
    } else {
        f.array = NULL;
    }

という感じですか?


これでもいいんですけど、もっといい方法があります。

struct foo {
    size_t size;
    char array[0]; // ここをサイズ 0 の配列にする
};

構造体をこんな感じに変更します。

まずここでひとつメリットが発生しています。

    printf("%d\n", sizeof(struct foo));

変更前の構造体だと、上記は(32bit 環境なら)8 が出力されますが、変更後の構造体では 4 が出力されます。構造体のサイズがスリムになりました*1

使い方としては、以下のようになります。

int main(int argc, char *argv[]) {
    struct foo *f; // ポインタに変更
    char i;

    f = (struct foo *)malloc(sizeof(struct foo) + 100); // 動的配列分だけ多く確保
    f->size = 100;

    for (i = 0; i < f.size; i++) {
        f->array[i] = i; // いきなりアクセスしても大丈夫
    }

    free(f); // 構造体を解放
    return 0;
}

変更後の構造体は構造体と動的配列がメモリ的に地続きになるので、malloc で一気に確保、一気に解放が行えます。え、変更前とあまり変わらない?それがそうでもないんですよ。

変更前の例では、構造体をスタック上に用意していますが、実際のところ、ヒープに確保しなければならないケースがほとんどです。そうすると、

    struct foo *f = (struct foo *)malloc(sizeof(struct foo));
    f->size = 100;
    f->array = (char *)malloc(f->size);

    free(f->array);
    free(f);

こんな感じで malloc と free を 2 回ずつ使用しなければならなくなります。これは面倒です。

C の構造体には、こんな裏ワザが用意されているわけですが、これは比較的新しい規格によるものです。C99 に含まれる仕様です。ただし、C99 をサポートしていないコンパイラでもこの機能をサポートしている(VC++ など)ものもあります。

仕様は以下の通りです。

  • サイズ 0 の配列は構造体の最後のメンバにのみ許されている
  • char array[] と char array[0] のふたつの書き方がある(どちらがより厳密なのでしょう?)

char array[] の方が仕様的に正しいような気がしますが、

    printf("%d\n", f->array);

とした場合に、char array[0] だと 0 が、char array[] だとコンパイルエラーが発生するため、僕は char array[0] を使うようにしています。

さて、気を付けなければならないのは、バッファオーバーランです。

    struct foo f;
    f.array[0] = 'x'; // スタックを破壊!

このようにすると、実際には存在しない構造体のオフセット 4 バイト目に値を書き込みに行き、スタックが破壊されてしまいます。


さて、僕はこの機能を活用して、メモリ管理ライブラリを作る仕事に戻るとするかな。

*1:このサイズの縮小に関しては、どうでもいいと感じるか、有効であると感じるかは、ターゲットが富豪環境かそうでないか、作成しているレイヤーがアプリケーションなのかライブラリなのかによるでしょう。