C++入門|複数のインスタンス

同じ設計図から生まれても、戦士ごとに力は違う。C++の複数インスタンスを使えば、同じクラスから個性あるオブジェクトを何体でも作れます。

ここまで、C++のオブジェクト指向として、クラス、オブジェクト、インスタンス、メンバ変数、メンバ関数について学んできました。

前回までの例では、Warriorクラスという戦士の設計図を作り、そこからwarriorというインスタンスを1つ生成して使いました。

ただ、クラスの本当の便利さは、1つのインスタンスだけを作るところでは終わりません。

C++では、1つのクラスから複数のインスタンスを生成できます。

ドラゴンボール風にたとえるなら、Warriorクラスは「戦士を作るための設計図」です。
その設計図から、亀仙流の戦士、神流の戦士、惑星戦士流の戦士など、複数の戦士を作ることができます。

同じWarriorクラスから作られていても、それぞれの戦士は別々の名前、別々の戦闘力、別々の気の増幅倍率を持てます。

つまり、同じ設計図から生まれたとしても、インスタンスが違えば中身もふるまいも変わります。

用語意味ドラゴンボール風のイメージ
クラスオブジェクトの設計図Warriorという戦士設計図
インスタンスクラスから作った実体実際に修行場にいる戦士
複数のインスタンス同じクラスから複数の実体を作ることwarrior1、warrior2のように複数の戦士を作る
メンバ変数インスタンスごとに持つデータ名前、通常戦闘力、気の倍率
メンバ関数インスタンスごとに実行できる動作状態表示、増幅後の戦闘力計算

今回の記事では、1つのWarriorクラスから、warrior1とwarrior2という2人の戦士を作ります。

それぞれに違う名前、通常時の戦闘力、気の増幅倍率を設定し、同じメンバ関数を呼び出したときに、結果が変わることを確認します。

図:1つのクラスから複数の戦士インスタンスを作る

この図が示していること

この図では、1つのWarriorクラスから、warrior1とwarrior2という2つのインスタンスを作る流れを表しています。

同じWarriorクラスから作られていても、warrior1とwarrior2は別々のオブジェクトです。
そのため、name、basePower、boostRateの値をそれぞれ別々に持てます。

C++では、このように同じクラスから複数のインスタンスを作り、それぞれ違う状態で動かすことができます。

1つのインスタンスだけでは分かりにくいクラスのメリット

クラスを初めて学ぶとき、最初は「これなら普通の変数と関数だけでも書けるのでは」と感じることがあります。

たしかに、1人の戦士だけを扱う短いプログラムなら、変数と関数を直接書くだけでも動かせます。

しかし、戦士が2人、3人、さらにチーム単位に増えていくと、クラスの便利さがはっきり見えてきます。

たとえば、戦士を2人扱う場合、通常の変数だけで書こうとすると、次のような変数を用意することになります。

戦士必要な変数の例
1人目name1、basePower1、boostRate1
2人目name2、basePower2、boostRate2

人数が増えるほど、変数名も増えて管理が大変になります。

一方、Warriorクラスを使えば、次のように書けます。

Warrior warrior1, warrior2;

この1行で、Warriorクラスから2人分のインスタンスを生成できます。

それぞれのインスタンスは、自分専用のメンバ変数を持ちます。

インスタンス持っているデータ
warrior1warrior1.name、warrior1.basePower、warrior1.boostRate
warrior2warrior2.name、warrior2.basePower、warrior2.boostRate

このように、クラスを使うと、同じ構造を持つデータをきれいにまとめられます。

ドラゴンボール風に言えば、同じ戦士設計図から複数の戦士を作り、それぞれの戦士に違う修行データを持たせられるということです。

今回作成するプログラムの内容

今回のプログラムでは、Warriorクラスを使って2人の戦士を作ります。

1人目はwarrior1です。
2人目はwarrior2です。

それぞれに、名前、通常時の戦闘力、気の増幅倍率を設定します。

インスタンス名前通常時の戦闘力気の増幅倍率
warrior1Leo15003
warrior2Noa22002

この2人に対して、同じshowStatusメンバ関数を呼び出します。

warrior1.showStatus();
warrior2.showStatus();

同じshowStatusを呼び出しているのに、表示結果はそれぞれ異なります。

なぜなら、warrior1とwarrior2が持っているメンバ変数の値が違うからです。

サンプルプログラムの構成

今回は、Chap3_02プロジェクトとして、次の3つのファイルを作成します。

ファイル役割
warrior.hWarriorクラスの宣言を書く
warrior.cppWarriorクラスのメンバ関数の定義を書く
main.cpp複数のインスタンスを生成して使う

前回までと同じように、クラス宣言、メンバ関数の定義、利用する処理をファイルごとに分けます。

ドラゴンボール風にたとえるなら、次のような役割です。

