プログラミング

C言語超入門⑧ 構造体と列挙型を使おう

構造体の型・変数・メンバを区別し、値のコピーとポインタ経由の変更を同じ例で比較。ドットと矢印、typedef、enumまで順に学びます。

名前は文字列、点数は整数。この2つは型が違いますが、「1人の学生の情報」としてはセットですよね。

別々の配列に保存すると、名前の2番目と点数の2番目をいつも対応させなければなりません。そこで使うのが構造体です。関連する値を1つのまとまりにして、対応をコードでも表します。

今回はstruct、typedef、->、enumを、1つずつ分けて学びます。ポインタの回を読んだ後が目安です。

型や関数の定義はmainより前、値を作って表示する短い例はmainの中へ書きます。途中で示す別の定義へ切り替えるときは、元の定義と重複させないようにしてください。

1. まず型を作り、それから実際の値を作る

#include <stdio.h>

struct Student {
    char name[50];
    int score;
};

int main(void) {
    struct Student student = {"Aoi", 85};
    printf("%s: %d点\n", student.name, student.score);
    return 0;
}
Aoi: 85点

struct Student { ... };は、「学生の情報には何を入れるか」を決める型の定義です。この時点で全員分の学生データができるわけではありません。

struct Student student = ...で、その型の実際の値を1つ作っています。型が記入用紙の形式、変数が記入済みの用紙、と考えると分けやすいでしょう。

nameやscoreのように中へ入れた項目をメンバと呼びます。定義の最後の};を忘れないようにしてください。

コードの部分 役割 この例で表すもの
struct Student 型 名前と点数を組にする形式
student 変数 "Aoi"と85を入れた1人分のデータ
score メンバ名 その形式にある「点数」という項目
student.score 特定の変数のメンバ この学生の点数85

scoreというメンバを書いただけでは、独立した変数scoreが外にできるわけではありません。「誰の点数か」まで指定するのがstudent.scoreです。

2. ドットで中の値へアクセスする

student.score = 90;
printf("%d\n", student.score);

student.scoreは「studentの中のscore」です。値を読むときも書くときも、.でつなぎます。

struct Student another = {"Ren", 72};と作れば別の学生です。student.scoreを変更しても、another.scoreは72のままです。

初期化の順番を確認する

{"Aoi", 85}は、定義に書いたname、scoreの順番で初期化します。位置を取り違えそうなら、C11で使える指示付き初期化もあります。

struct Student student = {
    .name = "Aoi",
    .score = 85
};

項目名を明示するので、何を設定しているか読みやすくなります。どちらも同じ学生情報を作ります。

3. typedefは型の別名を付ける

毎回struct Studentと書くのを短くしたい場合は、typedefを使えます。

typedef struct {
    char name[50];
    int score;
} Student;

この定義にすると、Student student = {"Aoi", 85};と書けます。Studentという新しい別名を付けているのであって、別の処理が動くわけではありません。

以降はこのtypedef版を使います。先ほどのstruct Student版とは別の定義例なので、同じファイルへ両方を重複して貼る必要はありません。

数字のメンバと文字配列のメンバは、更新方法が違う

student.score = 90;は書けますが、student.name = "Ren";は書けません。nameが文字配列だからです。

短い決まった文字列を容量を守って入れ直す例なら、次のように書けます。

snprintf(student.name, sizeof(student.name), "%s", "Ren");

snprintfは、指定した容量を超えないよう文字列を書き込む関数です。長すぎる場合は途中で切れるので、実用では戻り値を使って切り詰めも確認します。ここでは50バイトの配列に短い"Ren"を入れる例です。

4. 構造体の配列で、複数人を扱う

#include <stdio.h>

typedef struct {
    char name[50];
    int score;
} Student;

int main(void) {
    Student students[3] = {
        {"Aoi", 85},
        {"Ren", 72},
        {"Mio", 91}
    };

    for (int i = 0; i < 3; i++) {
        printf("%s: %d点\n", students[i].name, students[i].score);
    }
    return 0;
}

students[i].scoreは、次の2段階で読みます。

  1. students[i]でi番目の学生を選ぶ
  2. .scoreでその学生の点数を選ぶ

配列の添字は0から2までです。人数が3だからといってi <= 3にすると、存在しない4人目を読んでしまいます。

構造体の値は、Student copy = students[0];のようにコピーできます。この場合、内側の文字配列もコピーされます。ただし、構造体のメンバがポインタだった場合は、指す先のデータ全部ではなくポインタ値がコピーされる点に注意します。

5. 関数で変更するときはポインタを使う

「構造体をコピーする」と「構造体の場所を渡す」を比べましょう。下のコードは、これだけを別のファイルに保存して実行できる全体例です。

#include <stdio.h>

typedef struct {
    char name[50];
    int score;
} Student;

void addBonus(Student *student) {
    if (student == NULL) {
        return;
    }
    if (student->score >= 0 && student->score <= 95) {
        student->score += 5;
    }
}

