クラス

[C++] デストラクタが呼ばれるタイミングを解説

C++においてデストラクタは、オブジェクトのライフサイクルが終了したときに自動的に呼ばれます。

具体的には、以下のタイミングでデストラクタが呼ばれます。

デストラクタが呼ばれるタイミング

スコープを抜けたとき

C++では、オブジェクトがスコープを抜けると、そのオブジェクトのデストラクタが自動的に呼び出されます。

これにより、オブジェクトが使用していたリソースが解放されます。

以下は、スコープを抜けたときにデストラクタが呼ばれる例です。

#include <iostream>
class MyClass {
public:
    MyClass() {
        std::cout << "コンストラクタが呼ばれました" << std::endl;
    }
    ~MyClass() {
        std::cout << "デストラクタが呼ばれました" << std::endl;
    }
};
int main() {
    {
        MyClass obj; // スコープ内でオブジェクトを生成
    } // スコープを抜けるとデストラクタが呼ばれる
    return 0;
}
コンストラクタが呼ばれました
デストラクタが呼ばれました

動的メモリの解放時 (delete/delete[])

動的に生成されたオブジェクトは、deleteまたはdelete[]を使用して解放されると、そのデストラクタが呼び出されます。

これにより、メモリリークを防ぐことができます。

以下は、動的メモリの解放時にデストラクタが呼ばれる例です。

#include <iostream>
class MyClass {
public:
    MyClass() {
        std::cout << "コンストラクタが呼ばれました" << std::endl;
    }
    ~MyClass() {
        std::cout << "デストラクタが呼ばれました" << std::endl;
    }
};
int main() {
    MyClass* obj = new MyClass(); // 動的にオブジェクトを生成
    delete obj; // デストラクタが呼ばれる
    return 0;
}
コンストラクタが呼ばれました
デストラクタが呼ばれました

静的オブジェクトの終了時

静的オブジェクトは、プログラムの実行が終了する際にデストラクタが呼ばれます。

これにより、静的オブジェクトが使用していたリソースが適切に解放されます。

以下は、静的オブジェクトの終了時にデストラクタが呼ばれる例です。

#include <iostream>
class MyClass {
public:
    MyClass() {
        std::cout << "コンストラクタが呼ばれました" << std::endl;
    }
    ~MyClass() {
        std::cout << "デストラクタが呼ばれました" << std::endl;
    }
};
MyClass staticObj; // 静的オブジェクト
int main() {
    std::cout << "メイン関数が実行中" << std::endl;
    return 0; // プログラム終了時にデストラクタが呼ばれる
}
コンストラクタが呼ばれました
メイン関数が実行中
デストラクタが呼ばれました

グローバルオブジェクトの終了時

グローバルオブジェクトも、プログラムの終了時にデストラクタが呼ばれます。

これにより、グローバルオブジェクトが使用していたリソースが解放されます。

以下は、グローバルオブジェクトの終了時にデストラクタが呼ばれる例です。

#include <iostream>
class MyClass {
public:
    MyClass() {
        std::cout << "コンストラクタが呼ばれました" << std::endl;
    }
    ~MyClass() {
        std::cout << "デストラクタが呼ばれました" << std::endl;
    }
};
MyClass globalObj; // グローバルオブジェクト
int main() {
    std::cout << "メイン関数が実行中" << std::endl;
    return 0; // プログラム終了時にデストラクタが呼ばれる
}
コンストラクタが呼ばれました
メイン関数が実行中
デストラクタが呼ばれました

メンバオブジェクトのデストラクタ呼び出し

クラスのメンバとして定義されたオブジェクトのデストラクタは、親クラスのデストラクタが呼ばれた後に呼び出されます。

これにより、メンバオブジェクトが適切に解放されます。

以下は、メンバオブジェクトのデストラクタが呼ばれる例です。

#include <iostream>
class MemberClass {
public:
    MemberClass() {
        std::cout << "MemberClassのコンストラクタが呼ばれました" << std::endl;
    }
    ~MemberClass() {
        std::cout << "MemberClassのデストラクタが呼ばれました" << std::endl;
    }
};
class MyClass {
private:
    MemberClass member; // メンバオブジェクト
public:
    MyClass() {
        std::cout << "MyClassのコンストラクタが呼ばれました" << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClassのデストラクタが呼ばれました" << std::endl;
    }
};
int main() {
    MyClass obj; // オブジェクト生成
    return 0; // デストラクタが呼ばれる
}
MemberClassのコンストラクタが呼ばれました
MyClassのコンストラクタが呼ばれました
MyClassのデストラクタが呼ばれました
MemberClassのデストラクタが呼ばれました

