C言語の構造体の配列の使い方

334, 2021-10-12

目次

C言語の構造体の配列の使い方

C言語の構造体の変数は配列にすることができます

構造体変数はただの変数なので、配列として宣言することが可能です。
ほかの一般的なint型などの配列と同じように使うことが出来ます。

構造体の配列はレコードが複数必要な場合の処理などに使われることがあります。

この記事では構造体の配列について詳しく解説します。
具体的には↓の3つの配列について解説します。

  • 構造体の配列

  • 構造体のVLA(可変長配列)

  • 構造体の動的配列

関連記事
C言語で構造体を初期化する方法

前提とする構造体の内容

今回の記事で使用する構造体は↓のような内容の構造体になります。

struct Animal {
    int eyes;  // 目の数
    double weight;  // 体重
};

構造体Animaleyesweightをメンバーに持つ構造体です。
配列、VLA, 動的配列の解説ではこの構造体を使います。

構造体の配列の使い方

配列とはC言語で扱える一般的なデータ型のことを言います。
構造体の変数も配列として宣言・定義することが可能です。
具体的に見ていきたいと思います。

配列を宣言する方法

構造体の配列を宣言する方法です。
↓のように宣言します。

    struct Animal cats[10];  // 構造体配列の宣言

↑の場合、構造体Animalの配列が10要素分確保されます。
初期化はされていないので各要素のメンバにはでたらめな値が入っています。
初期化したい場合は後述の「定義」の方法を参照してください。

配列を定義する方法

構造体を宣言と同時に定義します。

    // 構造体配列の定義
    struct Animal dogs[] = {
        {2, 32.1},
        {3, 32.2},
        {4, 32.3},
    };

↑の定義の場合、配列dogsは要素3つの配列になります。
第1要素のeyesには2が入り、weightには32.1が入ります。
第2要素のeyesには3が入り、weightには32.2が入ります。
第3要素のeyesには4が入り、weightには32.3が入ります。

↑のように配列を定義する場合は配列の要素数は省略することができます。
また、要素数を省略しないで書くこともできます。

指示初期化子を使う場合は↓のように定義することも可能です。

    // 指示初期化子の指定
    struct Animal birds[] = {
        { .eyes = 2, .weight = 32.1 },
        { .eyes = 3, .weight = 32.2 },
        { .eyes = 4, .weight = 32.3 },
    };

配列を0クリアする方法

構造体の配列を0クリアする場合は↓のように書きます。

    struct Animal pigs[10] = {0};  // 配列を0クリア

↑のようにすると配列pigsの各要素のメンバは0クリアされます。
0クリアとは値0で初期化されるという意味です。
特に理由が無ければ配列は0クリアしておくとバグを減らすことができます。
宣言だけした配列の要素にはデタラメな値が入るからです。

配列へのアクセス方法

構造体の配列にアクセスするには普通の配列と同じように角カッコと添え字を使います。
配列の要素(構造体)のメンバの参照は↓のように行います。

    struct Animal dogs[] = {
        {2, 32.1},
        {3, 32.2},
        {4, 32.3},
    };

    printf("%d\n", dogs[0].eyes);  // 2
    printf("%f\n", dogs[0].weight);  // 32.100000

↑ではdogs[0]とやって配列dogsの0番目の要素にアクセスしています。
その要素のメンバeyesweightを参照してprintf()に出力しています。

構造体の配列にアクセスする場合は、配列の要素は構造体の変数になります。
そのためその変数のメンバにドット演算子でつなげてアクセスすることになります。
あるいは構造体変数を別に用意して配列の要素をコピーしてもいいです。
またあるいは構造体のポインタ変数を用意して配列の要素のアドレスをそこにコピーしてもいいでしょう。

    struct Animal dog = dogs[1];  // コピー
    struct Animal *pdog = &dogs[2];  // アドレスをコピー

構造体の配列の要素に値を代入したい場合は↓のようなコードになります。

    dogs[0].eyes = 20;
    dogs[1].weight = 54.3;

    dogs[0] = dogs[1];
    dogs[0] = (struct Animal) { 8, 65.4 };

eyesweightに直接値を代入するほかには、↑のように配列の要素を構造体変数で上書きする方法もあります。
構造体変数同士の代入は認められているため、↑のようなコードを書くことが可能です。

構造体のVLA(可変長配列)の使い方

C言語の配列にはVLA(可変長配列)という種類の配列があります。
これは、配列の要素数を動的に決定できるというものです。
構造体もVLAを使って宣言することができます。

VLAを宣言する方法

構造体のVLAを宣言するにはたとえば↓のようにコードを書きます。

    int n = 10;
    n *= 2;
    struct Animal gorillas[n];  // VLAで配列を宣言

↑の場合、構造体の配列gorillasの要素数は動的に決定されています。
nが要素数になりますが、この値は計算で求められています。
このようにVLAを使うと動的に配列の要素数を決定することができます。

VLAの注意点

便利なVLAですが、↓のように定義することはできません。

    int n = 10;
    n *= 2;
    struct Animal gorillas[n] = {0};
    // error: variable-sized object may not be initialized

↑の場合、配列gorillasを0クリアしていますが、これはエラーになります。
GCCでは「error: variable-sized object may not be initialized」というエラーが出力されます。

VLAへのアクセス方法

構造体のVLAは構造体の配列と同じようにアクセスすることができます。
前述の「配列へのアクセス方法」を参照してください。

構造体の動的配列の使い方

構造体の配列は動的に確保することができます。
配列の動的確保には動的メモリ確保を使います。
これはstdlib.hで定義されるmalloc()calloc()を使います。
具体的に見ていきたいと思います。

ヘッダーのインクルード