int main(void) {
    Student student = {"Aoi", 85};
    Student copy = student;
    Student *p = &student;

    copy.score = 60;
    printf("%d %d %d\n", student.score, copy.score, p->score);

    addBonus(p);
    printf("%d %d %d\n", student.score, copy.score, p->score);
    return 0;
}
85 60 85
90 60 90

表示は左から、元の学生、コピーした学生、ポインタが指す学生の点数です。

  1. Student copy = studentで、名前と点数を別の構造体へコピーします。
  2. copy.score = 60はコピー側だけを変更します。元の点数は85のままです。
  3. pには元のstudentのアドレスが入っています。addBonus(p)はそのアドレスを関数へ渡すので、元の点数が90になります。
  4. コピーは別のデータなので、copy.scoreは60のままです。

関数内の引数名studentと、mainの変数名studentは別物です。同じ名前だから変更されるのではなく、渡したアドレスが元の構造体を指しているから変更されます。引数名を変えても仕組みは同じです。

この例は0〜100点を想定し、5点を足しても範囲に収まる場合だけ変更します。前の配列の例なら、addBonus(&students[0]);で最初の学生を変更できます。

ドットと矢印は、左側の型で選ぶ

左側が表すもの 書き方
構造体の実体student student.score
構造体を指すポインタp p->score
*pで取り出した構造体 (*p).score

p->scoreは(*p).scoreと同じメンバを表します。まず*pで学生情報をたどり、それから.scoreを選ぶ、と2段階で読めば大丈夫です。かっこなしの*p.scoreは.が先にまとまるため、このポインタには使えません。C11公開委員会草案の6.5.2.3で両演算子を確認できます。

確認問題:コピーの方へボーナスを付けたら?

上のaddBonus(p);をaddBonus(&copy);へ変えると、2行目の表示はどうなるでしょう。

答えは85 65 85です。関数へ渡したのはcopyの場所なので、60が65になります。pは元のstudentを指したままで、p->scoreも85です。

読むだけならconstを付ける

void showStudent(const Student *student) {
    if (student == NULL) {
        return;
    }
    printf("%s: %d点\n", student->name, student->score);
}

const Student *は、そのポインタを通して学生の中身を書き換えない指定です。表示関数の役割が明確になります。constが付いても、有効なアドレスを渡す必要があることは変わりません。

6. enumで「0や1の意味」を名前にする

ゲームの状態を0、1、2だけで表すと、どれが何だったか忘れやすくなります。

typedef enum {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_PAUSED
} GameState;

このような、名前を付けた選択肢を定義する仕組みが列挙型です。明示的に値を付けなければ、この例の定数は0、1、2となります。

GameState state = STATE_RUNNING;

switch (state) {
    case STATE_IDLE:
        printf("待機中\n");
        break;
    case STATE_RUNNING:
        printf("実行中\n");
        break;
    case STATE_PAUSED:
        printf("一時停止中\n");
        break;
    default:
        printf("不明な状態\n");
        break;
}

表示は「実行中」です。GameState型だから必ずこの3つの正しい値だけが入る、と入力検査まで任せることはできません。外部から数値を受け取る場合は、範囲や選択肢を確認します。

構造体は「関係する複数の値をまとめる」、列挙型は「どの状態かに名前を付ける」役割です。構造体のメンバとしてGameState state;を入れることもできます。

定義上の違いはC11公開委員会草案の6.7.2.1(構造体)、6.7.2.2(列挙型)、6.7.8(typedef)で確認できます。

7. 練習:2つの商品を一覧表示しよう

商品名、価格、在庫数を持つProductを作り、2つの商品を表示してください。

模範解答

#include <stdio.h>

typedef struct {
    char name[50];
    int price;
    int stock;
} Product;

int main(void) {
    Product products[2] = {
        {"Keyboard", 4800, 6},
        {"Mouse", 2500, 10}
    };

    for (int i = 0; i < 2; i++) {
        printf("%s: %d円、在庫%d個\n",
               products[i].name,
               products[i].price,
               products[i].stock);
    }
    return 0;
}

解説

Product1つが商品1件、productsが商品2件の配列です。i = 0ではKeyboardの3項目、i = 1ではMouseの3項目を表示します。

価格と在庫を表示する%dは、後ろへ渡す順番と対応しています。結果が入れ替わったら、型だけでなく引数の順番も確認しましょう。

「在庫がある商品だけ表示する」場合の答えは、printfをif (products[i].stock > 0) { ... }で囲むことです。配列から1件を選び、その1件の条件で分ける流れになります。

次の一歩

データに名前とまとまりができると、プログラムが現実のものに近づいてきます。次の最終回では、この構造体へ入力した情報を保存し、平均や最高点を表示します。

前の回 / 次の回:成績管理プログラム

← 記事一覧へ戻る