ファイルドラゴンボール風のイメージ
warrior.h戦士の設計図
warrior.cpp戦士の技や表示処理の実装書
main.cpp修行場で戦士たちを呼び出す司令室

Warriorクラスの宣言

まず、Warriorクラスの宣言を書きます。

プロジェクト/ファイル名: Chap3_02/warrior.h

#ifndef _WARRIOR_H_
#define _WARRIOR_H_

#include <string>

class Warrior {
public:
    // 戦士の名前
    std::string name;

    // 通常時の戦闘力
    double basePower;

    // 気の増幅倍率
    double boostRate;

    // 気を高めた後の戦闘力を計算する
    double calcBoostedPower();

    // 戦士の状態を表示する
    void showStatus();
};

#endif // _WARRIOR_H_

warrior.hの解説

このファイルでは、Warriorクラスの形を宣言しています。

Warriorクラスには、3つのメンバ変数があります。

メンバ変数内容
std::stringname戦士の名前
doublebasePower通常時の戦闘力
doubleboostRate気の増幅倍率

さらに、2つのメンバ関数があります。

戻り値の型メンバ関数内容
doublecalcBoostedPower増幅後の戦闘力を計算して返す
voidshowStatus戦士の状態を表示する

このクラス宣言は、戦士を作るための設計図です。

この設計図から作られたインスタンスは、必ずname、basePower、boostRateを持ち、calcBoostedPowerとshowStatusを使えます。

ヘッダーガードの役割

warrior.hの先頭と末尾には、次の記述があります。

#ifndef _WARRIOR_H_
#define _WARRIOR_H_

// Warriorクラスの宣言

#endif // _WARRIOR_H_

これは、ヘッダーファイルの2重インクルードを防ぐための仕組みです。

同じヘッダーファイルが複数回読み込まれたときに、同じクラスが何度も宣言されるとエラーになることがあります。

ヘッダーガードを書いておくことで、Warriorクラスの宣言が重複して読み込まれるのを防げます。

C++でクラスをヘッダーファイルに書くときは、この形を覚えておくと安心です。

Warriorクラスのメンバ関数を定義する

次に、Warriorクラスのメンバ関数の中身を書きます。

プロジェクト/ファイル名: Chap3_02/warrior.cpp

#include "warrior.h"
#include <iostream>

using namespace std;

// 気を高めた後の戦闘力を計算する
double Warrior::calcBoostedPower() {
    return basePower * boostRate;
}

// 戦士の状態を表示する
void Warrior::showStatus() {
    cout << "=== 戦士データ ===" << endl;
    cout << "名前:" << name << endl;
    cout << "通常時の戦闘力:" << basePower << endl;
    cout << "気の増幅倍率:" << boostRate << "倍" << endl;
    cout << "増幅後の戦闘力:" << calcBoostedPower() << endl;
}

warrior.cppの解説

このファイルでは、Warriorクラスのメンバ関数を定義しています。

注目したいのは、次の書き方です。

double Warrior::calcBoostedPower()

void Warrior::showStatus()

関数名の前にWarrior::を付けています。

これは、calcBoostedPowerとshowStatusがWarriorクラスに属するメンバ関数であることを示しています。

書き方意味
Warrior::calcBoostedPowerWarriorクラスのcalcBoostedPower関数
Warrior::showStatusWarriorクラスのshowStatus関数
::クラスに属していることを示すスコープ解決演算子

ドラゴンボール風にたとえるなら、calcBoostedPowerやshowStatusという技が、Warrior設計図に登録された技であることを示しているようなものです。

calcBoostedPower関数の役割

calcBoostedPower関数では、増幅後の戦闘力を計算しています。

return basePower * boostRate;

basePowerは通常時の戦闘力です。
boostRateは気を高めたときの倍率です。

この2つを掛けることで、増幅後の戦闘力を求めています。

basePowerboostRate増幅後の戦闘力
150034500
220024400

ここで大切なのは、basePowerやboostRateをそのまま書いている点です。

クラスの外からアクセスするときは、warrior1.basePowerのようにインスタンス名が必要です。
しかし、Warriorクラスのメンバ関数の中では、自分自身のメンバ変数をそのまま名前だけで使えます。

showStatus関数の役割

showStatus関数では、戦士の情報を表示しています。

cout << "名前:" << name << endl;
cout << "通常時の戦闘力:" << basePower << endl;
cout << "気の増幅倍率:" << boostRate << "倍" << endl;
cout << "増幅後の戦闘力:" << calcBoostedPower() << endl;

この中では、name、basePower、boostRateをそのまま使っています。

また、同じクラスのメンバ関数であるcalcBoostedPowerも、そのまま呼び出しています。

calcBoostedPower()

つまり、showStatusは、Warriorインスタンスが持っているデータを使って、その戦士の状態を表示します。

