Menu

C++のコンストラクタ:オブジェクトを正しく初期化する

コンストラクタは、オブジェクトが生成されるときに実行される特別なメンバ関数です。デフォルトコンストラクタ、引数付きコンストラクタ、コピーコンストラクタ、メンバ初期化リスト、そしてオブジェクトを中途半端に初期化したまま放置しない方法を学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

コンストラクタとは

前のページでは、クラスを定義してメンバ変数を持たせました。しかし、生成したばかりのオブジェクトのメンバは、あなたが値を設定しない限り、そのメモリにたまたまあったゴミを保持します。コンストラクタはこれを解決します。コンストラクタは、オブジェクトが生成された瞬間に自動的に実行される特別なメンバ関数であり、その唯一の役割は、オブジェクトを有効で完全に初期化された状態にしておくことです。

コンストラクタはクラスと同じ名前を持ち、戻り値の型を持ちません——voidですらありません。あなたがコンストラクタを直接呼ぶことはありません。オブジェクトが存在するようになるたびに、コンパイラが代わりに呼び出します。

パラメータを持たないCounter()デフォルトコンストラクタと呼ばれます——引数を渡さずにオブジェクトを生成するときに使われるものです。

引数付きコンストラクタ

引数を取らないコンストラクタでも問題ありませんが、たいていは特定の値を「もった」オブジェクトを生成したいものです。引数付きコンストラクタは引数を受け取り、それを使ってメンバを初期化します。

クラスは、引数リストが異なる限り、複数のコンストラクタを持てます——これはコンストラクタに適用された通常の関数のオーバーロードです。ここではPointを座標ありでも座標なしでも生成できます。

よくある落とし穴:Point p();はオブジェクトを生成しません——コンパイラはこれを、Pointを返すpという名前の関数の宣言として読み取ります。デフォルトコンストラクタを呼ぶには、Point p;(括弧なし)または波括弧を使ったPoint p{};と書きます。

メンバ初期化リスト

これまでの例では、コンストラクタの本体の「中」でメンバに代入していました。単純な型ではこれでも動きますが、適切な手段ではありません。本体が実行される時点で、各メンバはすでにデフォルト構築されています。本体はそれを捨てて、上書き代入しているのです。メンバ初期化リストは、本体より前に、各メンバを一度の手順で直接初期化します。

構文は、パラメータリストのあとにコロンを置き、続けてmember(value)のペアを並べます。

stringメンバの場合、これは空の文字列を構築してから代入するという手間も避けられます——初期化リストは最初の一回で正しく構築します。

初期化リストは単なる最適化ではありません。本体は代入はできても初期化はできないため、次の3つのケースでは必須です。

  • constメンバ——constは一度存在してしまうと代入できません。
  • 参照メンバ——参照は生まれた瞬間に束縛されなければなりません。
  • 型がデフォルトコンストラクタを持たないメンバ。
class Sensor {
    const int id;        // const メンバ
    int& slot;           // 参照メンバ

public:
    Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
    // id や slot を本体で設定しようとするとコンパイルできない。
};

知っておくべき微妙な点:メンバはクラス内で宣言された順序で初期化され、初期化リストに並べた順序では初期化されません。あるメンバの初期化子が別のメンバを読む場合に重要なのは宣言順であり、両者を取り違えると、まだ初期化されていない値を使ってしまうという典型的なバグの原因になります。

デフォルト引数と委譲コンストラクタ

別々のオーバーロードが常に必要なわけではありません。デフォルト引数を使えば、一つのコンストラクタで複数のケースをカバーできます——引数を省略すると、デフォルト値が埋めてくれます。

デフォルト値を持つ引数付きコンストラクタと、別個のPoint()デフォルトコンストラクタを組み合わせるときは注意してください——コンパイラはPoint p;に対してどちらを呼べばよいか判断できず、あいまいだと報告します。どちらか一方の方法を選んでください。

共通のセットアップを共有する複数のコンストラクタがある場合、委譲コンストラクタ(C++11)を使えば、ロジックを繰り返す代わりに、あるコンストラクタが別のコンストラクタを呼び出せます。もう一方のコンストラクタを初期化リストに置くことで「委譲」します。

コピーコンストラクタ

あるオブジェクトを別のオブジェクトのコピーとして生成するとき——値渡しで渡したり、返したり、Foo b = a;と書いたりするとき——コピーコンストラクタが実行されます。そのシグネチャは、同じ型へのconst参照を取ります。

ClassName(const ClassName& other);

自分で書かなければ、コンパイラは各メンバをコピーするデフォルトのコピーコンストラクタを生成します。値(int、string、vector)だけを保持するクラスでは、それがまさに正しい挙動であり、自前のものを書くべきではありません。

大きな落とし穴は、メモリを扱う次の章にあります。クラスがヒープメモリへの生ポインタを所有している場合、デフォルトのコピーコンストラクタはデータではなくポインタをコピーします——その結果、2つのオブジェクトが同じメモリを指すことになり、両方がそれを解放しようとします。これがダブルフリー(二重解放)のバグです。経験則は「三/五の規則」です。カスタムのデストラクタを書くなら、ほぼ確実にカスタムのコピーコンストラクタ(およびコピー代入)も必要になります。現代的なC++では、より洗練された解決策はstd::vectorやスマートポインタを保持することで、コンパイラが生成するコピーがそのまま正しく動くようにすることです。

もう一点、パラメータを参照で受け取ることは任意ではなく必須です。引数を値で受け取るコピーコンストラクタは、自分自身を呼び出すために引数をコピーしなければならず——それは無限再帰となり、そもそもコンパイルすら通りません。

次は:デストラクタ

コンストラクタはオブジェクトを組み立て、デストラクタはそれを片付けます。オブジェクトがスコープを抜けるか削除されると、そのデストラクタが自動的に実行されます——オブジェクトが保持していたファイル、ネットワーク接続、ヒープメモリを解放するのに最適な場所です。次のページでは、デストラクタがどのように動くのか、正確にいつ起動するのか、そしてC++に強力なRAIIパターンをもたらすためにコンストラクタとどう対になるのかを扱います。

よくある質問

C++のコンストラクタとは何ですか?

コンストラクタは、クラスと同じ名前を持ち、戻り値の型を持たない特別なメンバ関数です。オブジェクトが生成されると自動的に実行され、その役割は、ほかのコードがオブジェクトを使う前に、有効で完全に初期化された状態にすることです。

デフォルトコンストラクタと引数付きコンストラクタの違いは何ですか?

デフォルトコンストラクタは引数を取らず、値を渡さずにオブジェクトを生成するとき(Point p;)に使われます。引数付きコンストラクタは引数を取り、呼び出し側が特定の値でオブジェクトを初期化できるようにします(Point p(3, 4);)。コンストラクタは引数リストでオーバーロードされるため、クラスは両方を持つことができます。

C++でメンバ初期化リストを使うべき理由は何ですか?

メンバ初期化リスト(: name(n), age(a))は、コンストラクタの本体が実行される前に、メンバを直接初期化します。constメンバ、参照、デフォルトコンストラクタを持たないメンバには必須であり、本体内で代入するときに起こる「いったんデフォルト構築してから代入する」という無駄を避けられます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める