例外が発生したときのデストラクタ呼び出し

C++では、例外が発生した場合、スコープを抜ける際にデストラクタが呼ばれます。

これにより、リソースが適切に解放され、メモリリークを防ぐことができます。

以下は、例外が発生したときにデストラクタが呼ばれる例です。

#include <iostream>
class MyClass {
public:
    MyClass() {
        std::cout << "コンストラクタが呼ばれました" << std::endl;
    }
    ~MyClass() {
        std::cout << "デストラクタが呼ばれました" << std::endl;
    }
};
int main() {
    try {
        MyClass obj; // オブジェクト生成
        throw std::runtime_error("例外が発生しました"); // 例外を投げる
    } catch (const std::exception& e) {
        std::cout << e.what() << std::endl;
    } // スコープを抜けるとデストラクタが呼ばれる
    return 0;
}
コンストラクタが呼ばれました
例外が発生しました
デストラクタが呼ばれました

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

メンバオブジェクトのデストラクタ呼び出し順序

C++では、クラスのデストラクタが呼ばれる際、メンバオブジェクトのデストラクタは親クラスのデストラクタが呼ばれた後に呼び出されます。

これにより、メンバオブジェクトが適切に解放されることが保証されます。

以下は、メンバオブジェクトのデストラクタ呼び出し順序を示す例です。

#include <iostream>
class MemberClass {
public:
    MemberClass() {
        std::cout << "MemberClassのコンストラクタが呼ばれました" << std::endl;
    }
    ~MemberClass() {
        std::cout << "MemberClassのデストラクタが呼ばれました" << std::endl;
    }
};
class MyClass {
private:
    MemberClass member; // メンバオブジェクト
public:
    MyClass() {
        std::cout << "MyClassのコンストラクタが呼ばれました" << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClassのデストラクタが呼ばれました" << std::endl;
    }
};
int main() {
    MyClass obj; // オブジェクト生成
    return 0; // デストラクタが呼ばれる
}
MemberClassのコンストラクタが呼ばれました
MyClassのコンストラクタが呼ばれました
MyClassのデストラクタが呼ばれました
MemberClassのデストラクタが呼ばれました

継承関係におけるデストラクタの呼び出し順序

継承関係にあるクラスでは、デストラクタが呼ばれる順序は、子クラスから親クラスの順に呼び出されます。

これにより、子クラスが使用しているリソースが先に解放され、親クラスのリソースが後に解放されることが保証されます。

以下は、継承関係におけるデストラクタの呼び出し順序を示す例です。

#include <iostream>
class Base {
public:
    Base() {
        std::cout << "Baseのコンストラクタが呼ばれました" << std::endl;
    }
    virtual ~Base() { // 仮想デストラクタ
        std::cout << "Baseのデストラクタが呼ばれました" << std::endl;
    }
};
class Derived : public Base {
public:
    Derived() {
        std::cout << "Derivedのコンストラクタが呼ばれました" << std::endl;
    }
    ~Derived() {
        std::cout << "Derivedのデストラクタが呼ばれました" << std::endl;
    }
};
int main() {
    Derived obj; // オブジェクト生成
    return 0; // デストラクタが呼ばれる
}
Baseのコンストラクタが呼ばれました
Derivedのコンストラクタが呼ばれました
Derivedのデストラクタが呼ばれました
Baseのデストラクタが呼ばれました

仮想デストラクタの役割と呼び出し順序

仮想デストラクタは、基底クラスのポインタを通じて派生クラスのオブジェクトを削除する際に、正しいデストラクタが呼ばれることを保証します。

これにより、リソースの適切な解放が行われます。

以下は、仮想デストラクタの役割と呼び出し順序を示す例です。

#include <iostream>
class Base {
public:
    Base() {
        std::cout << "Baseのコンストラクタが呼ばれました" << std::endl;
    }
    virtual ~Base() { // 仮想デストラクタ
        std::cout << "Baseのデストラクタが呼ばれました" << std::endl;
    }
};
class Derived : public Base {
public:
    Derived() {
        std::cout << "Derivedのコンストラクタが呼ばれました" << std::endl;
    }
    ~Derived() {
        std::cout << "Derivedのデストラクタが呼ばれました" << std::endl;
    }
};
int main() {
    Base* obj = new Derived(); // 基底クラスのポインタで派生クラスのオブジェクトを生成
    delete obj; // 正しいデストラクタが呼ばれる
    return 0;
}
Baseのコンストラクタが呼ばれました
Derivedのコンストラクタが呼ばれました
Derivedのデストラクタが呼ばれました
Baseのデストラクタが呼ばれました

