C++でクラスを作るとき、まず押さえたいのがコンストラクタです。
コンストラクタはオブジェクトを安全に使い始められる最初の状態へ整えるための特別な関数で、クラス設計の品質を大きく左右します。
本記事では、初心者の方がつまずきやすいポイントに絞って、書き方と使い方、そして初期化の基本を丁寧に解説します。
C++のコンストラクタの基本
コンストラクタとは?初期化の役割
コンストラクタはクラスと同じ名前を持ち、戻り値を書かない初期化用の特別なメンバ関数です。
オブジェクト生成時に自動的に呼び出され、メンバ変数に正しい初期値を与えます。
「生成された瞬間から矛盾のない状態」にするのが主な役割です。
例えば、数値を0に、文字列を空文字に、フラグをfalseにするなど、使い始める前の整備を一手に引き受けます。
いつ実行されるか(オブジェクト生成時)
コンストラクタはオブジェクトが生成された直後に一度だけ実行されます。
ローカル変数として生成した場合も、動的に生成した場合も同様です。
#include <iostream>
// 生成の瞬間にログを出すトレーサ
class Tracer {
public:
Tracer() { // クラス名と同じ、戻り値なし
std::cout << "Tracerのコンストラクタが呼ばれました\n";
}
};
int main() {
std::cout << "生成前\n";
Tracer t; // ここでコンストラクタが自動的に呼ばれる
std::cout << "生成後\n";
}
生成前
Tracerのコンストラクタが呼ばれました
生成後
何も書かない場合の挙動(自動生成の有無)
クラスにコンストラクタを1つも宣言しない場合、コンパイラは引数なしの「デフォルトコンストラクタ」を自動生成します。
ただし、組み込み型(intやdoubleなど)のメンバは未初期化のままになる点に注意が必要です。
標準ライブラリ型(std::stringなど)は自分自身が正しく初期化されます。
#include <iostream>
#include <string>
class Sample {
public:
std::string label; // 文字列は空文字で正しく初期化される
int count; // 組み込み型は未初期化(読み出しは未定義動作なので禁止)
};
int main() {
Sample s;
std::cout << "labelは空? -> '" << s.label << "' (空なら何も表示されない)\n";
// std::cout << s.count; // これは未初期化読み出しで危険。絶対にしない。
}
labelは空? -> '' (空なら何も表示されない)
なお、クラスに引数つきコンストラクタを定義すると、自動のデフォルトコンストラクタは生成されません。
引数なしで生成したい場合は自分で書くか、= default
を使います。
class OnlyParam {
public:
OnlyParam(int x) { /*...*/ }
};
// OnlyParam a; // エラー: デフォルトコンストラクタがない
// 対策: OnlyParam() = default; を追加する
表: 自動生成のざっくりルール
状況 | デフォルトコンストラクタ |
---|---|
コンストラクタを1つも宣言していない | 自動生成される |
引数つきコンストラクタを宣言した | 自動生成されない |
明示的に必要な場合 | ClassName() = default; と書く |
コンストラクタの書き方とルール
クラス名と同名/戻り値なし
コンストラクタはクラス名と同名で、戻り値を書く必要も権利もありません。
voidを付けるとコンストラクタではなく通常関数になってしまいます。
#include <iostream>
#include <string>
class Person {
public:
// 正しいコンストラクタ
Person() {
std::cout << "Personの正しいコンストラクタ\n";
}
// 間違いの例: これはコンストラクタではなく"戻り値voidの関数"
// void Person() { } // 定義してもコンストラクタ扱いにならないので注意
};
デフォルトコンストラクタ(引数なし)
引数なしのコンストラクタを自分で定義すると、メンバに任意の初期値を与えられます。
#include <iostream>
#include <string>
class Person {
private:
std::string name;
int age;
public:
// デフォルトコンストラクタ: 使いやすい初期値をセット
Person() {
name = "unknown"; // 簡単な代入で初期状態を決める
age = 0;
}
void print() const {
std::cout << name << " (" << age << ")\n";
}
};
int main() {
Person a; // 引数なしで生成
a.print();
}
unknown (0)
引数つきコンストラクタ(初期値を受け取る)
必要な初期値を引数で受け取って柔軟に初期化できます。
#include <iostream>
#include <string>
class Person {
private:
std::string name;
int age;
public:
Person(const std::string& n, int a) {
// コンストラクタの本体でメンバに代入する
name = n;
age = a;
}
void print() const {
std::cout << name << " (" << age << ")\n";
}
};
int main() {
Person taro("Taro", 20); // 引数つきで生成
taro.print();
}
Taro (20)
コンストラクタの使い方
オブジェクト生成時の呼び出し方(引数なし)
引数なしの生成はType obj;
かType obj{};
と書きます。
Type obj(); は関数宣言になってしまうので避けてください。
#include <iostream>
class X {
public:
X() { std::cout << "X()\n"; }
};
int main() {
X a; // OK: デフォルトコンストラクタ呼び出し
X b{}; // OK: ブレースによる呼び出し(確実に変数定義)
// X c(); // NG: これは"cという関数の宣言"と解釈される
}
X()
X()
オブジェクト生成時の呼び出し方(引数あり)
引数がある場合は()
または{}
で渡します。
ブレースは意図しない型変換を抑える効果があるため、初心者のうちはなるべく{}を使うと安全です。
#include <iostream>
#include <string>
class Point {
public:
int x, y;
Point(int x_, int y_) { x = x_; y = y_; }
void print() const { std::cout << "(" << x << "," << y << ")\n"; }
};
int main() {
Point p1(3, 4); // ()で引数を渡す
Point p2{5, 6}; // {}でもOK
p1.print();
p2.print();
}
(3,4)
(5,6)
クラス内でのメンバ初期化の書き方(簡単な代入)
本記事ではまずコンストラクタ本体での代入による初期化を使います。
以下のように、受け取った引数や既定値をメンバへ代入します。
#include <iostream>
#include <string>
class Config {
private:
std::string mode;
int retry;
public:
// 既定値で初期化
Config() {
mode = "release";
retry = 3;
}
// 引数で初期値を変える
Config(const std::string& m, int r) {
mode = m;
retry = r;
}
void print() const {
std::cout << "mode=" << mode << ", retry=" << retry << "\n";
}
};
int main() {
Config a; // 既定値
Config b("debug", 10); // 指定値
a.print();
b.print();
}
mode=release, retry=3
mode=debug, retry=10
なお、より効率的で安全なメンバイニシャライザ(初期化子リスト)という書き方もありますが、これは別の記事で詳しく解説します。
ここでは「まずはコンストラクタ内で値をそろえる」ことを徹底してください。
よくあるミスとチェックリスト
voidを付けない/returnしない
コンストラクタに戻り値は書きません。
void
を付けると別物になり、return
で値を返すこともできません。
終了したい場合は単にブロックを抜けるだけです。
class Bad {
public:
// void Bad() { } // NG: これは関数
Bad() {
// return 0; // NG: 値は返せない
}
};
コンストラクタ名の綴りミスに注意
クラス名と1文字でも違うとただのメンバ関数になります。
クラス名Person
に対してperson()
やPersom()
は誤りです。
エディタのシンタックスハイライトや補完を活用しましょう。
初期化の場所を統一する(コンストラクタ内)
オブジェクトの初期状態は必ずコンストラクタで完結させます。
生成直後にsetup()
やinit()
を別途呼ばないと使えない設計はバグの温床です。
初期値の決定ルールをコンストラクタに集約して、いつ作っても同じ正しい状態にしてください。
例: 悪い例と良い例の対比
// 悪い例: 生成後に別途initを呼ばないと未完成
class EngineBad {
public:
int state;
void init() { state = 1; }
};
// 良い例: 生成だけで完成する
class EngineGood {
public:
int state;
EngineGood() { state = 1; }
};
まとめ
コンストラクタはオブジェクトを正しく使い始めるための初期化専用の関数で、クラス名と同名・戻り値なしというルールに従います。
引数なしのデフォルトコンストラクタと、引数つきのコンストラクタを使い分け、生成と同時に矛盾のない状態を作ることがポイントです。
引数なしの生成はType a;
やType a{};
を使い、Type a(); は書かないように注意します。
コンストラクタを1つも書かないと自動生成されますが、組み込み型は未初期化になり得ます。
まずは本体での代入による初期化に慣れ、次のステップとしてメンバイニシャライザへ進むと理解が深まります。