プログラミング

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と同じもの
  1. Player copy = heroで、この型の名前・HP・速度をコピーします。copyはheroの別名ではなく、別のデータです。
  2. copy.hp = 60で変わるのはcopyだけなので、heroのHPは100のままです。
  3. 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 = &copy;を追加した場合、最後の表示を予想してください。

答えは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;と書けます。このような不正な状態を作りにくくする話が、次回のクラスへつながります。

前の記事 / 次の記事:クラスの基本

← 記事一覧へ戻る