C++においてクラスのインスタンスを生成する際、メンバ変数を適切な状態で使い始めるための「初期化」は、バグの少ない堅牢なプログラムを書く上で極めて重要な工程です。
特に、コンストラクタの初期化子リストを活用した初期化は、効率性や言語仕様上の制約から、現代のC++開発において「必須のテクニック」と言っても過言ではありません。
本記事では、基本的な初期化の方法から、初期化子リストのメリット、そして注意すべき落とし穴まで、網羅的に詳しく解説していきます。
コンストラクタによる初期化の基本
C++のクラスにおいて、オブジェクトが生成される瞬間に自動的に呼び出される特別な関数がコンストラクタです。
このコンストラクタの主な役割は、クラスが持つメンバ変数を初期化し、オブジェクトを使用可能な状態にすることです。
初期化の方法には大きく分けて「コンストラクタ本体での代入」と「初期化子リスト」の2種類が存在します。

コンストラクタ本体で値を設定する方法は、多くのプログラミング言語で見られる一般的な手法ですが、C++においては厳密には「初期化」ではなく「代入」として扱われます。
これに対し、初期化子リストはメンバ変数がメモリ上に確保されるタイミングで値を決定するため、真の意味での初期化を実現します。
まずは、この2つの書き方の違いをコードで確認してみましょう。
コンストラクタ本体での代入
以下のコードは、コンストラクタの波括弧{}の中でメンバ変数に値を代入する例です。
#include <iostream>
#include <string>
class User {
private:
std::string name;
int age;
public:
// コンストラクタ本体での代入(初期化ではない)
User(std::string n, int a) {
name = n; // ここで代入
age = a; // ここで代入
std::cout << "コンストラクタ本体で値を設定しました。" << std::endl;
}
void show() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
int main() {
User user("Alice", 25);
user.show();
return 0;
}
コンストラクタ本体で値を設定しました。
Name: Alice, Age: 25
この方法でも動作に問題はありませんが、「デフォルトコンストラクタで一旦作成された後、代入によって上書きされる」という二度手間が発生しています。
特に、std::stringのようなオブジェクト型の場合、このコストは無視できません。
初期化子リストによる徹底解説
C++で推奨される最も標準的な初期化方法が初期化子リスト(Member Initializer List)です。
これはコンストラクタの引数リストの後にコロン:を付け、メンバ変数名と初期値を記述する形式です。

初期化子リストの基本構文
初期化子リストを使用すると、先ほどのコードは次のように書き換えられます。
#include <iostream>
#include <string>
class User {
private:
std::string name;
int age;
public:
// 初期化子リストによる初期化
// メンバ名(引数) の形式で記述する
User(std::string n, int a) : name(n), age(a) {
// 本体は空で良い
std::cout << "初期化子リストで初期化しました。" << std::endl;
}
void show() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
int main() {
User user("Bob", 30);
user.show();
return 0;
}
初期化子リストで初期化しました。
Name: Bob, Age: 30
この書き方では、nameやageが生成される瞬間に引数の値がセットされます。
無駄なデフォルト初期化が行われないため、パフォーマンスが向上するのが大きな特徴です。
初期化子リストを使わなければならないケース
「単なる推奨」ではなく、初期化子リストを使わなければコンパイルエラーになるケースが3つ存在します。
これらは代入が不可能な型であるため、初期化のタイミングでしか値を決定できないからです。
| ケース | 理由 |
|---|---|
| constメンバ変数 | const(定数)は一度決定すると変更(代入)ができないため。 |
| 参照メンバ変数 | 参照は宣言時に初期化が必要であり、後から参照先を変えられないため。 |
| デフォルトコンストラクタがないクラス型のメンバ | 引数なしで作成できないため、作成時に引数を渡す必要がある。 |
これらのケースをコードで確認してみましょう。
#include <iostream>
class Inner {
public:
// デフォルトコンストラクタがないクラス
Inner(int value) {
std::cout << "Inner created with: " << value << std::endl;
}
};
class Outer {
private:
const int id; // constメンバ
int& ref; // 参照メンバ
Inner inner; // デフォルトコンストラクタなしのクラス
public:
// これらは必ず初期化子リストで初期化しなければならない
Outer(int i, int& r, int innerVal)
: id(i), ref(r), inner(innerVal) {
// ここでは id = i; といった代入は不可
}
void display() {
std::cout << "id: " << id << ", innerVal is set." << std::endl;
}
};
int main() {
int x = 100;
Outer obj(1, x, 500);
obj.display();
return 0;
}
Inner created with: 500
id: 1, innerVal is set.
このように、特定の型が含まれるクラスでは初期化子リストが唯一の手段となります。
メンバ変数の初期化順序に関する罠
初期化子リストを使用する際に、多くのプログラマが陥りやすい「罠」があります。
それは、初期化子リストに書いた順番ではなく、クラス定義(ヘッダ等)で宣言された順番で初期化が行われるというルールです。

順序によるバグの例
以下のコードは、一見正しく見えますが、実行すると意図しない挙動(未定義動作)を引き起こす可能性があります。
#include <iostream>
class Rectangle {
private:
// 宣言の順序: height が先、width が後
int height;
int width;
public:
// リストの順序: width を先に書き、height を後に書いている
// しかし、実際には height -> width の順で実行される!
Rectangle(int h) : width(height * 2), height(h) {
// width の初期化時に height を使っているが、
// この時点では height はまだ初期化されていない(ゴミ値)
}
void print() {
std::cout << "Width: " << width << ", Height: " << height << std::endl;
}
};
int main() {
Rectangle rect(10);
rect.print();
return 0;
}
実行結果(例:環境によって異なります):
Width: -858993460, Height: 10
この例では、widthの計算に未初期化のheightを使用しているため、widthにデタラメな値が入ってしまいました。
これを防ぐための鉄則は、「メンバ変数の宣言順と初期化子リストの記述順を一致させること」です。
多くのコンパイラはこの不一致に対して警告を出してくれますが、常に意識しておく必要があります。
モダンC++における初期化手法の進化
C++11以降、初期化に関する機能が大幅に強化されました。
これらを知っておくことで、コンストラクタの記述をよりシンプルかつ安全に保つことができます。
メンバ変数のデフォルト初期化(C++11以降)
クラスの宣言時に、メンバ変数に直接デフォルト値を指定できるようになりました。
これにより、どのコンストラクタが呼ばれても共通の初期値を保証できます。
class Config {
private:
int port = 8080; // 直接初期値を指定
std::string host = "localhost";
public:
Config() = default; // デフォルト値をそのまま使う
Config(int p) : port(p) {} // portだけ上書き、hostは"localhost"になる
};
この機能を使うと、初期化子リストでの書き忘れを防ぐことができ、「デフォルト値があるなら宣言時に、個別に変えるなら初期化子リストで」という使い分けが可能です。
委譲コンストラクタ(C++11以降)
あるコンストラクタから、同じクラスの別のコンストラクタを呼び出す機能です。
これにより、初期化処理の重複を避けることができます。
#include <iostream>
class Player {
private:
std::string name;
int level;
public:
// メインのコンストラクタ
Player(std::string n, int l) : name(n), level(l) {
std::cout << "メインコンストラクタ呼び出し" << std::endl;
}
// 委譲コンストラクタ(別のコンストラクタに処理を任せる)
Player(std::string n) : Player(n, 1) {
std::cout << "名前のみのコンストラクタ呼び出し" << std::endl;
}
};
int main() {
Player p("Hero");
return 0;
}
メインコンストラクタ呼び出し
名前のみのコンストラクタ呼び出し
初期化子リストの場所でPlayer(...)のように自クラスのコンストラクタを呼び出します。
これにより、初期化ロジックの一貫性を保ちやすくなります。
継承における基底クラスの初期化
クラスを継承している場合、派生クラスのコンストラクタは「まず基底クラスの部分を初期化」しなければなりません。
これも初期化子リストで行います。

#include <iostream>
class Base {
protected:
int value;
public:
Base(int v) : value(v) {
std::cout << "Base initialized with " << value << std::endl;
}
};
class Derived : public Base {
public:
// 基底クラスのコンストラクタを明示的に呼び出す
Derived(int v) : Base(v) {
std::cout << "Derived initialized" << std::endl;
}
};
int main() {
Derived d(100);
return 0;
}
Base initialized with 100
Derived initialized
基底クラスにデフォルトコンストラクタがない場合、派生クラスの初期化子リストで明示的に基底クラスを呼び出さないとコンパイルエラーになります。
オブジェクトの構築は「土台(親)から順に積み上げる」イメージで理解しておくと良いでしょう。
まとめ
C++におけるコンストラクタの初期化は、単に値を代入するだけの作業ではなく、オブジェクトのライフサイクルを制御する重要なプロセスです。
本記事で解説したポイントを振り返ってみましょう。
- 初期化子リストは、コンストラクタ本体での代入よりも効率的で、推奨される方法である。
constメンバや参照メンバ、デフォルトコンストラクタのない型は、初期化子リストでしか初期化できない。- 初期化の実行順序はリストの記述順ではなく、クラス定義での宣言順に従うため、記述順序を合わせることが重要である。
- C++11以降のデフォルトメンバ初期化や委譲コンストラクタを活用することで、より簡潔で安全なコードが書ける。
これらのルールを正しく理解し、適切に使い分けることで、パフォーマンスに優れ、かつ予期せぬ動作の少ない高品質なC++プログラムを構築することができるようになります。
日頃のコーディングから、初期化子リストを優先的に使用する習慣を身につけておきましょう。
