閉じる

【C++】デフォルトコンストラクタの基本|生成条件や注意点を分かりやすく解説

C++におけるクラス設計において、オブジェクトが生成される瞬間に何が起きているのかを理解することは非常に重要です。

その中心的な役割を担うのが「コンストラクタ」ですが、中でも引数を取らずに呼び出されるデフォルトコンストラクタは、配列の確保や標準ライブラリのコンテナ利用時に不可欠な存在となります。

本記事では、デフォルトコンストラクタの基礎から、コンパイラによる自動生成のルール、モダンC++での制御方法、そして実務で陥りやすい注意点までを詳しく解説します。

デフォルトコンストラクタとは何か

デフォルトコンストラクタとは、引数を一つも受け取らずに呼び出すことができるコンストラクタのことを指します。

クラスのインスタンスを生成する際、明示的に初期値を指定しない場合にこのコンストラクタが実行されます。

C++では、クラスを定義した際に開発者が一つもコンストラクタを定義しなかった場合、コンパイラが自動的に暗黙のデフォルトコンストラクタを生成します。

しかし、何らかのコンストラクタを自前で定義した瞬間に、この自動生成は停止します。

この挙動を正しく理解していないと、「なぜかオブジェクトが生成できない」といったコンパイルエラーに悩まされることになります。

デフォルトコンストラクタの基本構造

デフォルトコンストラクタは、クラス名と同じ名前の関数であり、戻り値の型を持ちません。

C++
#include <iostream>
#include <string>

class Player {
public:
    // これがデフォルトコンストラクタです
    Player() {
        name = "Unknown";
        level = 1;
        std::cout << "デフォルトコンストラクタが呼ばれました" << std::endl;
    }

    void display() const {
        std::cout << "Name: " << name << ", Level: " << level << std::endl;
    }

private:
    std::string name;
    int level;
};

int main() {
    // 引数なしでインスタンス化
    Player p1; 
    p1.display();
    
    return 0;
}
実行結果
デフォルトコンストラクタが呼ばれました
Name: Unknown, Level: 1

上記のコードでは、Player p1; と記述した瞬間にデフォルトコンストラクタが走り、メンバ変数が初期化されています。

暗黙のデフォルトコンストラクタの生成条件

C++のコンパイラは、一定の条件下でデフォルトコンストラクタを勝手に作ってくれます。

これを「暗黙のデフォルトコンストラクタ」と呼びますが、これには明確なルールが存在します。

自動生成されるケース

クラス内にコンストラクタが一つも定義されていない場合、コンパイラは引数なしのコンストラクタを内部的に作成します。

この自動生成されたコンストラクタは、各メンバ変数のデフォルトコンストラクタを再帰的に呼び出します。

ただし、intやdoubleなどの組み込み型(プリミティブ型)の変数は初期化されず、不定値(ゴミデータ)が入ることに注意が必要です。

自動生成されないケース

開発者が引数を持つコンストラクタを一つでも定義した場合、コンパイラは「このクラスの初期化は開発者が完全にコントロールするつもりだ」と判断し、デフォルトコンストラクタの自動生成を放棄します。

C++
class Enemy {
public:
    // 引数付きコンストラクタを定義
    Enemy(int hp) {
        health = hp;
    }
private:
    int health;
};

int main() {
    // Enemy e1; // エラー!デフォルトコンストラクタが存在しないためコンパイルできない
    Enemy e2(100); // これはOK
    return 0;
}

このように、特定の初期化方法を強制したい場合には有効ですが、後述する配列の作成などができなくなる制約が発生します。

モダンC++での制御:defaultとdelete

C++11以降、デフォルトコンストラクタの挙動をより明示的に制御するためのキーワードが導入されました。

これにより、コードの意図が明確になり、コンパイラの最適化も受けやすくなります。

= default による明示的な宣言

引数付きコンストラクタを定義しつつ、コンパイラによる標準の挙動(各メンバのデフォルト初期化)を持つデフォルトコンストラクタも残したい場合に、= default を使用します。

C++
class Boss {
public:
    // コンパイラに標準のデフォルトコンストラクタを生成させる
    Boss() = default;

    // 特定の初期化を行うコンストラクタ
    Boss(int power) : strength(power) {}

private:
    int strength = 500; // インプレース初期化
};

このように記述することで、Boss b1;Boss b2(1000); の両方が可能になります。

また、自分で Boss() {} と中身が空の関数を書くよりも、= default を使うほうが「トリビアル(些細な)」なコンストラクタとして扱われ、メモリコピーなどの処理が高速化される可能性があります

= delete による生成禁止

逆に、特定の生成方法を禁止したい場合には = delete を使います。

例えば、「必ず引数を指定して生成しなければならないクラス」を作りたい場合に、デフォルトコンストラクタを明示的に禁止できます。

C++
class SecureToken {
public:
    // デフォルトでの生成を禁止する
    SecureToken() = delete;

    SecureToken(const std::string& key) : secret(key) {}

private:
    std::string secret;
};

これにより、SecureToken t; と書いた時点でコンパイルエラーとなり、不完全な状態のオブジェクトが作られるのを防ぐことができます。

デフォルトコンストラクタが必要になる場面