warrior1から呼び出せば、warrior1のデータを表示します。
warrior2から呼び出せば、warrior2のデータを表示します。

図:同じshowStatusでもインスタンスごとに結果が変わる

この図が示していること

この図では、warrior1とwarrior2が同じshowStatusメンバ関数を持っていても、表示される結果が異なることを表しています。

warrior1はbasePowerが1500、boostRateが3なので、増幅後の戦闘力は4500です。
warrior2はbasePowerが2200、boostRateが2なので、増幅後の戦闘力は4400です。

同じクラスから作られたインスタンスでも、それぞれが別々のメンバ変数を持っているため、実行結果が変わります。

main.cppで複数のインスタンスを生成する

最後に、main.cppで2つのインスタンスを作って使います。

プロジェクト/ファイル名: Chap3_02/main.cpp

#include <iostream>
#include "warrior.h"

using namespace std;

int main(int argc, char** argv) {
    // 2人の戦士インスタンスを生成する
    Warrior warrior1, warrior2;

    // warrior1の情報を設定する
    warrior1.name = "Leo";
    warrior1.basePower = 1500;
    warrior1.boostRate = 3;

    // warrior2の情報を設定する
    warrior2.name = "Noa";
    warrior2.basePower = 2200;
    warrior2.boostRate = 2;

    // warrior1の状態を表示する
    cout << "warrior1のケース" << endl;
    warrior1.showStatus();

    // warrior2の状態を表示する
    cout << "warrior2のケース" << endl;
    warrior2.showStatus();

    // プログラムが正常に終わったことを示す
    return 0;
}

実行結果は、次のようになります。

warrior1のケース
=== 戦士データ ===
名前:Leo
通常時の戦闘力:1500
気の増幅倍率:3倍
増幅後の戦闘力:4500
warrior2のケース
=== 戦士データ ===
名前:Noa
通常時の戦闘力:2200
気の増幅倍率:2倍
増幅後の戦闘力:4400

複数のインスタンスを生成する書き方

このプログラムで最も大切なのは、次の行です。

Warrior warrior1, warrior2;

これは、Warriorクラスからwarrior1とwarrior2という2つのインスタンスを生成しています。

書き方意味
Warriorクラス名
warrior11つ目のインスタンス
warrior22つ目のインスタンス
Warrior warrior1, warrior2;同じクラスから2つのインスタンスを作る

この1行によって、同じ設計図から2人の戦士が誕生します。

ドラゴンボール風に言えば、Warriorという戦士設計図から、LeoとNoaという2人の修行戦士を呼び出したようなイメージです。

インスタンスごとにメンバ変数は別物

次に、warrior1とwarrior2に値を設定している部分を見てみましょう。

warrior1.name = "Leo";
warrior1.basePower = 1500;
warrior1.boostRate = 3;

warrior2.name = "Noa";
warrior2.basePower = 2200;
warrior2.boostRate = 2;

同じname、basePower、boostRateというメンバ変数を使っていますが、warrior1のものとwarrior2のものは別々です。

書き方対象
warrior1.namewarrior1が持つname
warrior2.namewarrior2が持つname
warrior1.basePowerwarrior1が持つbasePower
warrior2.basePowerwarrior2が持つbasePower

warrior1.basePowerに1500を代入しても、warrior2.basePowerには影響しません。

同じように、warrior2.boostRateに2を代入しても、warrior1.boostRateには影響しません。

これは、warrior1とwarrior2が別々のインスタンスだからです。

同じ名前のメンバでもインスタンスが違えば別物

クラスから作られたインスタンスは、それぞれ自分専用のメンバ変数を持ちます。

そのため、同じbasePowerという名前でも、warrior1のbasePowerとwarrior2のbasePowerは別物です。

インスタンスbasePowerの値boostRateの値calcBoostedPowerの結果
warrior1150034500
warrior2220024400

この仕組みがあるから、同じWarriorクラスから複数の戦士を作り、それぞれ違う状態で扱えます。

ドラゴンボール風に言えば、同じ修行場で生まれた戦士でも、1人目は亀仙流で気の倍率が高く、2人目は神流で基礎戦闘力が高い、というように個性を持てるわけです。

メンバ関数もインスタンスごとに動きが変わる

次に、メンバ関数を呼び出している部分を見てみましょう。

warrior1.showStatus();
warrior2.showStatus();

同じshowStatus関数を呼び出していますが、結果は違います。

なぜなら、showStatusの中で使われるname、basePower、boostRateが、呼び出したインスタンスのものになるからです。

warrior1.showStatus()では、warrior1のデータが使われます。
warrior2.showStatus()では、warrior2のデータが使われます。

呼び出し使われるデータ
warrior1.showStatus()warrior1.name、warrior1.basePower、warrior1.boostRate
warrior2.showStatus()warrior2.name、warrior2.basePower、warrior2.boostRate

