C++のコンストラクタは、オブジェクト生成と同時に初期化処理を自動で実行できる仕組みです。
初期値の設定だけでなく、リソース確保や不変条件の確立、例外安全まで、設計の質を左右します。
本記事では、基礎から初期化子リスト、コピー/ムーブ、RAII、指定子の活用、落とし穴まで体系的に解説し、動くサンプルコードで具体的に理解を深めます。
C++コンストラクタの基礎:オブジェクト生成時の自動初期化とは
コンストラクタの定義・呼び出しタイミング・オーバーロード
コンストラクタはクラス名と同名で戻り値を持たない特殊なメンバ関数です。
オブジェクト生成の直後に自動的に呼ばれます。
自動記憶域(T x;
)、動的記憶域(new T{...};
)、テンポラリ(T{...}
)のいずれでも呼ばれ、複数のシグネチャを定義するオーバーロードが可能です。
- 呼び出しタイミングは「完全なオブジェクトが使われる前」で、基底クラス→メンバ→本体の順に進みます。
- デフォルト引数とオーバーロードは併用できますが、あいまいな呼び分けは避けます。
- 破棄時には対応するデストラクタが自動的に呼ばれます。
例:最小のコンストラクタと呼び出しの観察
#include <iostream>
#include <memory>
struct Logger {
Logger() { std::cout << "Logger: default constructed\n"; }
explicit Logger(int level) { std::cout << "Logger: level = " << level << "\n"; }
~Logger() { std::cout << "Logger: destructed\n"; }
};
int main() {
Logger a; // 自動変数:デフォルトコンストラクタ
Logger b{2}; // 一時オブジェクトの生成 → 直接初期化
auto p = std::make_unique<Logger>(5); // 動的確保:ヒープ上で生成
// スコープ終了時に a, b のデストラクタ、最後に p の指すオブジェクトのデストラクタが呼ばれる
}
Logger: default constructed
Logger: level = 2
Logger: level = 5
Logger: destructed
Logger: destructed
Logger: destructed
デフォルトコンストラクタと引数付きコンストラクタの使い分け
引数なしで生成したいケース(コンテナでの再配置、配列、ライブラリ要求など)がある場合はデフォルトコンストラクタを用意します。
一方、クラスの不変条件維持に最低限の値が必要な場合は、引数付きのコンストラクタのみを公開し、無効状態を防ぎます。
- 任意のコンストラクタを自前で宣言すると、暗黙のデフォルトコンストラクタは生成されません。必要なら
= default
で明示します。 - 意図しない暗黙変換を避ける単一引数コンストラクタは
explicit
にします(後述)。
struct Config {
int port;
bool ssl;
Config() = default; // 明示的にデフォルト生成可能にする
Config(int p, bool s) : port{p}, ssl{s} {}
};
メンバ初期化子リストと初期化順序のポイント
初期化子リストの書き方とパフォーマンス上の利点
初期化子リストは、メンバや基底クラスを「生成と同時」に初期化する構文です。
代入ではなく初期化なので、不必要なデフォルト構築と再代入を避けられ、パフォーマンスと安全性に優れます。
const
メンバや参照メンバは必ず初期化子リストで初期化しなければなりません。
#include <string>
#include <vector>
struct Widget {
const int id; // const は初期化子リスト必須
std::string name;
std::vector<int> data;
// 良い例:生成時に必要な値で直接初期化
Widget(int id_, std::string n, std::vector<int> d)
: id{id_}, name{std::move(n)}, data{std::move(d)} {
// 本体では不変条件の検証など、ロジックのみを書くのが原則
}
// 悪い例:本体で代入(name と data は一度デフォルト構築されてから代入される)
// Widget(int id_, std::string n, std::vector<int> d) : id{id_} {
// name = std::move(n);
// data = std::move(d);
// }
};
初期化順序(宣言順)と未定義動作の回避
メンバの初期化順序は「記述した初期化子リストの順」ではなく「クラス内の宣言順」で決まります。
依存関係があるメンバは宣言順序を意識し、初期化子で他メンバを参照する場合はそのメンバが「すでに初期化済み」であることを保証しなければなりません。
#include <cassert>
struct Good {
int a; // 先に宣言
int b; // 後に宣言
Good() : b{0}, a{42} { // 並びは逆だが、実際には a → b の順で初期化される
assert(a == 42 && b == 0);
}
};
// 悪い例:宣言順に b が先、a が後。b の初期化子で a を参照すると未初期化の a を読むことになり未定義動作になり得る。
// struct Bad {
// int b; // 先に宣言
// int a; // 後に宣言
// Bad() : b{a}, a{42} {} // a はまだ初期化されていない
// };
依存関係を伴う初期化では、宣言順を揃え、初期化子の中で未初期化メンバを参照しないことが重要です。
主要なコンストラクタの種類と指定子
コピーコンストラクタとムーブコンストラクタの使い方
コピーコンストラクタはconst T&
からの複製、ムーブコンストラクタはT&&
からの資源移動を担います。
格納コストの大きい型ではムーブを用意し、可能ならnoexcept
にすることで標準コンテナの再配置最適化を引き出せます。
#include <iostream>
#include <utility>
struct Buffer {
size_t size{};
int* data{};
Buffer() = default;
explicit Buffer(size_t n) : size{n}, data{ new int[n]{} } {
std::cout << "Buffer: allocate " << n << "\n";
}
// コピー(ディープコピー)
Buffer(const Buffer& other) : size{other.size}, data{ new int[other.size] } {
std::cout << "Buffer: copy\n";
for (size_t i = 0; i < size; ++i) data[i] = other.data[i];
}
// ムーブ(ポインタの所有権移動)
Buffer(Buffer&& other) noexcept : size{other.size}, data{other.data} {
std::cout << "Buffer: move\n";
other.size = 0;
other.data = nullptr;
}
// 代入演算子(コピー/ムーブ)省略のための簡略版
Buffer& operator=(Buffer rhs) noexcept {
std::cout << "Buffer: assign (copy-and-swap)\n";
swap(rhs);
return *this;
}
~Buffer() {
delete[] data;
if (data) std::cout << "Buffer: free\n";
}
void swap(Buffer& other) noexcept {
std::swap(size, other.size);
std::swap(data, other.data);
}
};
int main() {
Buffer a{10};
Buffer b = a; // コピー
Buffer c = std::move(a); // ムーブ
}
Buffer: allocate 10
Buffer: copy
Buffer: move
Buffer: free
Buffer: free
委譲コンストラクタ・継承時の基底クラス初期化
委譲コンストラクタは同一クラス内の別コンストラクタへ初期化を委ね、重複実装を避けます。
継承では、基底クラスは派生クラスのメンバより先に初期化されます。
#include <iostream>
#include <string>
struct Base {
explicit Base(std::string n) : name{std::move(n)} {
std::cout << "Base(" << name << ")\n";
}
std::string name;
};
struct Derived : Base {
int level;
// 委譲コンストラクタ:共通処理を集約
Derived() : Derived("default", 0) {}
// 基底クラスの明示的な初期化とメンバ初期化
Derived(std::string n, int lv) : Base(std::move(n)), level{lv} {
std::cout << "Derived(level=" << level << ")\n";
}
};
int main() {
Derived d1;
Derived d2{"app", 2};
}
Base(default)
Derived(level=0)
Base(app)
Derived(level=2)
explicit, =default, =delete, noexcept, constexpr の活用
これらの指定子は、型の意図・安全性・最適化をコンパイラに明示します。
指定子 | 目的 | 典型的な用途 | 注意点 |
---|---|---|---|
explicit | 暗黙変換の抑制 | 単一引数コンストラクタ | 必要な場合のみ暗黙変換を許可 |
=default | 既定動作の明示 | トリビアルなコピー/デストラクタ | パフォーマンス/ABIの意図伝達 |
=delete | 生成・コピー禁止 | ムーブ専用型、非コピーable | インタフェースレベルでエラーに |
noexcept | 例外非発生の表明 | ムーブ、デストラクタ | コンテナが最適化を適用 |
constexpr | コンパイル時計算 | 定数式な型・設定値 | 実装が制約に適合している必要 |
#include <type_traits>
#include <iostream>
struct Meter {
double v;
// 暗黙変換禁止
explicit Meter(double x) noexcept : v{x} {}
// デフォルト生成を禁止
Meter() = delete;
// コピーはデフォルト、ムーブもデフォルト(noexcept は保証されることが多い)
Meter(const Meter&) = default;
Meter(Meter&&) noexcept = default;
// 定数式コンストラクタ
constexpr Meter(int x, int y) : v{static_cast<double>(x + y)} {}
};
int main() {
Meter a{1.5}; // OK(explicit により {} か明示キャストが必要)
constexpr Meter b{2, 3}; // コンパイル時に評価可能
std::cout << std::boolalpha
<< std::is_nothrow_move_constructible_v<Meter> << "\n";
}
true
RAIIによるリソース管理と例外安全
コンストラクタ/デストラクタでのRAIIパターン
RAII(Resource Acquisition Is Initialization)は、コンストラクタで資源を獲得し、デストラクタで確実に解放する設計です。
スコープを抜けるだけで自動解放されるため、例外が発生してもリークを防げます。
#include <cstdio>
#include <stdexcept>
#include <utility>
#include <iostream>
class File {
std::FILE* fp{nullptr};
public:
// リソース獲得(失敗時は例外)
File(const char* path, const char* mode) : fp{ std::fopen(path, mode) } {
if (!fp) throw std::runtime_error("open failed");
std::cout << "File: opened\n";
}
// 解放は例外を投げない
~File() noexcept {
if (fp) {
std::fclose(fp);
std::cout << "File: closed\n";
}
}
// ムーブのみ許可
File(File&& other) noexcept : fp{other.fp} { other.fp = nullptr; }
File& operator=(File&& other) noexcept {
if (this != &other) {
if (fp) std::fclose(fp);
fp = other.fp;
other.fp = nullptr;
}
return *this;
}
File(const File&) = delete;
File& operator=(const File&) = delete;
void write_line(const char* s) {
std::fputs(s, fp);
std::fputc('\n', fp);
}
};
int main() {
try {
File f{"example.txt", "w"};
f.write_line("hello RAII");
// ここで例外が発生しても、~File() により確実に close される
} catch (const std::exception& e) {
std::cout << "error: " << e.what() << "\n";
}
}
File: opened
File: closed
例外発生時の安全な初期化と強い例外保証
コンストラクタが途中で例外を投げると、そのオブジェクトの生成は失敗し、本体は実行されません。
ただし、すでに構築済みの基底クラスやメンバは逆順に破棄されます。
強い例外保証を満たすためには以下が重要です。
- メンバは初期化子リストで一度だけ構築し、副作用を最小化します。
- ムーブコンストラクタを
noexcept
にしておくと、標準コンテナがコピーではなくムーブを選び、失敗時のロールバック戦略が有効になります。 - リソースはスマートポインタや小さなRAII部品に分解し、途中で例外が出ても自動解放されるようにします。
サンプルコードで学ぶC++コンストラクタの使い方
基本的なコンストラクタ定義とメンバ初期化子リスト
#include <iostream>
struct Point {
int x;
int y;
// デフォルトは原点
Point() : x{0}, y{0} {}
// 値指定のコンストラクタ
Point(int x_, int y_) : x{x_}, y{y_} {}
};
int main() {
Point a; // (0, 0)
Point b{3, 4}; // (3, 4)
std::cout << "(" << a.x << ", " << a.y << ")\n";
std::cout << "(" << b.x << ", " << b.y << ")\n";
}
(0, 0)
(3, 4)
委譲コンストラクタ・コピー/ムーブ対応の実装例
#include <iostream>
#include <vector>
#include <utility>
struct Blob {
std::vector<int> v;
Blob() : Blob(0, 0) { /* 委譲:共通ロジックを一本化 */ }
Blob(int n, int seed) {
v.reserve(n);
for (int i = 0; i < n; ++i) v.push_back(seed + i);
std::cout << "Blob: constructed size=" << v.size() << "\n";
}
// コピー/ムーブはデフォルトで十分(std::vector が適切に定義)
Blob(const Blob&) = default;
Blob(Blob&&) noexcept = default;
Blob& operator=(const Blob&) = default;
Blob& operator=(Blob&&) noexcept = default;
};
int main() {
Blob a(3, 10);
Blob b = a; // コピー
Blob c = std::move(a); // ムーブ
std::cout << "b.size=" << b.v.size() << ", c.size=" << c.v.size() << "\n";
}
Blob: constructed size=3
Blob: constructed size=0
b.size=3, c.size=3
注:Blob() : Blob(0, 0)
で委譲しているため、引数付きのロジックに一本化され、重複実装を避けられます。
explicitやnoexceptを用いた安全な設計例
#include <iostream>
#include <vector>
#include <type_traits>
#include <utility>
struct Distance {
double m;
explicit Distance(double meters) noexcept : m{meters} {}
Distance(const Distance&) = default;
Distance(Distance&&) noexcept = default;
};
int main() {
Distance d1{3.5}; // OK
// Distance d2 = 3.5; // NG(explicit なので暗黙変換不可)
std::vector<Distance> vec;
vec.reserve(2);
vec.push_back(Distance{1.0});
vec.push_back(Distance{2.0});
std::cout << std::boolalpha
<< std::is_nothrow_move_constructible_v<Distance> << "\n";
std::cout << vec[0].m << " " << vec[1].m << "\n";
}
true
1 2
noexcept
なムーブにより、std::vector
は再配置でムーブを選びやすくなり、例外安全と性能の両立が期待できます。
よくある落とし穴とベストプラクティス
初期化と代入の違い・仮想関数呼び出しの注意点
コンストラクタ本体での代入は、すでに構築済みのオブジェクトを上書きします。
初期化子リストで直接構築するほうが効率的です。
コンストラクタ・デストラクタ内では動的型が完成していないため、仮想関数は「現在構築中の段階のクラスの実装」が呼ばれます。
派生クラスのオーバーライドを期待して呼び出す設計は避けます。
constメンバと参照メンバの必須初期化
const
や参照はデフォルト構築後に代入できないため、必ず初期化子リストで初期化します。
未初期化のまま使用するコードは未定義動作の温床です。
ヘッダ設計、ODR、インクルード順の注意事項
ヘッダではヘッダガード(または#pragma once
)を用い、宣言と定義の分離を意識します。
=default
や=delete
の宣言はヘッダに置くのが一般的で、インライン化されます。
非トリビアルな実装は.cpp
に置き、ODR(One Definition Rule)違反を避けます。
依存を減らすためには前方宣言やPIMPLイディオムの活用が有効です。
インクルード順は依存の浅いものからに揃え、循環依存を避けます。
まとめ
コンストラクタは「生成と同時に正しく初期化する」ための中核機能であり、初期化子リスト、宣言順序、コピー/ムーブ、委譲、基底クラス初期化、そしてexplicit
やnoexcept
等の指定子を適切に組み合わせることで、安全かつ高性能な設計が可能になります。
さらにRAIIを取り入れると、例外が絡む複雑な状況でもリークなく堅牢にリソースを扱えます。
日常的なクラス設計で本記事の原則を踏まえ、初期化とリソース管理を意識したコンストラクタを書くことが、品質と保守性の高いC++コードへの近道です。