C++入門|3章を例題で確認する

クラスは戦士の設計図、インスタンスは実際に戦う戦士。ドラゴンボールの世界観で学べば、C++のオブジェクト指向はぐっと身近になる。

3章では、C++の大きな特徴であるオブジェクト指向の入り口として、クラス、インスタンス、メンバ変数、メンバ関数、ファイル分割といった大切な考え方を学びました。

ここまでの内容は、文法だけを追いかけると少し抽象的に見えやすいのですが、ドラゴンボールのような「戦士」「修行」「気」「戦闘力」の世界に置き換えてみると、とてもイメージしやすくなります。

たとえば、
クラスは戦士の設計図
インスタンスは実際に存在する戦士
メンバ変数は戦士が持つ能力値
メンバ関数は戦士が行う行動や計算
として考えることができます。

今回は、ここまでの3章で学んだ内容をしっかり確認するために、戦士の名前・通常時の戦闘力・気の増幅倍率を管理する Warrior クラスを作る例題に取り組みます。

この例題では、ただ数値を入れて表示するだけではなく、

  • クラスを作る
  • ヘッダーファイルに宣言を書く
  • cppファイルにメンバ関数の定義を書く
  • main.cppでインスタンスを生成する
  • メンバ変数に値を入れる
  • メンバ関数を呼び出す

という流れをまとめて確認できます。

ドラゴンボール風の世界でたとえるなら、
戦士のプロフィールを管理し、気を高めたときの戦闘力をスカウターで表示するプログラムを作るイメージです。

まずは今回の例題の全体像をつかもう

今回の例題では、Warriorクラスを使って1人の戦士を表現します。

このクラスに持たせる情報は次の3つです。

種類名前内容
メンバ変数name戦士の名前
メンバ変数basePower通常時の戦闘力
メンバ変数boostRate気を高めたときの増幅倍率

さらに、次の2つのメンバ関数を持たせます。

種類名前内容
メンバ関数calcBoostedPower増幅後の戦闘力を計算して返す
メンバ関数showStatus戦士の状態を表示する

このように、データと動作を1つにまとめるのがオブジェクト指向の基本です。

図:クラスとインスタンスの関係

この図が示していること

この図では、Warriorクラスという設計図から、warriorという実体を作る流れを表しています。

クラスは、まだ「こういう戦士を作れますよ」という設計情報の段階です。
そこから実際にインスタンスを生成してはじめて、名前や戦闘力を持つ「使える戦士オブジェクト」になります。

例題3-1

問題文

以下の仕様を持つ Warrior クラスを作り、戦士名、通常時の戦闘力、気の増幅倍率を入力すると、その戦士の状態と増幅後の戦闘力を表示するプログラムを作成しなさい。
なお、すべてのメンバのアクセス指定子は public とするものとします。

メンバ変数

変数名概要
stringname戦士の名前
doublebasePower通常時の戦闘力
doubleboostRate気の増幅倍率

メンバ関数

戻り値の型関数名引数概要
doublecalcBoostedPowerなし増幅後の戦闘力を返す
voidshowStatusなし戦士の状態を表示する

プログラムの仕様は次のとおりです。

  • プログラムを実行すると「戦士名:」と表示される
  • キーボードから戦士名を入力する
  • 次に「通常時の戦闘力:」と表示される
  • キーボードから通常時の戦闘力を入力する
  • 次に「気の増幅倍率:」と表示される
  • キーボードから倍率を入力する
  • 最後に、戦士の情報と、気を高めた後の戦闘力を表示する

期待される実行結果

戦士名:Leo
通常時の戦闘力:1500
気の増幅倍率:3
=== 戦士データ ===
名前:Leo
通常時の戦闘力:1500
気の増幅倍率:3倍
増幅後の戦闘力:4500

この例題がオブジェクト指向らしい理由

このプログラムは、単なる計算プログラムではなく、1人の戦士という「もの」を中心に設計しているところがオブジェクト指向らしいポイントです。

たとえば、通常の手続き型の考え方だけで書くと、

  • 名前を入れる変数
  • 戦闘力を入れる変数
  • 倍率を入れる変数
  • 増幅後を求める処理

