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段階で読みます。
students[i]でi番目の学生を選ぶ.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
表示は左から、元の学生、コピーした学生、ポインタが指す学生の点数です。
Student copy = studentで、名前と点数を別の構造体へコピーします。copy.score = 60はコピー側だけを変更します。元の点数は85のままです。pには元のstudentのアドレスが入っています。addBonus(p)はそのアドレスを関数へ渡すので、元の点数が90になります。- コピーは別のデータなので、
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(©);へ変えると、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件の条件で分ける流れになります。
次の一歩
データに名前とまとまりができると、プログラムが現実のものに近づいてきます。次の最終回では、この構造体へ入力した情報を保存し、平均や最高点を表示します。