C++入門|クラスの相互参照

戦士が支援装置を呼び、支援装置が戦士の力を回復する。前方宣言とthisを使えば、2つのクラスを安全につなげられます。

C++で小さなプログラムを作っている間は、1つのクラスだけで処理が完結することもあります。

しかし、プログラムが大きくなると、複数のクラスが協力して1つの処理を行うようになります。

たとえば、戦士を表すクラスと、戦士を支援する装置を表すクラスがあるとします。

戦士は、気力が減ったときに支援装置を起動します。
支援装置は、どの戦士を回復させるのかを知っていなければなりません。

つまり、戦士クラスは支援装置クラスを参照し、支援装置クラスも戦士クラスを参照することになります。

このように、2つのクラスが互いのオブジェクトや機能を利用する関係を、クラスの相互参照と呼びます。

ドラゴンボール風にたとえるなら、戦士が修行用の回復装置へ起動命令を送り、回復装置がその戦士本人へ回復エネルギーを返すような関係です。

今回の記事では、次の3つを中心に学びます。

学習する内容役割
クラスの相互参照2つのクラスが互いを参照する
前方宣言ヘッダーの循環参照を防ぐ
thisポインタ現在処理しているオブジェクト自身を表す
const修飾子メンバ関数がオブジェクトの状態を変更しないことを示す

クラス同士が互いを必要とする場面

今回作成するプログラムには、WarriorクラスとSupportUnitクラスが登場します。

Warriorクラスは、戦士の気力と支援装置を管理します。
SupportUnitクラスは、支援対象となる戦士を記憶し、回復処理を実行します。

Warriorクラスから見ると、SupportUnitクラスの機能が必要です。

SupportUnit* m_support;

SupportUnitクラスから見ると、回復対象となるWarriorクラスの情報が必要です。

Warrior* m_warrior;

この関係を整理すると、次のようになります。

クラス参照する相手目的
WarriorSupportUnit支援装置を起動する
SupportUnitWarrior戦士の気力を回復する

WarriorからSupportUnitへの参照だけでなく、SupportUnitからWarriorへの参照もあります。

これが相互参照です。

図:クラスの相互参照と前方宣言

この図が示していること

WarriorクラスはSupportUnitクラスへのポインタを持ち、SupportUnitクラスはWarriorクラスへのポインタを持っています。

このように、2つのクラスが互いを参照すると、ヘッダーファイル同士を単純にインクルードするだけでは依存関係が複雑になります。

そこで、ヘッダーファイルでは前方宣言を使い、相手のクラスが存在することだけをコンパイラへ伝えます。

ヘッダーファイル同士をincludeする問題

WarriorクラスでSupportUnitクラスを使うからといって、warrior.hでsupport_unit.hをインクルードしたとします。

同時に、SupportUnitクラスでもWarriorクラスを使うため、support_unit.hでwarrior.hをインクルードしたとします。

warrior.h
    ↓ support_unit.hをインクルード
support_unit.h
    ↓ warrior.hをインクルード
warrior.h
    ↓ support_unit.hをインクルード

一見すると、インクルードがいつまでも続くように見えます。

実際には、適切なインクルードガードがあれば、同じヘッダーファイルが無限に展開され続けることは防げます。

ただし、片方のクラス定義を読み込んでいる途中で、まだ完全には定義されていないもう片方のクラスを使おうとするため、クラスの型が分からないというコンパイルエラーが発生することがあります。

つまり、問題の中心は、コンパイラが相手のクラスを完全に理解できる前に、そのクラス名が必要になることです。

前方宣言でクラスの存在を伝える

この問題を解決するために使うのが、前方宣言です。

前方宣言では、classのあとにクラス名を書きます。

class SupportUnit;

この宣言は、SupportUnitというクラスがあとで定義されることをコンパイラへ伝えます。

Warriorクラスのヘッダーでは、SupportUnitクラスの詳しい中身はまだ必要ありません。

次のように、SupportUnitへのポインタを宣言できれば十分です。

SupportUnit* m_support;

同じように、SupportUnitクラスのヘッダーでは、Warriorを前方宣言します。

class Warrior;

これにより、次のポインタを宣言できます。

Warrior* m_warrior;

前方宣言でできること

前方宣言によって相手のクラス名が分かれば、そのクラスへのポインタや参照を宣言できます。

class SupportUnit;

SupportUnit* m_support;

class Warrior;