デストラクタの応用例

動的メモリ管理とデストラクタ

デストラクタは、動的に確保したメモリを解放するために重要な役割を果たします。

オブジェクトがスコープを抜ける際や、deleteを使用してオブジェクトを削除する際に、デストラクタが呼ばれ、メモリリークを防ぎます。

以下は、動的メモリ管理におけるデストラクタの例です。

#include <iostream>
class MyClass {
private:
    int* data; // 動的メモリを使用するメンバ
public:
    MyClass(int size) {
        data = new int[size]; // メモリを動的に確保
        std::cout << "メモリを確保しました" << std::endl;
    }
    ~MyClass() {
        delete[] data; // メモリを解放
        std::cout << "メモリを解放しました" << std::endl;
    }
};
int main() {
    MyClass obj(10); // オブジェクト生成
    return 0; // デストラクタが呼ばれ、メモリが解放される
}
メモリを確保しました
メモリを解放しました

ファイルやリソースのクリーンアップ

デストラクタは、ファイルやその他のリソースをクリーンアップするためにも使用されます。

オブジェクトが破棄される際に、リソースを適切に解放することで、リソースリークを防ぎます。

以下は、ファイルのクリーンアップにおけるデストラクタの例です。

#include <iostream>
#include <fstream>
class FileHandler {
private:
    std::ofstream file; // ファイルストリーム
public:
    FileHandler(const std::string& filename) {
        file.open(filename); // ファイルをオープン
        std::cout << "ファイルをオープンしました" << std::endl;
    }
    ~FileHandler() {
        if (file.is_open()) {
            file.close(); // ファイルをクローズ
            std::cout << "ファイルをクローズしました" << std::endl;
        }
    }
};
int main() {
    FileHandler handler("example.txt"); // オブジェクト生成
    return 0; // デストラクタが呼ばれ、ファイルがクローズされる
}
ファイルをオープンしました
ファイルをクローズしました

スマートポインタとデストラクタ

スマートポインタは、デストラクタを利用して動的メモリの管理を自動化します。

スマートポインタがスコープを抜けると、自動的にメモリが解放されるため、メモリリークのリスクが低減します。

以下は、std::unique_ptrを使用した例です。

#include <iostream>
#include <memory> // スマートポインタ用のヘッダ
class MyClass {
public:
    MyClass() {
        std::cout << "コンストラクタが呼ばれました" << std::endl;
    }
    ~MyClass() {
        std::cout << "デストラクタが呼ばれました" << std::endl;
    }
};
int main() {
    std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>(); // スマートポインタを使用
    return 0; // スコープを抜けるとデストラクタが呼ばれる
}
コンストラクタが呼ばれました
デストラクタが呼ばれました

RAIIパターンにおけるデストラクタの活用

RAII(Resource Acquisition Is Initialization)パターンでは、リソースの取得と解放をオブジェクトのライフサイクルに結びつけます。

デストラクタを利用して、オブジェクトが破棄される際にリソースを自動的に解放することができます。

以下は、RAIIパターンの例です。

#include <iostream>
class Resource {
public:
    Resource() {
        std::cout << "リソースを取得しました" << std::endl;
    }
    ~Resource() {
        std::cout << "リソースを解放しました" << std::endl;
    }
};
void useResource() {
    Resource res; // リソースを取得
    // リソースを使用する処理
}
int main() {
    useResource(); // 関数が終了するとデストラクタが呼ばれる
    return 0;
}
リソースを取得しました
リソースを解放しました

デストラクタに関する注意点

デストラクタで例外を投げるべきでない理由

デストラクタ内で例外を投げることは避けるべきです。

なぜなら、デストラクタが呼ばれる際に、すでに例外が発生している場合、C++では新たな例外を投げることができず、プログラムが異常終了する可能性があるからです。

デストラクタ内での例外は、リソースの解放が適切に行われない原因にもなります。

以下は、デストラクタで例外を投げることの問題を示す例です。