malloc()calloc()などのメモリ確保関数を使うにはstdlib.hをインクルードする必要があります。

#include <stdlib.h>

動的配列を確保する方法

構造体Animalの配列を10要素だけ確保したい場合は↓のようにコードを書きます。

    // Animalの配列を10要素だけ確保
    struct Animal *animals = calloc(10, sizeof(struct Animal));

動的メモリの確保にはcalloc()を使っています。
calloc()は第1引数に確保する要素数、第2引数に要素1つのバイト数を指定します。
今回はstruct Animalのバイト数を10要素だけ確保します。
これはつまりstruct Animal animals[10];と同じことです。

calloc()はメモリ確保に成功すると、確保したメモリのアドレスを返します。
メモリの確保に失敗した場合はNULLポインタを返してきます。
↑のコードではNULLポインタのチェックは省略しています。

メモリを動的に確保しているので、ポインタ変数animalはメモリを解放する必要があります。
これの解放を忘れるとメモリリークと言うバグになります。

動的配列を解放する方法

確保した構造体の動的配列を解放するにはfree()を使います。

    // メモリの解放
    free(animals);

メモリの解放は大事ですが、C言語で動的確保したメモリをすべて解放するのは至難の業です。
かなり高度なプログラミングスキルが必要とされるので、解放漏れを気にする場合はvalgrindなどのメモリチェックツールを使うといいでしょう。

動的配列へのアクセス方法

動的確保した構造体の配列は、普通の構造体の配列と同じように参照・代入することができます。
前述の「配列へのアクセス方法」を参照してください。

構造体の配列はいつ使う?

C言語の構造体の配列はいつ使うのでしょうか?
構造体の配列の使いどころは?

レコードが複数必要な時に使う

C言語の構造体の配列は、たとえばデータがあり、そのデータのレコードが複数必要な時に使われます。
たとえば「顧客」というテーブルがあり、このテーブルに「ID」「名前」「性別」などのフィールドが定義されているとします。
複数の顧客のデータを扱う必要が出た場合に、構造体の配列を使うことができます。
たとえば↓のようにです。

// 顧客を表す構造体
struct Client {
    long id;  // ID
    char name[100];  // 名前
    int sex;  // 性別
};

int main(void) {
    // 顧客の初期データ
    struct Client clients[] = {
        {1, "匿名太郎", 0},
        {2, "匿名花子", 1},
        {3, "匿名ジョリーン", 2},
    };

    return 0;
}

このような顧客のレコードを扱う場合、適しているのは構造体の動的配列です。
動的配列であれば要素数を可変にできるため、自由に伸縮することができます。

関連記事
C言語の動的配列のリサイズ方法

構造体の配列の利点は?

C言語の構造体の配列の利点は何でしょうか?
C言語の構造体の配列のメリットとは?

複数のレコードをまとめて扱える

C言語の構造体の配列のメリットとは、複数の構造体変数をまとめて使える点です。
たとえばファイルにデータを保存しているとします。
ファイルのデータの並びは構造体の配列の並びと一緒です。
その場合、fread()などを使えば簡単に配列としてデータを読み込むことが可能です。

たとえば↓のようにです。

#include <stdio.h>
#include <stdint.h>

struct Animal {
    int32_t eyes;  // 4 byte
    double weight;  // 8 byte ... 4 + 8 = 12 byte はキリがわるい・・・
    char pad[4];  // パディング 4 byte
};

int main(void) {
    struct Animal animals[] = {
        {2, 32.1},
        {3, 32.2},
        {4, 32.3},
    };

    // 構造体の配列をファイルに出力
    FILE *fout = fopen("file.dat", "wb");
    if (!fout) {
        return 1;
    }

    fwrite(animals, sizeof(struct Animal), 3, fout);
    fclose(fout);

    // ファイルから構造体の配列を読み込む
    FILE *fin = fopen("file.dat", "rb");
    if (!fin) {
        return 1;
    }

    fread(animals, sizeof(struct Animal), 3, fin);
    fclose(fin);

    printf("%d %f\n", animals[0].eyes, animals[0].weight);
    printf("%d %f\n", animals[1].eyes, animals[1].weight);
    printf("%d %f\n", animals[2].eyes, animals[2].weight);

    return 0;
}

構造体をファイルに出力するさいに注意したいのは、パディングです。
C言語のコンパイラは賢いので、構造体のアライメント(境界調整)を行って構造体にパディングを挿入します。
そのため構造体のデータをファイルに出力し、構造体にデータを読み込む際はこのパディングを意識する必要があります。
↑の例では構造体Animal4バイト分のパディングを入れています。
こうすることでファイルの入出力が正常に行えるようになります。

構造体の配列の注意点は?

C言語の構造体の配列の注意点はなんでしょうか?
C言語の構造体の配列を使う際に注意したいことは?

配列の範囲外のアクセス

構造体の配列の場合も、注意したいのは配列の範囲外へのアクセスです。
配列の要素数を超えた添え字へのアクセスや、0より下の添え字へのアクセスはセグフォになる可能性があります。
一番怖いのは範囲外のアクセスでプログラムがクラッシュしないケースですが、C言語ではそういうことはよくあります。

おそろC

C言語で構造体の配列をあつかう際は、配列の要素数を意識するようにして扱うようにしましょう。

おわりに

今回はC言語の構造体の配列について詳しく解説しました。

構造体の配列を扱えるようになると、高度なプログラムも作れるようになります。

たとえばゲームのセーブデータを扱うモジュールを作る際に、構造体配列を使ってセーブデータを管理する・・・といった具合です。
ほかにも可能性は無限大です。

構造体の配列で高度なプログラミング

ファイルのデータもまとめて読み込める