C++コンストラクタの基礎と継承における注意点

C++におけるコンストラクタの役割

C++のコンストラクタは、クラスのオブジェクトが生成される際に自動的に呼び出される特殊なメンバー関数です。その主な目的は、オブジェクトの初期状態を適切に設定することにあります。コンストラクタはクラスと同名の識別子を持ち、戻り値の型を持ちません。

コンストラクタの特性と継承時の振る舞い

1. コンストラクタの非継承性

クラスのコンストラクタは派生クラスに継承されません。これは理にかなっています。なぜなら、たとえ継承されたとしても、その名前は基底クラスのものであり、派生クラスのコンストラクタとして機能することはできませんし、通常のメンバー関数として呼び出すこともできないためです。

2. 派生クラスからの基底クラスコンストラクタ呼び出し

派生クラスのコンストラクタからは、基底クラスのコンストラクタを明示的に呼び出すことができます。これは初期化リストを用いて行われます。


#include <iostream>
#include <string> // std::string を使用するために追加

class Person {
protected:
    std::string name_;
    int age_;
public:
    Person(const std::string& name, int age) : name_(name), age_(age) {
        std::cout << "Personコンストラクタ呼び出し: " << name_ << std::endl;
    }
    // デストラクタを追加 (stringのメモリ管理を考慮)
    virtual ~Person() {
        std::cout << "Personデストラクタ呼び出し: " << name_ << std::endl;
    }
};

class Student : public Person {
private:
    double grade_; // 成績をdouble型に変更
public:
    // 初期化リストで基底クラスのコンストラクタを呼び出す
    Student(const std::string& name, int age, double grade)
        : Person(name, age), grade_(grade) {
        std::cout << "Studentコンストラクタ呼び出し: " << name_ << std::endl;
    }

    void showInfo() const {
        std::cout << name_ << "さんの年齢は" << age_ << "歳で、平均成績は" << grade_ << "点です。" << std::endl;
    }
    // デストラクタを追加
    ~Student() override {
        std::cout << "Studentデストラクタ呼び出し: " << name_ << std::endl;
    }
};

int main() {
    Student student("山田太郎", 18, 87.5);
    student.showInfo();
    return 0;
}

上記の例では、Student クラスのコンストラクタ Student(const std::string& name, int age, double grade) の初期化リストで Person(name, age) を呼び出しています。これにより、nameage が基底クラス Person のコンストラクタに渡され、初期化されます。grade_(grade) は派生クラス自身のメンバー変数の初期化です。

初期化リストにおける基底クラスコンストラクタの呼び出し順序は、リスト内での記述順序に依存しません。C++の標準では、まず基底クラスのコンストラクタが呼び出され、次にメンバー初期化リスト、最後にコンストラクタ本体のコードが実行されます。

誤った記述例:


Student::Student(const std::string& name, int age, double grade) {
    // Person(name, age); // これはコンストラクタとして機能せず、一時オブジェクトを生成するだけ
    name_ = name; // 基底クラスのメンバーを直接初期化しようとしても、protected/privateの場合アクセスできない
    age_ = age;
    grade_ = grade;
}

基底クラスのコンストラクタは、派生クラスのコンストラクタ本体内で通常の関数のように呼び出すことはできません。これは、コンストラクタはオブジェクトが完全に構築される前に一度だけ実行されるべきであり、関数呼び出しではその要件を満たせないためです。必ず初期化リストを使用する必要があります。

3. コンストラクタの呼び出し順序

オブジェクトが継承階層を持つ場合、コンストラクタは常に基底クラスから派生クラスへと順に呼び出されます。例えば、A → B → C という継承関係がある場合、C クラスのオブジェクトが生成されるとき、コンストラクタは A → B → C の順で実行されます。この呼び出し順序は、オブジェクトの各部分が正しく初期化されることを保証します。

派生クラスのコンストラクタは、自身の直接の基底クラスのコンストラクタのみを明示的に呼び出すことができます。間接的な基底クラス(例: C から見た A)のコンストラクタを直接呼び出すことはできません。これは、直接の基底クラスがすでにその上の基底クラスのコンストラクタを呼び出す責任を負っているためです。もし間接的な基底クラスのコンストラクタを複数回呼び出すことを許してしまうと、冗長な初期化や意図しない副作用が生じる可能性があります。

コンストラクタ利用時の重要な考慮事項

