C++ Learning

第30回 コンストラクタ

オブジェクトが生まれる瞬間に自動で呼ばれる特殊な関数がコンストラクタ。「このクラスのインスタンスが有効に使える状態」を作る責任を持ちます。本回は基本の書き方、メンバ初期化子リスト(最重要)、複数コンストラクタの連携、explicit= default / = delete を一気に整理します。

このページで押さえること
✅ 最低限ここだけ覚える
  • コンストラクタ = クラス名と同じ名前の特殊関数
  • 戻り値を書かない
  • メンバは 初期化子リスト : a_(x), b_(y) で初期化
  • 書かなくてもコンパイラが暗黙のデフォルト版を用意する
⭐ 余裕があれば読む
  • 初期化子リスト vs 代入の違い(性能)
  • デリゲートコンストラクタ(他のコンストラクタを呼ぶ)
  • explicit で暗黙変換を防ぐ
  • = default / = delete

1. まず触ってみる ― Point を作る

オブジェクトを作った瞬間に自動で走る「初期化処理」がコンストラクタ。クラス名と同じ名前で、戻り値は書きません。

▶ Point が作られる様子
step 0 / 5
0Point p{3, 4}; ← この行にきた
1メモリを確保 (スタック上に 8 バイト)
2メンバ初期化子リストで x_=3, y_=4
3コンストラクタ本体 { ... } を実行
4オブジェクト p は使える状態に
5(スコープ終了時 → デストラクタ、次章で)
▶ Step ボタンで「作る」の各段階を追いかけましょう。

最小コード