がバラバラになりやすいです。

でもオブジェクト指向では、それらをWarriorという1つのまとまりにできます。

つまり、

  • 戦士が持つ情報 → name, basePower, boostRate
  • 戦士ができること → calcBoostedPower(), showStatus()

というように、戦士に関するものをWarriorクラスに集めているわけです。

ドラゴンボール風に言えば、
スカウターが測る情報も、修行後の力も、全部「その戦士自身」にひもづいているという考え方ですね。

解答例

まずは解答例全体を見ていきましょう。
今回は3章の復習なので、ヘッダーファイル、実装ファイル、main.cppの3つに分けて作ります。

Warriorクラスの宣言

最初に、Warriorクラスの宣言をヘッダーファイルに書きます。

プロジェクト名/ファイル名: Ex3_01/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 の見方

ここでいちばん大事なのは、クラスの中に何を持たせるかを宣言しているということです。

class Warrior {
public:
    ...
};

この部分で、Warriorというクラスを作ることを宣言しています。

その中にある

  • name
  • basePower
  • boostRate

がメンバ変数です。

そして

  • calcBoostedPower()
  • showStatus()

がメンバ関数です。

public の意味

public は、クラスの外から使ってよいメンバを表します。

今回は学習用としてシンプルにするため、すべて public にしています。
そのため、main.cpp から

  • warrior.name
  • warrior.basePower
  • warrior.boostRate
  • warrior.calcBoostedPower()
  • warrior.showStatus()

のようにアクセスできます。

ヘッダーファイルで string を使う

戦士名は文字列なので、name の型は string 系の型を使います。
ここでは次のように書いています。

#include <string>

そしてクラス宣言の中では

std::string name;

としています。

これは、C++の文字列型である string を使うためです。

2重インクルード防止も確認しよう

ヘッダーファイルの先頭と末尾には、次のような記述があります。

#ifndef _WARRIOR_H_
#define _WARRIOR_H_
...
#endif // _WARRIOR_H_

これは、2重インクルードを防止するための仕組みです。

たとえば warrior.h が複数の場所から読み込まれたときに、同じクラス宣言が何度も読み込まれてしまうとエラーの原因になります。
それを防ぐために、この仕組みを書いています。

3章で学んだファイル分割では、とても大事なポイントです。

メンバ関数の定義

次に、warrior.cpp にメンバ関数の中身を書きます。

プロジェクト名/ファイル名: Ex3_01/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;
}

クラス名::関数名 の形に注目

メンバ関数の定義では、次の形が出てきます。

double Warrior::calcBoostedPower()

この Warrior:: という部分がとても重要です。

これは、
Warriorクラスに属する calcBoostedPower 関数ですよ
という意味です。

showStatus も同じです。

void Warrior::showStatus()

この書き方によって、warrior.h で宣言したメンバ関数の中身を、warrior.cpp に書けるようになります。

クラス内ではメンバ名をそのまま使える

calcBoostedPower の中では、次のように書いています。

return basePower * boostRate;

ここでは warrior.basePower のように書いていません。
なぜなら、クラスのメンバ関数の中では、自分のメンバ変数をそのまま名前だけで使えるからです。

これは3章で学んだとても大切なポイントです。

クラスの外、たとえば main.cpp からは

warrior.basePower

のように書きます。

でもクラスの中では、

basePower

だけで大丈夫です。

showStatus の中で name や boostRate をそのまま使っているのも同じ理由です。

main.cpp でインスタンスを使う

最後に、main.cpp を見てみましょう。

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

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

using namespace std;

int main(int argc, char** argv) {
    // Warriorクラスのインスタンスを生成
    Warrior warrior;

    // 戦士の情報を入力
    cout << "戦士名:";
    cin >> warrior.name;

    cout << "通常時の戦闘力:";
    cin >> warrior.basePower;

    cout << "気の増幅倍率:";
    cin >> warrior.boostRate;

    // 戦士の状態を表示
    warrior.showStatus();

    return 0;
}

インスタンス生成の意味

次の1行で、Warriorクラスのインスタンスを生成しています。