派生クラスのオブジェクトが生成される際には、必ず基底クラスのコンストラクタが呼び出されます。これはC++の言語仕様によるものです。

派生クラスのコンストラクタを定義する際、明示的に基底クラスのコンストラクタを指定することを強く推奨します。もし基底クラスのコンストラクタが指定されない場合、コンパイラは自動的に基底クラスのデフォルトコンストラクタ(引数を持たないコンストラクタ)を呼び出そうとします。しかし、もし基底クラスにデフォルトコンストラクタが定義されておらず、かつ他の引数付きコンストラクタのみが存在する場合、コンパイルエラーが発生します。


#include <iostream>
#include <string>

class BaseEntity {
protected:
    std::string id_;
    int lifespan_;
public:
    // デフォルトコンストラクタ
    BaseEntity() : id_("UNKNOWN"), lifespan_(0) {
        std::cout << "BaseEntity デフォルトコンストラクタ呼び出し" << std::endl;
    }
    // パラメータ付きコンストラクタ
    BaseEntity(const std::string& id, int life) : id_(id), lifespan_(life) {
        std::cout << "BaseEntity パラメータ付きコンストラクタ呼び出し: " << id_ << std::endl;
    }
    virtual ~BaseEntity() {
        std::cout << "BaseEntity デストラクタ呼び出し: " << id_ << std::endl;
    }
};

class DerivedItem : public BaseEntity {
private:
    std::string material_;
public:
    // 派生クラスのデフォルトコンストラクタ
    // 明示的に基底クラスコンストラクタを指定しないため、BaseEntityのデフォルトコンストラクタが呼び出される
    DerivedItem() : material_("Plastic") {
        std::cout << "DerivedItem デフォルトコンストラクタ呼び出し" << std::endl;
    }
    // 派生クラスのパラメータ付きコンストラクタ
    // 明示的に基底クラスのパラメータ付きコンストラクタを指定
    DerivedItem(const std::string& id, int life, const std::string& mat)
        : BaseEntity(id, life), material_(mat) {
        std::cout << "DerivedItem パラメータ付きコンストラクタ呼び出し: " << id_ << std::endl;
    }

    void printDetails() const {
        std::cout << "ID: " << id_ << ", 寿命: " << lifespan_ << "年, 素材: " << material_ << std::endl;
    }
    ~DerivedItem() override {
        std::cout << "DerivedItem デストラクタ呼び出し: " << id_ << std::endl;
    }
};

int main() {
    std::cout << "--- 最初のオブジェクト (デフォルトコンストラクタ) ---" << std::endl;
    DerivedItem item1;
    item1.printDetails();

    std::cout << "\n--- 次のオブジェクト (パラメータ付きコンストラクタ) ---" << std::endl;
    DerivedItem item2("UniqueAmulet", 500, "Mythril");
    item2.printDetails();

    return 0;
}
/*
実行結果例:
--- 最初のオブジェクト (デフォルトコンストラクタ) ---
BaseEntity デフォルトコンストラクタ呼び出し
DerivedItem デフォルトコンストラクタ呼び出し
ID: UNKNOWN, 寿命: 0年, 素材: Plastic
DerivedItem デストラクタ呼び出し: UNKNOWN
BaseEntity デストラクタ呼び出し: UNKNOWN

--- 次のオブジェクト (パラメータ付きコンストラクタ) ---
BaseEntity パラメータ付きコンストラクタ呼び出し: UniqueAmulet
DerivedItem パラメータ付きコンストラクタ呼び出し: UniqueAmulet
ID: UniqueAmulet, 寿命: 500年, 素材: Mythril
DerivedItem デストラクタ呼び出し: UniqueAmulet
BaseEntity デストラクタ呼び出し: UniqueAmulet
*/

上記のコードでは、item1 の生成時に DerivedItem() が呼び出され、このコンストラクタは基底クラスのコンストラクタを明示的に指定していないため、BaseEntity() デフォルトコンストラクタが自動的に呼び出されます。

一方、item2 の生成時には DerivedItem(const std::string& id, int life, const std::string& mat) が呼び出され、これは初期化リストで BaseEntity(id, life) を明示的に指定しています。もし BaseEntity クラスにデフォルトコンストラクタ BaseEntity() が存在しない場合、DerivedItem() の定義箇所でコンパイルエラーが発生することになります。

タグ: C++ コンストラクタ 継承 初期化リスト オブジェクト指向プログラミング

6月11日 19:40 投稿