first_ctor.cpp最小例
class Point { int x_, y_; public: Point(int x, int y) : x_(x), y_(y) {} // ← コンストラクタ int x() const { return x_; } int y() const { return y_; } }; Point p{3, 4}; // コンストラクタが呼ばれる std::cout << p.x() << ", " << p.y(); // 3, 4
ここまでで覚えること(3 つ):
  • コンストラクタはクラス名と同じ名前戻り値なし
  • メンバの初期化は : メンバ(値) のリストで
  • Point p{3, 4}; で作ると自動で呼ばれる

よくある素朴な疑問

Q. コンストラクタを書かないとどうなる?
→ コンパイラが引数なしのデフォルトコンストラクタを暗黙に作ってくれます(メンバが全部デフォルト初期化できる場合)。ただし自分でコンストラクタを 1 つでも定義すると、暗黙のデフォルト版は作られなくなる。

Q. 戻り値を書かないのはなぜ?
→ コンストラクタの「戻り値」はそのオブジェクト自身。明示的に書く必要がないのが規約。

Q. new Point(3, 4) は何が違う?
→ 同じくコンストラクタが呼ばれますが、こちらはヒープに確保。生のポインタが返るので、使い終わったら delete が必要(詳しくは STEP 6 のスマートポインタ編で)。

2. コンストラクタの基本

引数で複数のバリエーションを作る

普通の関数と同じく、オーバーロードできます。

オーバーロード複数バリエーション
class Point { int x_, y_; public: Point() : x_(0), y_(0) {} // デフォルト Point(int x, int y) : x_(x), y_(y) {} // 2 引数版 Point(int same) : x_(same), y_(same) {} // 1 引数版 }; Point p1; // (0, 0) Point p2{3, 4}; // (3, 4) Point p3{5}; // (5, 5)

宣言と定義を分ける

ヘッダに宣言、.cpp に定義を分けられるのは通常のメンバ関数と同じ。

point.hヘッダ
class Point { int x_, y_; public: Point(int x, int y); };
point.cpp実装
Point::Point(int x, int y) : x_(x), y_(y) // ← 初期化子リスト { // コンストラクタ本体 }

3. メンバ初期化子リスト ← 最重要

コンストラクタの: x_(x), y_(y) の部分をメンバ初期化子リストと呼びます。メンバ変数を初期化する正しい書き方で、代入とは違います。

代入との違い

代入版(推奨しない)遅い場合あり
Point(int x, int y) { x_ = x; // 代入(= 前のデフォルト値が一瞬存在) y_ = y; } // 手順: // 1. x_ / y_ をデフォルト初期化(0) // 2. その上に x_ = x で上書き代入
初期化子リスト版推奨
Point(int x, int y) : x_(x), y_(y) { // 本体は必要なら追加の処理 } // 手順: // 1. x_ / y_ を最初から x, y で初期化 // (デフォルト値は作られない)

初期化子リストが必須なケース

以下のメンバは代入では初期化できないので、必ず初期化子リストを使います:

必須ケースconst / reference
class Foo { const int MAX; // const: 代入不可 std::string& name_; // 参照: 初期化時以外は差し替え不可 public: Foo(int m, std::string& n) : MAX(m), name_(n) // ← 必須 { // ここでは MAX = m; も name_ = n; も書けない } };

注意: 初期化順序はメンバの宣言順

コンストラクタで書いた順ではなく、クラス宣言での定義順で初期化されます。

罠: class Foo { int a_; int b_; Foo() : b_(10), a_(b_) {} };
上のコードは a_ = b_ と書いてあるので「a_ は 10」と思いきや、初期化順序は宣言順なので a_ が先に初期化される。そのとき b_ はまだ初期化されていない(不定値)ので、a_ゴミ値に。

鉄則:初期化子リストの順序はクラス宣言の順序と一致させる。 コンパイラが警告を出してくれるようにしましょう(-Wreorder)。

4. デリゲートとデフォルト引数

デリゲートコンストラクタ(C++11)

コンストラクタから別のコンストラクタを呼ぶ仕組み。重複するコードをまとめられます。

デリゲートC++11~
class Point { int x_, y_; public: Point(int x, int y) : x_(x), y_(y) {} // 他のコンストラクタに委譲 Point() : Point(0, 0) {} // (0, 0) に委譲 Point(int v) : Point(v, v) {} // 同じ値 2 つに委譲 };
デフォルト引数別案
class Point { int x_, y_; public: Point(int x = 0, int y = 0) : x_(x), y_(y) {} // 1 つのコンストラクタで 0/1/2 引数すべて対応 }; Point a; // (0, 0) Point b{3}; // (3, 0) Point c{3, 4}; // (3, 4)
使い分け: 単純な場合はデフォルト引数で足り、複雑な初期化ロジックを共有したいときはデリゲートが向きます。

メンバのデフォルト初期化子(C++11)

メンバ宣言時に直接 = 値 を書けます。多くのコンストラクタで同じ初期値を使う場合に楽。

default member initializerC++11~
class Config { int timeout_ = 30; // 既定値 bool verbose_ = false; std::string host_ = "localhost"; public: Config() = default; // メンバの既定値を使う Config(std::string h) : host_(std::move(h)) {} // host だけ指定 }; Config c1; // timeout=30, verbose=false, host="localhost" Config c2{"example.com"}; // timeout=30, verbose=false, host="example.com"
ここまでで日常は OK
残りは explicit= default/= delete。実践的ですが、最初は軽く知っていれば十分。

5. explicit ― 暗黙変換を防ぐ

1 引数のコンストラクタは暗黙的な型変換を許します。これが便利な場合と危険な場合があります。

暗黙変換 OK便利だが危険
class Seconds { int s_; public: Seconds(int s) : s_(s) {} }; void sleep(Seconds s); sleep(10); // int → Seconds に暗黙変換、呼べる // 一見便利だが「10 は何秒?10 ミリ秒?」と誤解を生む
explicit で防ぐ安全
class Seconds { int s_; public: explicit Seconds(int s) : s_(s) {} }; sleep(10); // エラー(暗黙変換禁止) sleep(Seconds{10}); // OK: 明示的に書く
モダン流儀: 1 引数のコンストラクタには原則 explicit を付ける。暗黙変換で便利になる場面は少なく、誤呼び出しのリスクのほうが大きい。「意図的に暗黙変換させたい」と決めた場合だけ外します(std::string s = "hello"; のような自然なケース)。

6. = default と = delete

C++11 から、暗黙に生成されるコンストラクタの挙動を明示的に制御できるようになりました。

= default ― 明示的にデフォルト版を要求

= defaultC++11~
class Foo { int x_ = 0; public: Foo() = default; // コンパイラ生成版を明示的に欲しい Foo(int x) : x_(x) {} // カスタム版も追加 }; // 自分で 1 つでもコンストラクタを書くと暗黙のデフォルト版は消える // それを取り戻すのに = default を使う

= delete ― そのコンストラクタを禁止

= deleteC++11~
class Unique { int id_; public: Unique(int id) : id_(id) {} Unique(const Unique&) = delete; // コピー禁止 }; Unique a{1}; Unique b = a; // コンパイルエラー

= delete は「コピーしたくない型」「特定の型からのコンストラクトを禁止したい型」などで使います。std::unique_ptr はコピー不可だが所有権移動(ムーブ)はできる、といった設計で活躍。

広告スペース

確認クイズ

コンストラクタを 4 問で確認。

Q1. コンストラクタについて正しいのは?

戻り値は void
戻り値は必ずそのクラス型
戻り値は書かない(指定してはいけない)
戻り値は自由に指定できる
コンストラクタ(とデストラクタ)は戻り値の型を書きません。「そのオブジェクト自身が実質の戻り値」という扱い。

Q2. 次のメンバは初期化子リストが必須。どれ?

int 型
std::string 型
std::vector<int> 型
const int 型
const メンバや参照メンバは代入できないので、初期化子リストで「初期化」する以外に値を入れる手段がありません。他は代入でも動きますが、初期化子リストの方が効率的(デフォルトコンストラクションを省略できる)。

Q3. Point(int x, int y) : y_(y), x_(x) {} と書いたとき、初期化される順序は?
class Point { int x_; int y_; public: Point(int,int); };

書いた順:y_, x_
クラス宣言順:x_, y_
コンパイラ依存
逆順:y_, x_
初期化順序はクラス宣言での定義順であり、リストの記述順ではありません。リストの順序と宣言順が違うと、参照関係で思わぬバグになります。-Wreorder で警告を出すとよい。

Q4. 1 引数のコンストラクタに explicit を付ける理由は?

コンパイル速度を上げるため
このコンストラクタを必須にするため
暗黙の型変換を禁止するため
const にするため
explicit は「このコンストラクタを経由した暗黙変換を許さない」宣言。f(10) のような間接的な呼び出しを弾いて、f(MyType{10}) のように意図を明示させます。