Warrior warrior;

これは、Warriorという設計図から、warriorという実体を1つ作るという意味です。

ドラゴンボール風に言えば、
戦士の設計図をもとに、1人の修行戦士を実際に出現させた
というイメージです。

ここから先は、warriorという実体に対して、

  • 名前を入れる
  • 戦闘力を入れる
  • 倍率を入れる
  • 状態を表示する

といった操作をしていきます。

図:インスタンスに値を入れてメンバ関数を呼び出す流れ

この図が示していること

この図では、main.cpp から warrior インスタンスに値を入れ、最後に showStatus を呼び出して情報を表示する流れがわかります。

つまり、

  1. インスタンスを作る
  2. メンバ変数に値を入れる
  3. メンバ関数を呼び出す

という、オブジェクト指向の基本の流れを表しています。

ドット演算子でメンバにアクセスする

main.cpp で値を入力している部分を見てみましょう。

cin >> warrior.name;
cin >> warrior.basePower;
cin >> warrior.boostRate;

ここでは、warrior.メンバ名 という形が使われています。
この . がドット演算子です。

ドット演算子は、インスタンスのメンバにアクセスするための記号です。

今回の例では、

書き方意味
warrior.namewarriorの名前
warrior.basePowerwarriorの通常時の戦闘力
warrior.boostRatewarriorの気の増幅倍率
warrior.showStatus()warriorの状態を表示する

ということになります。

この書き方は、3章の中でもとても重要です。
クラスを作ったら、実際に使うときにはほぼ必ず出てきます。

メンバ関数の中で別のメンバ関数を使える

showStatus の中では、最後にこんな書き方をしています。

cout << "増幅後の戦闘力:" << calcBoostedPower() << endl;

ここで注目したいのは、showStatus の中から calcBoostedPower を呼び出していることです。

つまり、同じクラスのメンバ関数の中から、別のメンバ関数を呼び出せるわけです。

しかもここでも、

calcBoostedPower()

と、そのまま名前だけで使えています。

これも、クラス内だからこそできる書き方です。

このようにすると、処理をうまく分けられます。

  • 計算する役目 → calcBoostedPower
  • 表示する役目 → showStatus

と役割分担ができるので、プログラムが読みやすくなります。

この例題のファイルの依存関係

今回のプログラムは3つのファイルに分かれています。

  • warrior.h
  • warrior.cpp
  • main.cpp

それぞれの役割を整理すると、次のようになります。

ファイル名役割
warrior.hWarriorクラスの宣言を書く
warrior.cppWarriorクラスのメンバ関数の定義を書く
main.cppWarriorクラスを使う処理を書く

依存関係としては、

  • main.cpp は warrior.h を読む
  • warrior.cpp も warrior.h を読む

という形になります。

図:ファイル分割の依存関係

この図が示していること

この図では、warrior.h を中心にして、main.cpp と warrior.cpp がつながっていることがわかります。

warrior.h はクラスの設計図です。
main.cpp はその設計図を使って戦士を生成し、データを入力して使います。
warrior.cpp は設計図に書かれたメンバ関数の実際の動きを実装しています。

処理の流れを順番に追ってみよう

このプログラムがどのように動くのか、順番に整理してみましょう。

1. main関数からスタートする

プログラムは main.cpp の main 関数から始まります。

