はじめに
C++の継承では、子クラスのオブジェクトが作られるとき、まず親クラスの部分が作られ、その後に子クラスの部分が作られます。この構築プロセスを制御するのがコンストラクタです。逆に、オブジェクトが破棄されるときは、子クラスの部分が先に片付けられ、次に親クラスの部分が片付けられます。この後片付けを担うのがデストラクタです。
親クラスが引数を持つコンストラクタを用意している場合、子クラス側でそれをどう呼び出すかを明示的に指定する必要があります。
この記事では、子クラスから親クラスのコンストラクタを呼び出す「メンバ初期化子リスト」の構文と、コンストラクタとデストラクタがどのような順序で連鎖して呼び出されるのかを、網羅的に解説します。
コンストラクタ/デストラクタの呼び出し連鎖サンプルコード
このコードは、BaseCharacter
(親クラス)と、それを継承した PlayerCharacter
(子クラス)を定義します。それぞれのクラスに、コンストラクタとデストラクタを用意し、オブジェクトの生成から破棄までの一連の流れを追跡します。
完成コード
#include <iostream>
#include <string>
using namespace std;
// --- 親クラス(基底クラス) ---
class BaseCharacter {
public:
string name;
// 親クラスのデフォルトコンストラクタ
BaseCharacter() : name("名無し") {
cout << " (1) 親クラスの「引数なし」コンストラクタ" << endl;
}
// 親クラスの引数付きコンストラクタ
BaseCharacter(string n) : name(n) {
cout << " (1) 親クラスの「引数付き」コンストラクタ" << endl;
}
// 親クラスのデストラクタ
~BaseCharacter() {
cout << " (4) 親クラスのデストラクタ" << endl;
}
};
// --- 子クラス(派生クラス) ---
class PlayerCharacter : public BaseCharacter {
public:
int level;
// 子クラスのデフォルトコンストラクタ
PlayerCharacter() : level(1) {
cout << " (2) 子クラスの「引数なし」コンストラクタ" << endl;
}
// 子クラスの引数付きコンストラクタ
PlayerCharacter(string n, int lv) : BaseCharacter(n), level(lv) {
cout << " (2) 子クラスの「引数付き」コンストラクタ" << endl;
}
// 子クラスのデストラクタ
~PlayerCharacter() {
cout << " (3) 子クラスのデストラクタ" << endl;
}
};
int main() {
cout << "--- プレイヤーA (引数なし) を生成 ---" << endl;
PlayerCharacter playerA;
cout << "名前: " << playerA.name << ", レベル: " << playerA.level << endl;
cout << "------------------------------------" << endl;
cout << "\n--- プレイヤーB (引数あり) を生成 ---" << endl;
PlayerCharacter playerB("勇者", 10);
cout << "名前: " << playerB.name << ", レベル: " << playerB.level << endl;
cout << "------------------------------------" << endl;
cout << "\n--- main関数が終了します(オブジェクトが破棄されます) ---" << endl;
return 0;
}
実行結果
--- プレイヤーA (引数なし) を生成 ---
(1) 親クラスの「引数なし」コンストラクタ
(2) 子クラスの「引数なし」コンストラクタ
名前: 名無し, レベル: 1
------------------------------------
--- プレイヤーB (引数あり) を生成 ---
(1) 親クラスの「引数付き」コンストラクタ
(2) 子クラスの「引数付き」コンストラクタ
名前: 勇者, レベル: 10
------------------------------------
--- main関数が終了します(オブジェクトが破棄されます) ---
(3) 子クラスのデストラクタ
(4) 親クラスのデストラクタ
(3) 子クラスのデストラクタ
(4) 親クラスのデストラクタ
コードの解説
メンバ初期化子リスト
PlayerCharacter(string n, int lv) : BaseCharacter(n), level(lv)
コンストラクタの ()
の後、{}
の前に置かれた **:
(コロン)**から始まる部分が「メンバ初期化子リスト」です。
BaseCharacter(n)
: 親クラスのコンストラクタを呼び出しています。子クラスが受け取った引数n
を、親クラスのBaseCharacter(string n)
コンストラクタに渡しています。level(lv)
: 自クラスのメンバ変数level
を、引数lv
で初期化しています。{}
の中でlevel = lv;
と書くよりも効率的で推奨される書き方です。
呼び出しの順序
- コンストラクタ: 実行結果が示す通り、常に親クラスが先 (1)、子クラスが後 (2) の順で呼び出されます。メンバ初期化子リストは、どの親クラスコンストラクタを呼び出すかを指定するもので、呼び出し順序を変えるものではありません。
- デストラクタ: オブジェクトが破棄されるときは、コンストラクタとは真逆の順序で、子クラスが先 (3)、親クラスが後 (4) の順で呼び出されます。
まとめ
今回は、C++の継承における、コンストラクタとデストラクタの一連の呼び出しフローを解説しました。
- 子クラスのコンストラクタから親クラスの引数付きコンストラクタを呼び出すには、「メンバ初期化子リスト」を使う。
- 構文:
子コンストラクタ(...) : 親クラス名(引数), 自メンバ(引数) { ... }
- 構文:
- 構築時の呼び出し順は、親 → 子。
- 破棄時の呼び出し順は、子 → 親。
この仕組みを理解することで、親クラスと子クラスで役割分担をしながら、オブジェクトの初期化と後片付けを確実に行う、堅牢なクラス階層を設計することができます。