Warrior* m_warrior;

ただし、前方宣言だけでは、クラスの詳しい構造までは分かりません。

そのため、次のようなポインタや参照の宣言には使えますが、相手のクラスをメンバ変数として直接保持するには完全なクラス定義が必要です。

記述前方宣言だけで使用できるか
SupportUnit* m_supportできる
SupportUnit& supportできる
SupportUnit supportできない
support.activate()完全な定義が必要

ポインタの宣言では、アドレスを保存する領域の大きさが分かればよいため、相手のクラスの中身までは必要ありません。

しかし、オブジェクトそのものをメンバ変数として持つ場合、コンパイラはそのオブジェクトの大きさを知る必要があります。そのため、クラスの完全な定義が必要です。

Warriorクラスのヘッダーファイル

Warriorクラスでは、SupportUnitを前方宣言します。

Warriorクラスは、SupportUnitへのポインタと、戦士の気力を持ちます。

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

#ifndef CHAP7_02_WARRIOR_H
#define CHAP7_02_WARRIOR_H

// SupportUnitクラスが存在することを先に宣言する
class SupportUnit;

// 戦士を表すクラス
class Warrior {
private:
    // 支援装置へのポインタ
    SupportUnit* m_support;

    // 戦士の現在の気力
    int m_energy;

public:
    // コンストラクタ
    Warrior();

    // デストラクタ
    ~Warrior();

    // 現在の状態を表示する
    void showStatus() const;

    // 支援装置を起動する
    void requestSupport();

    // 気力を回復する
    void recoverEnergy(int amount);
};

#endif

warrior.hでは、support_unit.hをインクルードしていません。

代わりに、次の前方宣言を使用しています。

class SupportUnit;

これによって、SupportUnitというクラスが存在することをコンパイラへ伝えています。

そのあと、SupportUnitへのポインタを宣言しています。

SupportUnit* m_support;

SupportUnitクラスのヘッダーファイル

SupportUnitクラスでは、Warriorクラスを前方宣言します。

プロジェクト/ファイル名: Chap7_02/support_unit.h

#ifndef CHAP7_02_SUPPORT_UNIT_H
#define CHAP7_02_SUPPORT_UNIT_H

// Warriorクラスが存在することを先に宣言する
class Warrior;

// 戦士を支援する装置を表すクラス
class SupportUnit {
private:
    // 支援対象となる戦士へのポインタ
    Warrior* m_warrior;

public:
    // 支援対象の戦士を受け取るコンストラクタ
    explicit SupportUnit(Warrior* warrior);

    // 回復支援を実行する
    void activateSupport();
};

#endif

support_unit.hでも、warrior.hをインクルードしていません。

次の前方宣言だけを記述しています。

class Warrior;

これによって、Warriorへのポインタをメンバ変数として宣言できます。

Warrior* m_warrior;

ヘッダーとcppファイルの役割を分ける

ヘッダーファイルでは、相手のクラスへのポインタを宣言するために前方宣言を使いました。

一方、cppファイルでは、相手のクラスのメンバ関数を実際に呼び出します。

そのため、cppファイルでは相手のクラスが定義されたヘッダーをインクルードします。

Warriorクラスのcppファイルでは、SupportUnitのコンストラクタやactivateSupportを使うため、support_unit.hが必要です。

SupportUnitクラスのcppファイルでは、WarriorのrecoverEnergyやshowStatusを使うため、warrior.hが必要です。

ファイル必要な処理
warrior.hSupportUnitを前方宣言する
support_unit.hWarriorを前方宣言する
warrior.cppwarrior.hとsupport_unit.hをインクルードする
support_unit.cppsupport_unit.hとwarrior.hをインクルードする

このように、ヘッダーファイルでは依存関係を最小限にし、cppファイルで完全なクラス定義を読み込むのが基本です。

thisポインタで自分自身を渡す

Warriorクラスのコンストラクタでは、SupportUnitのオブジェクトを生成します。

そのとき、SupportUnitへthisを渡します。

m_support = new SupportUnit(this);

thisは、現在メンバ関数を実行しているオブジェクト自身を指すポインタです。

Warriorクラスのコンストラクタ内でthisを使った場合、thisは現在生成中のWarriorオブジェクトを指します。

たとえば、main関数で次のようにWarriorオブジェクトを作ったとします。

Warrior warrior;

Warriorのコンストラクタが実行されると、その中のthisはwarrior自身を指します。