つまり、メンバ関数は同じ名前でも、どのインスタンスから呼び出すかによって、使うデータが変わります。

インスタンスの違いを図で整理する

warrior1とwarrior2は、同じWarriorクラスから作られています。

しかし、メモリ上では別々のオブジェクトとして存在します。

Warrior warrior1, warrior2;

+------------+      +------------+
| warrior1   |      | warrior2   |
| name       |      | name       |
| basePower  |      | basePower  |
| boostRate  |      | boostRate  |
| showStatus |      | showStatus |
+------------+      +------------+

同じ構造を持っていますが、中に入る値は別々です。

warrior1
name      = Leo
basePower = 1500
boostRate = 3

warrior2
name      = Noa
basePower = 2200
boostRate = 2

このように、クラスは同じでも、インスタンスごとに状態を持てるのがオブジェクト指向の大きな特徴です。

図:warrior1とwarrior2は別々の状態を持つ

この図が示していること

この図では、warrior1とwarrior2が別々の状態を持っていることを表しています。

同じWarriorクラスから作られていても、warrior1のbasePowerとwarrior2のbasePowerは別々です。

そのため、warrior1.basePowerを変更しても、warrior2.basePowerには影響しません。

同じメンバ関数calcBoostedPowerを使っても、計算に使われる値が違うため、結果も変わります。

このプログラムの処理の流れ

このプログラムの処理を順番に見てみましょう。

手順処理内容
1Warrior warrior1, warrior2;2つのインスタンスを生成する
2warrior1に値を設定Leo、1500、3を設定する
3warrior2に値を設定Noa、2200、2を設定する
4warrior1.showStatus();warrior1の情報を表示する
5warrior2.showStatus();warrior2の情報を表示する

この流れを見ると、warrior1とwarrior2が別々に扱われていることが分かります。

特に大切なのは、同じshowStatusを呼び出していても、それぞれのインスタンスが持つデータを使って表示している点です。

1つのクラスから複数のインスタンスを作れる意味

クラスは設計図です。

設計図は1つでも、そこから複数の実体を作れます。

Warriorクラスも同じです。

Warriorクラスは1つだけですが、そこからwarrior1、warrior2、さらにwarrior3、warrior4と、複数のインスタンスを作れます。

Warrior warrior1, warrior2, warrior3;

このように書けば、3人の戦士を作れます。

それぞれの戦士は、同じメンバ変数とメンバ関数を持っています。
しかし、値はそれぞれ別々に設定できます。

インスタンス役割のイメージ
warrior1亀仙流の戦士
warrior2神流の戦士
warrior3惑星戦士流の戦士

同じWarriorクラスから作られたとしても、名前や戦闘力を変えれば、まったく違う戦士として扱えます。

複数インスタンスが便利な場面

複数のインスタンスは、同じ種類のデータをたくさん扱いたいときに便利です。

たとえば、戦士を複数人扱うプログラムを考えると分かりやすいです。

やりたいこと複数インスタンスが役立つ理由
2人の戦士を比較するwarrior1とwarrior2に別々の戦闘力を持たせられる
チームを作る同じWarriorクラスから複数の戦士を作れる
修行結果を表示する各戦士ごとにshowStatusを呼び出せる
戦闘力ランキングを作る各インスタンスのcalcBoostedPowerの結果を比較できる

クラスを使うと、同じ構造のデータを自然に増やせます。

ドラゴンボール風に言えば、1人の戦士だけでなく、チーム全体をプログラムで扱えるようになるということです。

複数のインスタンスで押さえたいポイント

複数のインスタンスを使うときは、次のポイントを押さえておきましょう。

ポイント内容
1つのクラスから複数のインスタンスを作れるWarrior warrior1, warrior2; のように書ける
インスタンスごとにメンバ変数は別々warrior1.basePowerとwarrior2.basePowerは別物
同じメンバ関数でも結果は変わる各インスタンスのデータを使って処理する
ドット演算子で対象を指定するwarrior1.showStatus()、warrior2.showStatus()
クラスは設計図、インスタンスは実体同じ設計図から複数の戦士を作れる

今回のポイントは、同じクラスから作られたインスタンスでも、それぞれが独立したデータを持つということです。

warrior1はwarrior1のname、basePower、boostRateを持っています。
warrior2はwarrior2のname、basePower、boostRateを持っています。

そのため、warrior1の値を変更しても、warrior2の値は変わりません。

C++のオブジェクト指向では、この仕組みを使うことで、同じ種類のものを複数扱いやすくなります。

戦士クラスを1つ作れば、そこから何人もの戦士を生み出せます。
そして、それぞれの戦士が自分だけの状態を持ち、自分のメンバ関数で動けるようになります。

これが、複数のインスタンスを使う大きなメリットです。