「引数付きコンストラクタがあれば、デフォルトコンストラクタは不要ではないか?」と考えるかもしれませんが、C++の言語仕様や標準ライブラリを利用する上で、デフォルトコンストラクタが必要不可欠な場面がいくつかあります。

1. 配列の生成

クラスの配列を静的または動的に確保する場合、個々の要素を初期化するための引数を渡すことができないため、デフォルトコンストラクタが呼ばれます。

C++
class Item {
public:
    Item() { std::cout << "Item created" << std::endl; }
};

int main() {
    // 5つのインスタンスが生成される際、デフォルトコンストラクタが5回呼ばれる
    Item inventory[5]; 
    return 0;
}

もし Item クラスにデフォルトコンストラクタがない場合、Item inventory[5]; という記述自体がコンパイルエラーになります。

2. 標準コンテナ(std::vectorなど)の特定操作

std::vector などのコンテナにおいて、要素数を指定してリサイズする場合や、要素を事前に確保する場合にもデフォルトコンストラクタが必要です。

C++
#include <vector>

class Node {
public:
    Node() = default;
    Node(int id) : id_(id) {}
private:
    int id_ = 0;
};

int main() {
    // サイズを10として確保。各要素はデフォルトコンストラクタで初期化される
    std::vector<Node> nodes(10); 
    return 0;
}

3. std::mapの角括弧演算子

std::map の要素に [] 演算子でアクセスした際、指定したキーが存在しない場合は新しい要素が作成されます。

このとき、値側の型にはデフォルトコンストラクタが求められます。

メンバ変数の初期化と注意点

デフォルトコンストラクタを扱う上で最も注意すべきは、「変数が初期化されるかどうか」という点です。

組み込み型の落とし穴

C++の古い仕様や、暗黙のデフォルトコンストラクタに頼った場合、数値型のメンバ変数は自動的に 0 にはなりません。

C++
class Data {
public:
    int value; // 初期化されていない
};

int main() {
    Data d;
    // d.value の中身は不定。実行するたびに変わる可能性がある
    std::cout << d.value << std::endl; 
    return 0;
}

これを防ぐためには、C++11から導入されたメンバ変数のインプレース初期化(クラス内初期化子)を積極的に利用すべきです。

C++
class SafeData {
public:
    // デフォルトコンストラクタが呼ばれた際、この値が使われる
    int value = 0; 
};

デフォルトコンストラクタの呼び出し失敗(Most Vexing Parse)

初心者が非常によく陥る罠として、デフォルトコンストラクタを呼び出すつもりでカッコを付けてしまう問題があります。

C++
class Widget {
public:
    Widget() { std::cout << "Widget!" << std::endl; }
};

int main() {
    // オブジェクトの生成ではなく、「引数なしでWidgetを返す関数の宣言」とみなされる
    Widget w1(); 

    // 正しい呼び出し
    Widget w2;   
    Widget w3{}; // C++11以降の推奨される書き方
    
    return 0;
}

Widget w1(); と書くと、コンパイラはそれを関数宣言として解釈します(Most Vexing Parse)。

オブジェクトを生成したい場合は、カッコを付けないか、波括弧 {} を使用するようにしましょう。

デフォルトコンストラクタ設計のベストプラクティス

ハイクオリティなC++コードを書くための、デフォルトコンストラクタに関するガイドラインをまとめます。

項目推奨されるアクション理由
初期値の設定メンバ変数は定義時に初期化するコンストラクタでの初期化漏れを防ぐため
明示的な宣言必要な場合は = default を使う意図を明確にし、最適化を促進するため
不要な生成の禁止引数必須なら = delete を使う不正な状態のオブジェクト生成を未然に防ぐため
波括弧の利用インスタンス化には {} を使う最も不愉快な構文解析(Most Vexing Parse)を避けるため

基本的には、「クラスの不変条件(そのオブジェクトが正しく動作するための最低限の条件)」を満たせるのであれば、常にデフォルトコンストラクタを提供することを検討すべきです。

これにより、標準ライブラリとの親和性が大幅に向上します。

委譲コンストラクタの活用

複数のコンストラクタで共通の初期化処理を行いたい場合、デフォルトコンストラクタから別のコンストラクタへ処理を「委譲」することができます。

C++
class Character {
public:
    // デフォルトコンストラクタから引数付きコンストラクタを呼ぶ
    Character() : Character("Hero", 100) {}

    Character(std::string name, int hp) : name_(name), hp_(hp) {
        std::cout << name_ << " が生成されました" << std::endl;
    }

private:
    std::string name_;
    int hp_;
};

このように記述することで、初期化ロジックを一箇所に集約でき、コードの重複とバグを減らすことが可能です。

まとめ

デフォルトコンストラクタは、C++のクラスライフサイクルにおいて非常に身近でありながら、その挙動には多くのルールが潜んでいます。

コンパイラによる自動生成の条件(他のコンストラクタがない場合のみ生成される)を正しく理解し、モダンな = default= delete を使いこなすことで、安全で効率的なクラス設計が可能になります。

特に、組み込み型の初期化漏れは発見しにくいバグの温床となるため、クラス内初期化子との併用は必須と言えるでしょう。

「とりあえずコンストラクタを書く」のではなく、そのクラスがどのように使われるか(配列にされるか、コンテナに入れられるか)を想定し、意図的なデフォルトコンストラクタの設計を心がけてみてください。

クラスの定義と基本

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!