そのthisをSupportUnitのコンストラクタへ渡すことで、SupportUnitは支援対象となるWarriorを記憶できます。

図:thisで自分自身を支援装置へ渡す

この図が示していること

Warriorオブジェクトは、SupportUnitオブジェクトを生成して所有します。

そのとき、Warriorのコンストラクタからthisを渡します。

SupportUnitは受け取ったWarriorへのポインタをm_warriorへ保存します。

これにより、WarriorはSupportUnitを呼び出すことができ、SupportUnitも支援対象のWarriorへ処理を返せるようになります。

Warriorクラスの処理を実装する

Warriorクラスの処理はwarrior.cppへ記述します。

ここでは、warrior.hとsupport_unit.hの両方をインクルードします。

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

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

using namespace std;

// 戦士を初期化する
Warrior::Warrior()
    : m_support(nullptr), m_energy(50) {

    // thisを渡して、この戦士を支援する装置を生成する
    m_support = new SupportUnit(this);
}

// 動的に生成した支援装置を解放する
Warrior::~Warrior() {
    delete m_support;
}

// 現在の気力を表示する
void Warrior::showStatus() const {
    cout << "戦士の現在気力:" << m_energy << endl;
}

// 支援装置を起動する
void Warrior::requestSupport() {
    m_support->activateSupport();
}

// 指定された量だけ気力を回復する
void Warrior::recoverEnergy(int amount) {
    m_energy += amount;

    // 最大気力を100に制限する
    if (m_energy > 100) {
        m_energy = 100;
    }
}

コンストラクタでは、最初に気力を50へ設定します。

m_energy(50)

続いて、SupportUnitを動的に生成します。

m_support = new SupportUnit(this);

このthisは、現在生成しているWarriorオブジェクト自身です。

SupportUnitのコンストラクタはWarriorへのポインタを受け取るため、thisを渡すことで支援対象を設定できます。

動的に生成したオブジェクトを解放する

Warriorのコンストラクタでは、newを使ってSupportUnitを生成しています。

m_support = new SupportUnit(this);

newで生成したオブジェクトは、不要になったときにdeleteで解放する必要があります。

そのため、Warriorのデストラクタで次の処理を行っています。

Warrior::~Warrior() {
    delete m_support;
}

これにより、Warriorオブジェクトが破棄されるときに、所有しているSupportUnitも解放されます。

今回の中心は相互参照ですが、newを使った場合はdeleteも必要になることを忘れないようにしましょう。

SupportUnitクラスの処理を実装する

SupportUnitクラスのcppファイルでは、support_unit.hとwarrior.hをインクルードします。

SupportUnitは、WarriorのrecoverEnergyとshowStatusを呼び出すため、Warriorクラスの完全な定義が必要です。

プロジェクト/ファイル名: Chap7_02/support_unit.cpp

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

using namespace std;

// 支援対象となる戦士を受け取る
SupportUnit::SupportUnit(Warrior* warrior)
    : m_warrior(warrior) {
}

// 戦士へ回復支援を行う
void SupportUnit::activateSupport() {
    cout << "==== 支援装置を起動 ====" << endl;
    cout << "戦士へ回復エネルギーを送信します。" << endl;

    // 参照している戦士の気力を回復する
    m_warrior->recoverEnergy(40);

    // 回復後の状態を表示する
    m_warrior->showStatus();

    cout << "========================" << endl;
}

SupportUnitのコンストラクタでは、引数で受け取ったWarriorへのポインタをm_warriorへ保存します。

SupportUnit::SupportUnit(Warrior* warrior)
    : m_warrior(warrior) {
}

このwarriorには、Warriorのコンストラクタから渡されたthisが入っています。

そのため、m_warriorは支援対象となるWarriorオブジェクトを指します。

相互参照による呼び出しの流れ

Warriorは、次の処理でSupportUnitのactivateSupportを呼び出します。

m_support->activateSupport();

SupportUnitは、次の処理でWarriorのrecoverEnergyを呼び出します。

m_warrior->recoverEnergy(40);

さらに、回復後の状態を表示するため、WarriorのshowStatusも呼び出します。

m_warrior->showStatus();

処理の流れを整理すると、次のようになります。

順番処理
1Warriorオブジェクトを生成する
2WarriorがSupportUnitを生成する
3WarriorからSupportUnitへthisを渡す
4SupportUnitがWarriorへのポインタを保存する
5WarriorがSupportUnitの支援処理を呼び出す
6SupportUnitがWarriorの気力を回復する
7SupportUnitがWarriorの状態を表示する