#include <iostream>
class MyClass {
public:
    ~MyClass() {
        throw std::runtime_error("デストラクタで例外が発生しました"); // 例外を投げる
    }
};
int main() {
    try {
        MyClass obj; // オブジェクト生成
    } catch (const std::exception& e) {
        std::cout << e.what() << std::endl; // 例外をキャッチ
    }
    return 0; // プログラムが異常終了する可能性がある
}
デストラクタで例外が発生しました

デストラクタの中でdeleteを使う際の注意点

デストラクタ内でdeleteを使用する際は、対象のポインタがすでに解放されていないことを確認する必要があります。

二重解放を避けるために、ポインタをnullptrに設定することが推奨されます。

以下は、デストラクタ内でのdeleteの使用に関する注意点を示す例です。

#include <iostream>
class MyClass {
private:
    int* data; // 動的メモリを使用するメンバ
public:
    MyClass() {
        data = new int(42); // メモリを動的に確保
    }
    ~MyClass() {
        delete data; // メモリを解放
        data = nullptr; // ポインタをnullptrに設定
    }
};
int main() {
    MyClass obj; // オブジェクト生成
    return 0; // デストラクタが呼ばれ、メモリが解放される
}
(出力はありませんが、メモリリークは発生しません)

仮想デストラクタを使わない場合の問題点

基底クラスに仮想デストラクタが定義されていない場合、基底クラスのポインタを通じて派生クラスのオブジェクトを削除すると、派生クラスのデストラクタが呼ばれず、リソースが適切に解放されない可能性があります。

これにより、メモリリークやリソースリークが発生します。

以下は、仮想デストラクタを使わない場合の問題を示す例です。

#include <iostream>
class Base {
public:
    Base() {
        std::cout << "Baseのコンストラクタが呼ばれました" << std::endl;
    }
    ~Base() { // 仮想デストラクタではない
        std::cout << "Baseのデストラクタが呼ばれました" << std::endl;
    }
};
class Derived : public Base {
public:
    Derived() {
        std::cout << "Derivedのコンストラクタが呼ばれました" << std::endl;
    }
    ~Derived() {
        std::cout << "Derivedのデストラクタが呼ばれました" << std::endl;
    }
};
int main() {
    Base* obj = new Derived(); // 基底クラスのポインタで派生クラスのオブジェクトを生成
    delete obj; // 派生クラスのデストラクタが呼ばれない
    return 0;
}
Baseのコンストラクタが呼ばれました
Derivedのコンストラクタが呼ばれました
Baseのデストラクタが呼ばれました
(Derivedのデストラクタは呼ばれないため、リソースリークが発生する)

コピーコンストラクタとデストラクタの関係

コピーコンストラクタとデストラクタは、オブジェクトのライフサイクルにおいて密接に関連しています。

コピーコンストラクタが呼ばれると、新しいオブジェクトが生成され、元のオブジェクトのリソースがコピーされます。

デストラクタは、オブジェクトが破棄される際にリソースを解放します。

適切なリソース管理を行うためには、コピーコンストラクタとデストラクタの実装が重要です。

以下は、コピーコンストラクタとデストラクタの関係を示す例です。

#include <iostream>
class MyClass {
private:
    int* data; // 動的メモリを使用するメンバ
public:
    MyClass(int value) {
        data = new int(value); // メモリを動的に確保
        std::cout << "コンストラクタが呼ばれました" << std::endl;
    }
    MyClass(const MyClass& other) { // コピーコンストラクタ
        data = new int(*other.data); // 深いコピー
        std::cout << "コピーコンストラクタが呼ばれました" << std::endl;
    }
    ~MyClass() {
        delete data; // メモリを解放
        std::cout << "デストラクタが呼ばれました" << std::endl;
    }
};
int main() {
    MyClass obj1(42); // オブジェクト生成
    MyClass obj2 = obj1; // コピーコンストラクタが呼ばれる
    return 0; // デストラクタが呼ばれ、メモリが解放される
}
コンストラクタが呼ばれました
コピーコンストラクタが呼ばれました
デストラクタが呼ばれました
デストラクタが呼ばれました

まとめ

この記事では、C++におけるデストラクタの呼び出しタイミングやその順序、応用例、注意点について詳しく解説しました。

デストラクタは、オブジェクトのライフサイクルにおいて重要な役割を果たし、リソースの適切な管理を実現するために欠かせない要素です。

今後は、デストラクタの特性を活かして、より安全で効率的なプログラムを作成することを心がけてください。

関連記事

Back to top button