C++超入門⑧ 構造体とenumでデータを整理しよう
構造体の型・変数・メンバーを区別し、コピーと元データの変更を比較。ドットと矢印、参照での受け渡し、enum classまでキャラクターの例で学びます。
ゲームのキャラクターを考えると、名前、HP、移動速度はセットで持ちたい情報です。変数を別々に並べていると、キャラクターが増えたときに「このHPは誰のもの?」と分かりにくくなります。
そこで、関連する値をまとめる構造体を使います。さらに、待機中・走行中などの状態へ名前を付ける列挙型も組み合わせましょう。
今回は「型の定義」と「実際のデータ」を分けて読めるようになることが目標です。
型や関数はmainより前、変数を作る部分や呼び出しはmain内へ書きます。#includeから始まる全体例を土台にして、説明に合わせて必要な部分を変更してください。
1. structでデータの形を決める
struct Player {
std::string name;
int hp = 100;
double speed = 4.5;
};
Playerという自分の型を定義しています。std::stringを使うため、実際のファイルには#include <string>が必要です。
name、hp、speedのような構造体内の項目をメンバーと呼びます。名前は文字列、HPは整数、速度は小数というように、違う型をまとめられます。
この時点で「Aoiというキャラクター」が作られたわけではありません。記入する項目の形を決めただけです。
Playerは型の名前、hpはその型に含める項目の名前です。まだ「誰のHPなのか」は決まっていません。次に作る変数heroと合わせて、hero.hpと書くと、特定のキャラクターのHPを表せます。
最後は};で終わります。ifやforの波かっこと違い、型の定義にはセミコロンが必要です。
2. Player型の実際の値を作る
#include <iostream>
#include <string>
struct Player {
std::string name;
int hp = 100;
double speed = 4.5;
};
int main() {
Player hero{"Aoi", 100, 4.5};
Player copy = hero;
Player* p = &hero;
copy.hp = 60;
std::cout << hero.hp << ' ' << copy.hp << ' ' << p->hp << '\n';
p->hp = 70;
std::cout << hero.hp << ' ' << copy.hp << ' ' << p->hp << '\n';
return 0;
}
100 60 100
70 60 70
表示は左からhero.hp、copy.hp、p->hpです。初めて出てきた矢印は、ポインタ経由で中身を使う記号。次の表で分けてみましょう。
| 書き方 | 意味 |
|---|---|
Player |
名前・HP・速度をまとめる型 |
hero |
その型で作った1人分のデータ |
hero.hp |
heroの中のHP |
copy.hp |
別にコピーしたキャラクターのHP |
p |
元のheroを指すポインタ |
p->hp |
pが指すキャラクターのHP。今回はhero.hpと同じもの |
Player copy = heroで、この型の名前・HP・速度をコピーします。copyはheroの別名ではなく、別のデータです。copy.hp = 60で変わるのはcopyだけなので、heroのHPは100のままです。p->hp = 70は、pが指すheroのHPへ70を書きます。copyのHPは60のままです。
この型のstd::stringも値としてコピーされます。ただし、後でポインタ型のメンバーを追加した場合、そのポインタの指す先まで自動で複製されるわけではありません。コピーの仕様では、各メンバーの型に応じてコピーすることが定められています。
ドットと矢印をどう選ぶ?
hero.hpの.は、構造体そのものから中の項目を選びます。p->hpの->は、構造体を指すポインタから中の項目を選びます。
p->hpは(*p).hpと同じ意味です。*pでheroをたどり、その.hpを見る、と2段階で読みます。p.hpではポインタを構造体として扱ってしまうので、この例では使えません。メンバーアクセスの仕様もこの関係を定めています。
確認問題:ポインタの相手を変えると?
p->hp = 70;の直前へp = ©を追加した場合、最後の表示を予想してください。
答えは100 70 70です。今のpが指しているのはcopyなので、copyのHPだけが70になります。元のheroは100のまま。ドットか矢印かだけでなく、今どのデータを選んでいるかを追うのがポイントです。
初期化の順番と既定値
{"Aoi", 100, 4.5}は、メンバーを宣言した順に値を入れます。この型なら名前、HP、速度の順です。
Player hero{"Aoi"};とした場合、残りには定義で指定した100と4.5が使われます。既定値を持たない数値メンバーを未初期化のまま使わないよう、今回のように初期値や波かっこを使います。
3. 構造体を関数へ渡す
表示するだけの関数を作ってみましょう。次の関数をPlayerの定義より後、mainより前に置きます。
void showPlayer(const Player& player) {
std::cout << player.name << ": HP " << player.hp << '\n';
}
mainからshowPlayer(hero);と呼びます。
const Player&を3つに分けて読むと、難しさが減ります。
Player:プレイヤー型を受け取る&:コピーではなく元の値への参照で受け取るconst:この参照を通して中身を変更しない
前回の参照を、構造体にも使っています。単に「速くなるおまじない」ではなく、読むだけという役割も表しています。
変更したい関数はconstを外す
void restoreHp(Player& player) {
player.hp = 100;
}
restoreHp(hero);なら、heroのHPが100へ戻ります。
Player playerという値渡しへ変えると、関数内のコピーだけが変わり、heroは変わりません。「表示用はconst参照、変更用は参照」という区別が読み取りやすくなります。
参照のplayerは元の構造体の別名なので、ここではplayer.hpとドットで書きます。Player* playerというポインタで受け取る場合のplayer->hpとは、宣言の型が違います。
4. vectorと組み合わせると、複数人を扱える
std::vector<Player> players = {
{"Aoi", 100, 4.5},
{"Ren", 80, 3.0}
};
for (const Player& player : players) {
showPlayer(player);
}
この部分には#include <vector>も必要です。
vector<Player>は、Player型の値を並べるvectorです。範囲forで1人ずつ参照し、表示関数へ渡しています。
構造体が「1人分の横のまとまり」、vectorが「複数人を並べるまとまり」だと考えると、役割が区別しやすいですね。
5. 状態を数字だけで書くと、意味が見えにくい
int state = 1;と書いても、1が走行なのか停止なのかはコードだけでは分かりません。選択肢へ名前を付けるのがenum class(列挙型)です。
enum class PlayerState {
Idle,
Running,
GameOver
};
Idleは待機、Runningは走行、GameOverは終了という名前にしました。
PlayerState state = PlayerState::Running;
if (state == PlayerState::Running) {
std::cout << "走っています。\n";
}
::は、PlayerStateの中のRunningを選ぶ書き方です。状態の意味が見えるので、「2って何だっけ?」と戻って調べる回数を減らせます。
enum classは整数と区別して扱う
enum classの値は、整数へ勝手に変換されません。state = 1;のように数をそのまま代入することもできません。
ただし、明示的な型変換などで名前を付けていない値を作れるため、外から受け取る番号の検査まで不要になるわけではありません。
C++にはenumだけの形もありますが、このシリーズでは名前の衝突や意図しない変換を避けやすいenum classを使います。Microsoftの列挙型の説明で違いを確認できます。
6. switchで状態ごとに処理を選ぶ
switch (state) {
case PlayerState::Idle:
std::cout << "待機中です。\n";
break;
case PlayerState::Running:
std::cout << "走っています。\n";
break;
case PlayerState::GameOver:
std::cout << "ゲーム終了です。\n";
break;
}
選んだcaseを実行し、breakで抜けます。これは状態を表示しているだけで、enumを定義したら自動でゲームの状態遷移が行われるわけではありません。
例えばHPが0なら終了にしたい場合には、別にif (hero.hp == 0)で判定し、状態を書き換える処理が必要です。
状態を構造体へ入れる
先にPlayerStateを定義したうえで、PlayerへPlayerState state = PlayerState::Idle;を追加できます。
こうすると、あるキャラクターは走行中、別のキャラクターは待機中、というように各キャラクターが自分の状態を持ちます。構造体と列挙型はどちらか一方を選ぶのではなく、一緒に使える道具です。
7. structとclassの関係も少し整理する
構造体だから関数を書けない、ということはありません。C++のstructにもメンバー関数やprivateを置けます。
classとの基本的な違いは、指定を省いたときのメンバーの公開範囲と継承の公開範囲です。structはpublic、classはprivateが既定です。継承の詳細は今回は扱いません。
データを外から自由に読めるまとまりにはstruct、値を変えるルールを型の中で守りたい場合にはclass、という使い分けを次回学びます。アクセス制御の仕様が定義上の根拠です。
8. 練習:商品情報をまとめよう
名前、価格、在庫数を持つProductを作り、1つの商品の3項目を表示してください。
模範解答
#include <iostream>
#include <string>
struct Product {
std::string name;
int price = 0;
int stock = 0;
};
int main() {
Product product{"Keyboard", 4800, 6};
std::cout << "商品名: " << product.name << '\n';
std::cout << "価格: " << product.price << "円\n";
std::cout << "在庫: " << product.stock << "個\n";
return 0;
}
解説
Productで3項目の型と初期値を決め、productで具体的な商品を作っています。初期化は名前、価格、在庫の順です。
在庫を1つ減らす場合の追加例は、if (product.stock > 0) { product.stock--; }です。0より大きいか先に確認するので、今回の操作から負の在庫にはなりません。
ただし、publicなメンバーなら別の場所からproduct.stock = -10;と書けます。このような不正な状態を作りにくくする話が、次回のクラスへつながります。