WarriorからSupportUnitへ処理が渡り、そのあとSupportUnitからWarriorへ処理が戻っています。

この双方向の呼び出しが、クラスの相互参照です。

main関数から動作を確認する

main関数では、Warriorオブジェクトを1つ作成します。

そのあと、修行前の状態を表示し、支援装置を起動します。

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

#include "warrior.h"

int main(int argc, char** argv) {
    // 戦士オブジェクトを生成する
    Warrior warrior;

    // 支援前の気力を表示する
    warrior.showStatus();

    // 支援装置を起動する
    warrior.requestSupport();

    return 0;
}

実行結果

戦士の現在気力:50
==== 支援装置を起動 ====
戦士へ回復エネルギーを送信します。
戦士の現在気力:90
========================

最初のshowStatusでは、気力50が表示されます。

warrior.showStatus();

次にrequestSupportを呼び出します。

warrior.requestSupport();

WarriorはSupportUnitのactivateSupportを呼び出します。

SupportUnitはWarriorのrecoverEnergyへ40を渡します。

その結果、気力は50から90へ変化します。

thisポインタの正体

thisは、非staticメンバ関数の中で利用できる特別なポインタです。

現在そのメンバ関数を実行しているオブジェクトを指します。

たとえば、次のようにWarriorオブジェクトを2つ作ったとします。

Warrior firstWarrior;
Warrior secondWarrior;

firstWarriorのメンバ関数を呼び出している間、thisはfirstWarriorを指します。

secondWarriorのメンバ関数を呼び出している間、thisはsecondWarriorを指します。

つまり、thisが指す対象は、どのオブジェクトからメンバ関数を呼び出したかによって変わります。

呼び出しthisが指すもの
firstWarrior.showStatus()firstWarrior
secondWarrior.showStatus()secondWarrior

今回のコンストラクタでは、thisをSupportUnitへ渡しています。

m_support = new SupportUnit(this);

これにより、各Warriorが自分専用のSupportUnitへ、自分自身のアドレスを教えられます。

thisは省略されていることがある

メンバ関数の中でメンバ変数へアクセスするとき、通常は次のように書きます。

m_energy = 100;

実際には、現在のオブジェクトが持つm_energyへアクセスしています。

thisを明示すると、次のように書けます。

this->m_energy = 100;

どちらも同じ意味です。

通常はthis->を省略することが多いですが、引数とメンバ変数の名前が同じ場合には、区別するためにthisを使うことがあります。

void Warrior::recoverEnergy(int energy) {
    this->m_energy += energy;
}

この例では、energyが引数で、this->m_energyがメンバ変数です。

ただし、今回のプログラムでは混乱を避けるため、引数名をamountにしています。

void Warrior::recoverEnergy(int amount)

const修飾子を付けたメンバ関数

WarriorクラスのshowStatusには、関数の末尾にconstが付いています。

void showStatus() const;

関数定義にもconstを付けます。

void Warrior::showStatus() const {
    cout << "戦士の現在気力:" << m_energy << endl;
}

このconstは、showStatusの中でWarriorオブジェクトの通常のメンバ変数を変更しないことを表します。

showStatusは、現在の気力を表示するだけの関数です。

そのため、気力を書き換える必要はありません。

constを付けることで、この関数は状態を読み取るだけの関数であることを、コンパイラとプログラムを読む人の両方へ伝えられます。

constメンバ関数では値を変更できない

showStatusの中でm_energyを変更しようとすると、コンパイルエラーになります。

void Warrior::showStatus() const {
    m_energy = 100;
}

関数の末尾にconstがあるため、m_energyの変更は許可されません。

一方、recoverEnergyは気力を変更するため、constを付けていません。

void Warrior::recoverEnergy(int amount) {
    m_energy += amount;
}
メンバ関数オブジェクトの状態const
showStatus読み取るだけ付ける
requestSupport支援装置を操作する今回は付けない
recoverEnergy気力を変更する付けない

メンバ変数を変更しない関数には、できるだけconstを付けると、関数の役割が分かりやすくなります。

図:constメンバ関数と通常のメンバ関数

この図が示していること

showStatusは、戦士の現在の気力を表示するだけなので、constメンバ関数として定義しています。

