コンストラクタの詳細
初期化リスト、委譲、explicit キーワード。オブジェクト初期化の奥深さ。
コンストラクタの役割
コンストラクタは、オブジェクト作成時に呼ばれ、 メンバ変数を初期化し、必要なリソースを確保します。
効率的で安全なコンストラクタの実装は、クラス設計の基盤です。
初期化リスト(Member Initializer List)
初期化リストを使用することで、 メンバ変数をより効率的に初期化できます。
#include <iostream>
#include <string>
class Person {
private:
std::string name; // クラス型
int age; // 基本型
const int id; // const メンバ
public:
// 初期化リストなし:非効率
Person(const std::string& n, int a, int i) {
name = n; // デフォルト初期化後に代入
age = a;
// id = i; // エラー! const メンバは代入不可
}
};
// 初期化リストあり:効率的かつ安全
class GoodPerson {
private:
std::string name;
int age;
const int id;
public:
GoodPerson(const std::string& n, int a, int i)
: name(n), age(a), id(i) { // 直接初期化
std::cout << "GoodPerson 作成: " << name << std::endl;
}
};
int main() {
GoodPerson p("太郎", 25, 12345);
return 0;
}
GoodPerson 作成: 太郎
初期化リストの順序
初期化リストの実行順序は、 クラス定義での メンバ宣言順です。 リストの順序ではありません。
#include <iostream>
class Example {
private:
int x;
int y;
public:
// 注意: 実行順序は x → y (宣言順)
// リスト順序の y → x ではない
Example(int a, int b) : y(b), x(a) { // 見づらい!
std::cout << "x=" << x << ", y=" << y << std::endl;
}
};
// 良い実装:宣言順と合わせる
class GoodExample {
private:
int x;
int y;
public:
GoodExample(int a, int b) : x(a), y(b) { // 明確
std::cout << "x=" << x << ", y=" << y << std::endl;
}
};
int main() {
Example e(10, 20);
GoodExample g(10, 20);
return 0;
}
x=10, y=20
x=10, y=20
複数のコンストラクタ(オーバーロード)
同じクラスに複数のコンストラクタを定義できます。 異なる初期化方法を提供します。
#include <iostream>
#include <string>
class Account {
private:
std::string owner;
double balance;
int pin;
public:
// コンストラクタ 1: すべてのパラメータ
Account(const std::string& o, double b, int p)
: owner(o), balance(b), pin(p) {
std::cout << "Account 1: " << owner << " 残高=" << balance << std::endl;
}
// コンストラクタ 2: PIN なし(デフォルト 1234)
Account(const std::string& o, double b)
: owner(o), balance(b), pin(1234) {
std::cout << "Account 2: " << owner << " 残高=" << balance << std::endl;
}
// コンストラクタ 3: 名義だけ(残高 0)
explicit Account(const std::string& o)
: owner(o), balance(0), pin(1234) {
std::cout << "Account 3: " << owner << " (新規)" << std::endl;
}
void display() const {
std::cout << " " << owner << ": " << balance << "円" << std::endl;
}
};
int main() {
Account a1("太郎", 100000, 5678); // コンストラクタ 1
Account a2("花子", 50000); // コンストラクタ 2
Account a3("次郎"); // コンストラクタ 3
a1.display();
a2.display();
a3.display();
return 0;
}
Account 1: 太郎 残高=100000
Account 2: 花子 残高=50000
Account 3: 次郎 (新規)
太郎: 100000円
花子: 50000円
次郎: 0円
委譲コンストラクタ (Delegating Constructor - C++11)
委譲コンストラクタにより、 あるコンストラクタが別のコンストラクタを呼び出せます。 重複コードを削減します。
#include <iostream>
#include <vector>
class Vector3D {
private:
double x, y, z;
public:
// メインコンストラクタ
Vector3D(double x_, double y_, double z_)
: x(x_), y(y_), z(z_) {
std::cout << "Vector3D(" << x << ", " << y << ", " << z << ")" << std::endl;
}
// 委譲: ゼロベクトル作成
Vector3D() : Vector3D(0, 0, 0) {
std::cout << " (ゼロベクトル)" << std::endl;
}
// 委譲: XY平面上のベクトル
Vector3D(double x_, double y_) : Vector3D(x_, y_, 0) {
std::cout << " (XY平面)" << std::endl;
}
// 委譲: 単位ベクトル
explicit Vector3D(int axis) : Vector3D(
axis == 0 ? 1 : 0,
axis == 1 ? 1 : 0,
axis == 2 ? 1 : 0) {
std::cout << " (単位ベクトル軸" << axis << ")" << std::endl;
}
};
int main() {
Vector3D v1(3, 4, 5); // メイン
Vector3D v2; // ゼロベクトル
Vector3D v3(1, 2); // XY平面
Vector3D v4(0); // X軸方向単位ベクトル
return 0;
}
Vector3D(3, 4, 5)
Vector3D(0, 0, 0)
(ゼロベクトル)
Vector3D(0, 0, 0)
(XY平面)
Vector3D(1, 0, 0)
(単位ベクトル軸0)
explicit キーワード
explicit キーワードは、 暗黙的な型変換を防ぎます。 意図しないコンストラクタ呼び出しを防止します。
#include <iostream>
#include <string>
class String {
private:
std::string data;
public:
// explicit なし:危険
String(const std::string& s) : data(s) {}
void print() const {
std::cout << data << std::endl;
}
};
// explicit あり:安全
class SafeString {
private:
std::string data;
public:
explicit SafeString(const std::string& s) : data(s) {}
void print() const {
std::cout << data << std::endl;
}
};
void processString(const String& s) {
std::cout << "処理中: ";
s.print();
}
void processSafeString(const SafeString& s) {
std::cout << "安全処理中: ";
s.print();
}
int main() {
// explicit なし:暗黙的変換
processString("Hello"); // String("Hello") と自動変換
// explicit あり:エラー
// processSafeString("Hello"); // コンパイルエラー
// 明示的に変換
processSafeString(SafeString("Hello"));
return 0;
}
処理中: Hello
安全処理中: Hello
デフォルトコンストラクタ
デフォルトコンストラクタは、引数がないコンストラクタです。 配列やコンテナ要素の初期化に使われます。
#include <iostream>
#include <vector>
class Counter {
private:
int value;
public:
// デフォルトコンストラクタ(引数なし)
Counter() : value(0) {
std::cout << "Counter 作成 (value=0)" << std::endl;
}
// 通常コンストラクタ
Counter(int v) : value(v) {
std::cout << "Counter 作成 (value=" << v << ")" << std::endl;
}
int getValue() const { return value; }
};
int main() {
// デフォルトコンストラクタが呼ばれる
Counter c1;
Counter c2[3]; // 配列は各要素でデフォルトコンストラクタ呼び出し
std::vector<Counter> v(5); // 5 個の Counter をデフォルト初期化
// 通常コンストラクタ
Counter c3(100);
return 0;
}
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=100)
= default と = delete (C++11)
コンストラクタを明示的に生成または削除することで、 意図を明確にします。
#include <iostream>
class Point {
private:
int x, y;
public:
// 点 (x, y) を初期化
Point(int x_, int y_) : x(x_), y(y_) {}
// デフォルトコンストラクタを明示的に用意
Point() = default; // = default で生成
void print() const {
std::cout << "(" << x << ", " << y << ")" << std::endl;
}
};
// コピー不可なクラス
class Unique {
private:
int id;
public:
Unique(int i) : id(i) {}
// コピーコンストラクタを削除
Unique(const Unique&) = delete;
// コピー代入演算子を削除
Unique& operator=(const Unique&) = delete;
int getId() const { return id; }
};
int main() {
Point p1; // デフォルトコンストラクタ(= default で生成)
Point p2(3, 4);
p1.print();
p2.print();
Unique u1(1);
// Unique u2 = u1; // エラー! = delete
return 0;
}
(0, 0)
(3, 4)
メンバの in-class初期化 (C++11)
メンバ変数をクラス定義時にデフォルト値で初期化できます。 すべてのコンストラクタで使用されます。
#include <iostream>
#include <string>
class Config {
private:
std::string name = "unnamed"; // in-class 初期化
int port = 8080;
bool debug = false;
public:
// デフォルト値を使用
Config() {
std::cout << "デフォルト: " << name << ":" << port << std::endl;
}
// 一部をオーバーライド
Config(const std::string& n) : name(n) {
// port と debug は in-class 初期値を使用
std::cout << "名前指定: " << name << ":" << port << std::endl;
}
// すべてをオーバーライド
Config(const std::string& n, int p, bool d)
: name(n), port(p), debug(d) {
std::cout << "全指定: " << name << ":" << port << std::endl;
}
};
int main() {
Config c1; // すべてデフォルト
Config c2("MyApp"); // 名前のみ指定
Config c3("API", 3000, true); // すべて指定
return 0;
}
デフォルト: unnamed:8080
名前指定: MyApp:8080
全指定: API:3000
よくある誤り
誤り1: 初期化リストの順序を無視
class Bad {
private:
int x;
int* px;
public:
Bad(int v) : px(new int(0)), x(v) { // 実行順: x → px (宣言順)
*px = x; // px が初期化される前に x が使用される可能性
}
};
誤り2: const メンバをコンストラクタ本体で初期化
class Bad {
private:
const int id;
public:
Bad(int i) {
id = i; // エラー! const メンバ
}
};
// 正しい
class Good {
private:
const int id;
public:
Good(int i) : id(i) {} // 初期化リストで
};
誤り3: explicit を忘れて暗黙的変換を許可
class Bad {
public:
Bad(int size) { } // explicit がない
};
void process(const Bad& b) { }
int main() {
process(100); // Bad(100) に暗黙的変換(意図不明)
}
ポイント
- 初期化リスト:メンバを効率的に初期化(const メンバ対応)
- 複数コンストラクタ:異なる初期化方法を提供
- 委譲コンストラクタ:重複を削減(C++11)
- explicit:暗黙的変換を防止
- デフォルコンス:配列・コンテナで必須
- in-class初期化:便利だが乱用に注意(C++11)
やってみよう
練習1: 複数のコンストラクタを持つクラスを設計
練習2: 初期化リストの順序による問題をテスト
練習3: explicit を使った暗黙的変換防止
チャレンジ: 委譲コンストラクタで重複を削減したクラス
まとめ
- 初期化リストはコンストラクタの必須スキル
- 複数のコンストラクタで柔軟な初期化方法を提供
- explicit で暗黙的変換による誤りを防止
- C++11 の機能(委譲、= default)で効率化