C++のコンストラクタとデストラクタについて解説

C++において、コンストラクタとデストラクタは非常に重要な役割を担っています。

コンストラクタはオブジェクトが生成された際に自動的に呼び出され、初期化処理を行います。

一方、デストラクタはオブジェクトが破棄される際に自動的に呼び出され、メモリの解放やリソースの解放などの後処理を行います。

この記事では、コンストラクタとデストラクタの基礎から応用まで解説していきます。

目次

C++のコンストラクタとは

C++において、コンストラクタはオブジェクトが生成される際に自動的に呼び出される特別なメソッドです。コンストラクタは、オブジェクトの初期化を行うために使用されます。

デフォルトコンストラクタ

デフォルトコンストラクタは引数を持たないコンストラクタであり、何も処理を行わない場合でも自動的に呼び出されます。以下はデフォルトコンストラクタの例です。

class MyClass {
public:
    MyClass() {
        // 何も処理を行わない
    }
};

パラメータ付きコンストラクタ

パラメータ付きコンストラクタは、引数を受け取ってオブジェクトを初期化するためのコンストラクタです。以下はパラメータ付きコンストラクタの例です。

class MyClass {
public:
    int value;
    MyClass(int v) {
        value = v;
    }
};

//valueが10で初期化される
MyClass m = MyClass(10);

このように、パラメータ付きコンストラクタでは引数を受け取り、その値でオブジェクトを初期化します。

コピーコンストラクタ

コピーコンストラクタは、同じ型の別のオブジェクトから新しいオブジェクトを作成するための特別なコンストラクタです。以下はコピーコンストラクタの例です。

class MyClass {
public:
    int value;
    MyClass(const MyClass& other) {
        value = other.value;
    }
};

MyClass m1;
m1.value = 10;
//クラスのコピーが簡単になる
MyClass m2 = MyClass(m1);

このように、コピーコンストラクタでは同じ型の別のオブジェクトから値を受け取り、それらと同じ値で新しいオブジェクトを初期化します。

C++のデストラクタとは

C++のデストラクタは、オブジェクトが破棄される際に呼び出される関数です。デストラクタは、オブジェクトが使用していたリソースを解放するために使用されます。

デストラクタの基本的な使い方

デストラクタは、次のように定義されます。

class MyClass {
public:
    ~MyClass() {
        // リソースの解放処理
    }
};

上記の例では、MyClassという名前のクラスにデストラクタが定義されています。デストラクタは、~で始まり、その後にクラス名が続きます。

以下は、オブジェクトを生成し、それを破棄する例です。

int main() {
    MyClass obj; // オブジェクトの生成
    // 何らかの処理
} // スコープを抜けるとオブジェクトが破棄される

上記の例では、MyClass型のオブジェクトを生成しました。

このオブジェクトは、main関数内で使用された後に自動的に破棄されます。この時点で、コンパイラーは自動的にデストラクタを呼び出します。

仮想デストラクタ

仮想デストラクタとは、ポリモーフィズム(多態性)を実現するために使用される特殊な種類のデストラクタです。

仮想デストラクタを持つ基底(親) クラスから派生(子) クラスを作成した場合、派生(子) クラスでも仮想デストラクタを定義する必要があります。

仮装デストラクタを定義することで、Derivedクラスの実体をBaseクラスのポインタに参照させた状態で破棄しても、正しくDerivedクラスのデストラクタも呼ばれるようになります。

以下は仮想デストラクタを持つ基底(親) クラスと派生(子) クラスの例です:

#include <iostream>

class Base {
public:
    virtual ~Base() { std::cout << "Baseのデストラクタが呼ばれました" << std::endl; }
};

class Derived : public Base {
public:
    ~Derived() { std::cout << "Derivedのデストラクタが呼ばれました" << std::endl; }
};

int main()
{
    Base* c1 = new Derived;
    delete c1;
    
    return 0;
}
Derivedのデストラクタが呼ばれました
Baseのデストラクタが呼ばれました

こちらは、Derivedクラスのオブジェクトを動的に作成して基底クラスのBaseに代入、その後破棄していますが、破棄する際に両方のデストラクタが正しく呼ばれています。

ですが、もしvirtual ~Base()virtualを外して~Base()と定義していたらどうなるでしょうか。

#include <iostream>

class Base {
public:
    ~Base() { std::cout << "Baseのデストラクタが呼ばれました" << std::endl; }
};

class Derived : public Base {
public:
    ~Derived() { std::cout << "Derivedのデストラクタが呼ばれました" << std::endl; }
};

int main()
{
    Base* c1 = new Derived;
    delete c1;
    
    return 0;
}
Baseのデストラクタが呼ばれました

c1にはDerivedクラスの実体を代入しているにも関わらず、破棄する際にはBaseクラスのデストラクタしか呼ばれていません。

このような挙動を防止するためにあるのがvirtualキーワードを付けてデストラクタを定義する仮装デストラクタとなります。

コンストラクタとデストラクタの使い方

コンストラクタとデストラクタは、C++において非常に重要な役割を持っています。

コンストラクタはオブジェクトが生成された際に自動的に呼び出され、オブジェクトの初期化を行います。一方、デストラクタはオブジェクトが破棄される際に自動的に呼び出され、オブジェクトの終了処理を行います。

コンストラクタとデストラクタの呼び出し順序

複数の親子関係にあるオブジェクトがある場合、それらのコンストラクタとデストラクタはどのような順序で呼び出されるかが問題となります。C++では、以下のルールで呼び出しが行われます。

  • 親オブジェクトから子オブジェクトへと順番にコンストラクタが呼ばれる。
  • 子オブジェクトから親オブジェクトへと順番にデストラクタが呼ばれる。

このルールを理解しておくことで、複雑な継承関係を持つプログラムでも正しい順序で初期化・終了処理を行うことができます。

コンストラクタとデストラクタの例外処理

コンストラクタやデストラクタ内で例外が発生した場合、その例外は上位のクラスまで伝播します。

しかし、C++では例外発生時に自動的に生成されたオブジェクトは自動的に破棄されません。そのため、例外安全性を確保するためには明示的な記述が必要です。

そのため、「try-catch」構文を使用して例外処理を行い、「throw」キーワードを使用して例外発生時に明示的な破棄処理を行うないと、動的に確保したメモリが開放されずにメモリリークが発生します。

コンストラクタとデストラクタの継承

C++では、親子関係にある2つ以上のオブジェクト間で同じ名前のコンストラクタやデストラクタが定義されている場合、それらは継承関係も考慮して適切な順序で呼び出されます。

また、「virtual」キーワードを使用することで仮想コンストラクタや仮想デストラクタを定義することも可能です。これらは多重継承時やポリモーフィズム時など特殊なケースで利用されます。

以上がC++のコンストラクタとデストラクタの使い方です。正しく使いこなすことでプログラム全体の安定性や可読性向上につながります。

目次