constメンバ関数では、通常のメンバ変数を書き換えられません。

一方、recoverEnergyは気力を実際に変更する関数なので、constを付けません。

関数がオブジェクトの状態を読むだけなのか、変更するのかを、constによって明確にできます。

constオブジェクトから呼び出せる関数

constを付けて作ったオブジェクトからは、基本的にconstメンバ関数だけを呼び出せます。

たとえば、次のようなconstオブジェクトがあるとします。

const Warrior warrior;

このオブジェクトからshowStatusを呼び出すことはできます。

warrior.showStatus();

showStatusはconstメンバ関数だからです。

一方、recoverEnergyは状態を変更する関数なので、constオブジェクトからは呼び出せません。

warrior.recoverEnergy(40);

この呼び出しは、constオブジェクトを変更しようとしているため、コンパイルエラーになります。

ただし、今回のWarriorクラスはコンストラクタ内でSupportUnitを動的に生成しているため、単純なconstオブジェクトとして使う設計については、所有するオブジェクトとの関係も考える必要があります。

ここでは、constメンバ関数は読み取り専用のオブジェクトからも呼び出せる、という基本を押さえておきましょう。

相互参照で前方宣言を使う理由

今回のクラス構成では、WarriorとSupportUnitが互いを参照しています。

ヘッダーファイル同士を直接読み込ませるのではなく、次のように前方宣言しています。

class SupportUnit;

class Warrior;

そして、相手のクラスをポインタとして保持します。

SupportUnit* m_support;

Warrior* m_warrior;

メンバ関数を実装するcppファイルでは、必要なヘッダーファイルをインクルードします。

#include "warrior.h"
#include "support_unit.h"

この分け方によって、ヘッダーファイル同士の依存関係を整理できます。

前方宣言だけではメンバ関数を呼び出せない

前方宣言は、クラス名の存在だけを伝える仕組みです。

そのため、前方宣言しか見えていない場所では、相手のクラスのメンバ関数を呼び出せません。

たとえば、次の前方宣言があるとします。

class SupportUnit;

これだけで、ポインタは宣言できます。

SupportUnit* m_support;

しかし、SupportUnitの完全な定義がない場所で、次の処理を実装することはできません。

m_support->activateSupport();

コンパイラは、SupportUnitにactivateSupportという関数があるかどうかをまだ知らないからです。

そのため、メンバ関数の実装を置くcppファイルでsupport_unit.hをインクルードします。

#include "support_unit.h"

この違いを整理すると、次のようになります。

行いたいこと前方宣言だけで可能か
ポインタを宣言する可能
参照を宣言する可能
オブジェクト本体を生成する不可
メンバ関数を呼び出す不可
クラスのサイズを調べる不可

クラスの相互参照で身につけたい感覚

クラスの相互参照では、2つのクラスが互いのオブジェクトを利用します。

今回のWarriorとSupportUnitでは、WarriorがSupportUnitへのポインタを持っています。

SupportUnit* m_support;

SupportUnitもWarriorへのポインタを持っています。

Warrior* m_warrior;

ヘッダーファイルでは、前方宣言を使って相手のクラスが存在することだけを伝えます。

class SupportUnit;

class Warrior;

完全なクラス定義が必要になる処理は、cppファイルへ記述します。

また、WarriorのコンストラクタではthisをSupportUnitへ渡しました。

m_support = new SupportUnit(this);

thisは、現在処理しているWarriorオブジェクト自身を指します。

SupportUnitは、そのポインタを保存することで、あとからWarriorのrecoverEnergyやshowStatusを呼び出せるようになります。

そして、状態を表示するだけのshowStatusにはconstを付けました。

void showStatus() const;

これにより、showStatusがWarriorの状態を変更しない関数であることを明確にできます。

ドラゴンボール風に考えると、Warriorは自分専用のSupportUnitを持ち、SupportUnitは支援対象となるWarriorを記憶しています。

戦士が支援装置を起動し、支援装置が戦士へ回復エネルギーを送る。
この双方向のつながりを安全に設計するために、前方宣言、ポインタ、this、constを組み合わせています。

プログラムが大きくなり、複数のクラスが協力して動くようになると、相互参照はよく登場します。

ヘッダーファイルでは前方宣言、cppファイルでは必要なヘッダーのインクルード、自分自身を渡すときはthis、読み取り専用のメンバ関数にはconstという使い分けを身につけておきましょう。