int main(int argc, char** argv) {

ここが、戦いのスタート地点のようなものです。

2. Warriorクラスのインスタンスを生成する

Warrior warrior;

この1行で warrior という戦士を1人作ります。

この時点では、まだ名前や戦闘力は入力されていません。
ただし、Warriorという型の変数として使える状態になっています。

3. メンバ変数に値を入力する

cin >> warrior.name;
cin >> warrior.basePower;
cin >> warrior.boostRate;

ここで、ユーザーが入力した値を、warrior の各メンバ変数に代入しています。

つまり、

  • warrior.name に名前
  • warrior.basePower に通常戦闘力
  • warrior.boostRate に倍率

が入ります。

4. showStatus を呼び出す

warrior.showStatus();

この1行で、warrior の状態表示が始まります。

showStatus の中では、

  • 名前の表示
  • 通常時の戦闘力の表示
  • 倍率の表示
  • 増幅後の戦闘力の表示

を行います。

5. calcBoostedPower が呼ばれる

showStatus の中では、増幅後の戦闘力を求めるために calcBoostedPower を呼び出しています。

calcBoostedPower()

この関数では、

basePower * boostRate

を計算して、その結果を返します。

今回の実行例なら、

1500 × 3 = 4500

なので、増幅後の戦闘力は 4500 になります。

この例題で確認できる3章のポイント

この例題には、3章で学んだ大切な要素がぎゅっと詰まっています。

クラスの宣言

warrior.h に class Warrior と書くことで、クラスを宣言しています。

メンバ変数

name、basePower、boostRate が、戦士のデータです。

メンバ関数

calcBoostedPower と showStatus が、戦士の動作です。

インスタンス生成

main.cpp の Warrior warrior; で、設計図から実体を作っています。

ドット演算子

warrior.name や warrior.showStatus() のように使います。

ファイル分割

宣言は warrior.h、定義は warrior.cpp、利用は main.cpp に分かれています。

このように見ていくと、今回の例題は3章の復習としてかなりちょうどいい内容になっています。

どこが「ドラゴンボールの世界観らしい」のか

今回の例題がドラゴンボールらしいのは、単に名前だけではありません。
プログラムの考え方そのものが、かなり相性がいいんです。

たとえば今回の Warrior クラスは、ドラゴンボールの世界でいうと、まさに「戦士そのもの」を表しています。

  • name → 戦士名
  • basePower → 通常時の戦闘力
  • boostRate → 気を高めたときの増幅率
  • calcBoostedPower → 気を解放したときの戦闘力計算
  • showStatus → スカウターでの表示

こんなふうに、1人の戦士に関する情報と行動をまとめて表現できるので、オブジェクト指向の考え方がとても自然に見えてきます。

自動車の例でももちろん理解できますが、
ドラゴンボール風にすると、

  • 修行で強くなる
  • 気を高める
  • 戦闘力を測る
  • 戦士ごとに違う能力を持つ

といったイメージがわきやすいので、クラスやオブジェクトの考え方が頭に入りやすくなります。

次に見ると理解が深まるポイント

この例題を理解したら、次は次のような点にも目を向けると、さらにオブジェクト指向の感覚が深まります。

メンバ変数を増やしてみる

たとえば、

  • style(流派)
  • stamina(体力)
  • maxPower(最大戦闘力)

などを足してみると、さらに戦士らしいクラスになります。

メンバ関数を増やしてみる

たとえば、

  • train() → 修行して戦闘力を上げる
  • shout() → セリフを表示する
  • isStrongerThan() → 他の戦士と比較する

といった関数を追加すると、クラスの面白さがもっと見えてきます。

複数のインスタンスを作ってみる

今は warrior を1人分だけ作っていますが、

Warrior warrior1;
Warrior warrior2;

のようにすれば、複数の戦士を作れます。

同じ設計図から、何人でも戦士を生み出せる。
これがクラスとインスタンスのとても大事な考え方です。

例題を通して押さえておきたいこと

今回の例題では、ドラゴンボール風の世界観の中で、ここまでの3章の基本をひと通り確認できました。

特に大切なのは次の3つです。

1つ目は、クラスは設計図だということ

Warriorクラスは、戦士を作るための設計図です。
name や basePower など、「戦士が何を持つか」が書かれています。

2つ目は、インスタンスは実体だということ

Warrior warrior; で作られた warrior は、実際に使える戦士です。
この warrior に対して、値を入れたり関数を呼び出したりできます。

3つ目は、メンバ変数とメンバ関数をまとめるのがオブジェクト指向だということ

戦士が持つ情報と、戦士が行う処理を、バラバラにせず1つのクラスにまとめています。
これがオブジェクト指向の基本です。

今回はかなりドラゴンボールの世界観を意識した例題にしたので、
「クラスって、戦士の設計図みたいなものなんだな」
「インスタンスって、実際に戦う戦士なんだな」
というイメージがつかめたら、とてもいい状態です。