閉じる

【C++】デストラクタの使い方入門|呼ばれるタイミングと書き方

C++でクラスを使い始めると、コンストラクタはすぐに登場しますが、デストラクタについては「なんとなく書いているだけ」「実際いつ呼ばれているのかよく分からない」というケースが意外と多いです。

本記事では、C++のデストラクタが呼ばれるタイミング正しい書き方・使い方を、初心者の方にも分かりやすく、図解とサンプルコードを交えて丁寧に解説していきます。

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

デストラクタの基本的な役割

C++でクラスを定義するとき、オブジェクトの「後片付け」を担当する特別な関数がデストラクタです。

コンストラクタが「初期化担当」だとすると、デストラクタは「終了処理担当」と考えるとイメージしやすくなります。

デストラクタは主に次のような役割を持ちます。

  • 動的に確保したメモリの解放
  • ファイルやネットワークソケットのクローズ
  • ミューテックスやロックの解放
  • ログ出力など、終了時に1回だけ実行したい処理

「クラスが何かを取得したら、デストラクタで必ず手放す」という対応関係を意識しておくと、設計がきれいになり、メモリリークも防ぎやすくなります。

デストラクタのシグネチャ(形)と書き方

デストラクタはクラス名の先頭に~を付けた名前で定義します。

戻り値はなく、引数も取ることができません。

C++
class Sample {
public:
    // コンストラクタ
    Sample() {
        // 初期化処理
    }

    // デストラクタ
    ~Sample() {
        // 終了処理・後片付け
    }
};

ここでのポイントは次の通りです。

  • 戻り値を書かない(cst-code>voidも書かない)
  • 引数を取れない(オーバーロードもできない)
  • 1クラスにつきデストラクタは1つだけ

「クラス名の前に~を付けたメンバ関数が1つだけ書ける」というルールを押さえておくと、シンプルに覚えられます。

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

自動変数(ローカルオブジェクト)の場合

もっとも基本的なパターンは、関数内で宣言したローカル変数としてのオブジェクトです。

この場合、スコープ(波括弧)を抜けた瞬間にデストラクタが自動で呼ばれます

C++
#include <iostream>

class Sample {
public:
    Sample() {
        std::cout << "コンストラクタ呼び出し\n";
    }

    ~Sample() {
        std::cout << "デストラクタ呼び出し\n";
    }
};

int main() {
    std::cout << "スコープ開始\n";
    {
        Sample obj;  // ここでコンストラクタが呼ばれる
        std::cout << "スコープ内処理中\n";
    }               // ここでスコープを抜けるのでデストラクタが呼ばれる
    std::cout << "スコープ終了\n";
}
実行結果
スコープ開始
コンストラクタ呼び出し
スコープ内処理中
デストラクタ呼び出し
スコープ終了

この動きを理解しておくと、「ブロックを抜けると必ずデストラクタが呼ばれるので、そのタイミングで自動的に片付けが行われる」というC++の基本的な動作が見えてきます。

動的確保(new)したオブジェクトの場合

newで確保したオブジェクトは、スコープから抜けても自動的には破棄されません。

必ずdeleteを呼び出したときにデストラクタが実行されます。

C++
#include <iostream>

class Sample {
public:
    Sample() {
        std::cout << "コンストラクタ呼び出し\n";
    }

    ~Sample() {
        std::cout << "デストラクタ呼び出し\n";
    }
};

int main() {
    Sample* ptr = new Sample();  // コンストラクタ呼び出し

    std::cout << "動的オブジェクトを利用中\n";

    delete ptr;                  // ここでデストラクタが呼ばれる

    return 0;
}
実行結果
コンストラクタ呼び出し
動的オブジェクトを利用中
デストラクタ呼び出し

もしdeleteを呼ばずに終わると、デストラクタは一度も呼ばれず、メモリリークの原因になります

手動new/deleteを使う場合はこの点に注意してください。

グローバル変数・staticオブジェクトの場合

グローバル変数や、関数内のstaticオブジェクトは、プログラムの開始と終了のタイミングで生成・破棄されます。

  • グローバル変数: mainが始まる前にコンストラクタ、プログラム終了時にデストラクタ
  • 関数内static: 最初にその行を通ったときにコンストラクタ、プログラム終了時にデストラクタ
C++
#include <iostream>

class GlobalSample {
public:
    GlobalSample() {
        std::cout << "GlobalSample コンストラクタ\n";
    }
    ~GlobalSample() {
        std::cout << "GlobalSample デストラクタ\n";
    }
};

GlobalSample g_obj;  // グローバルオブジェクト

void func() {
    static GlobalSample s_obj;  // 関数内staticオブジェクト
    std::cout << "funcの中\n";
}

int main() {
    std::cout << "main開始\n";
    func();
    std::cout << "main終了\n";
}
実行結果
GlobalSample コンストラクタ
main開始
GlobalSample コンストラクタ
funcの中
main終了
GlobalSample デストラクタ
GlobalSample デストラクタ

このように、プログラム全体で1つだけ存在するようなオブジェクトも、終了時にデストラクタが呼ばれてきちんと後片付けされます

デストラクタの実用的な使い方

動的メモリを解放するデストラクタ

デストラクタの典型的な使い方は、コンストラクタでnewしたメモリを、デストラクタでdeleteするパターンです。

C++
#include <iostream>

class IntArray {
private:
    int* data;
    std::size_t size;

public:
    // 配列サイズを受け取るコンストラクタ
    IntArray(std::size_t n)
        : data(nullptr), size(n) {
        data = new int[size];  // 動的メモリ確保
        std::cout << "IntArray: " << size << " 要素分の配列を確保\n";
    }

    // デストラクタでメモリを解放
    ~IntArray() {
        delete[] data;  // 配列は delete[] を使う
        std::cout << "IntArray: 配列を解放\n";
    }

    // 要素にアクセスするメンバ関数(簡易版)
    int& at(std::size_t index) {
        return data[index];
    }
};

int main() {
    {
        IntArray arr(5);
        arr.at(0) = 10;
        arr.at(1) = 20;
        std::cout << "処理中...\n";
    }  // ここでarrがスコープを抜けるので、デストラクタが自動で呼ばれる

    std::cout << "main終了\n";
}
実行結果
IntArray: 5 要素分の配列を確保
処理中...
IntArray: 配列を解放
main終了

クラスの外からdeleteしなくても、スコープを抜けた瞬間に勝手に解放されるのがポイントです。

これにより、メモリ管理の漏れを大幅に減らせます。

ファイルなどのリソース解放(RAIIのイメージ)

C++ではRAII(Resource Acquisition Is Initialization)という設計がよく使われます。

これは「リソースの取得と解放を、オブジェクトの生成と破棄に結びつける」という考え方です。

例として、簡易的なファイルラッパークラスを見てみます。

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

class FileWriter {
private:
    std::ofstream ofs;

public:
    // コンストラクタでファイルを開く
    explicit FileWriter(const std::string& filename) {
        ofs.open(filename);
        if (!ofs) {
            std::cerr << "ファイルを開けませんでした: " << filename << "\n";
        } else {
            std::cout << "ファイルをオープン: " << filename << "\n";
        }
    }

    // デストラクタでファイルを閉じる
    ~FileWriter() {
        if (ofs.is_open()) {
            ofs.close();
            std::cout << "ファイルをクローズ\n";
        }
    }

    // 1行書き込むメンバ関数
    void writeLine(const std::string& line) {
        if (ofs) {
            ofs << line << "\n";
        }
    }
};

int main() {
    {
        FileWriter writer("output.txt");  // ここでファイルを開く
        writer.writeLine("1行目のテキスト");
        writer.writeLine("2行目のテキスト");
        // ここでスコープを抜けると自動的にファイルが閉じられる
    }

    std::cout << "プログラム終了\n";
}
実行結果
ファイルをオープン: output.txt
ファイルをクローズ
プログラム終了

FileWriterオブジェクトがスコープを抜けた瞬間に必ずファイルが閉じられるため、うっかりcloseを呼び忘れてファイルが開きっぱなしになる心配がなくなります。

このように、デストラクタを上手く利用すると「片付け忘れしない、安全なコード」を書きやすくなります。

デストラクタを定義するときの注意点

仮想デストラクタ(ポリモーフィズムでの必須ポイント)

継承関係でポリモーフィズムを使う場合、基底クラスのデストラクタは仮想関数にするのが重要です。

そうしないと、Base*型のポインタからdeleteしたときに、派生クラス側のデストラクタが呼ばれません。

C++
#include <iostream>

class Base {
public:
    // 仮想デストラクタにしておく
    virtual ~Base() {
        std::cout << "Base デストラクタ\n";
    }
};

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived デストラクタ\n";
    }
};

int main() {
    Base* ptr = new Derived();  // 実体はDerived
    delete ptr;                 // 仮想デストラクタならDerivedのデストラクタも呼ばれる
}
実行結果
Derived デストラクタ
Base デストラクタ

もしBaseのデストラクタが仮想でないと、Derived デストラクタが呼ばれず、Derived側で確保したリソースが解放されないといった問題が起きます。

「ポリモーフィズムで使う可能性のある基底クラスのデストラクタはvirtualにしておく」というルールは、C++で非常に重要です。

デストラクタ内で例外を投げない

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

特に、スタックを巻き戻している途中(別の例外がすでに飛んでいる途中)にデストラクタからさらに例外が投げられると、std::terminateが呼ばれてプログラムが強制終了してしまいます。

どうしてもエラーを通知したい場合は、次のような方針が一般的です。

  • デストラクタ内では例外を投げず、ログ出力や内部フラグに記録する
  • 例外を投げる可能性のある処理は、デストラクタではなく通常のメンバ関数で行う

「デストラクタは絶対に投げない」と覚えておくと安全です。

まとめ

デストラクタは、C++における「後片付け専用の特別なメンバ関数」です。

クラス名の前に~を付けて定義し、スコープを抜けると自動で呼ばれます。

ローカル変数として使えばスコープ終了時に、newしたオブジェクトならdelete時に、グローバルやstaticオブジェクトならプログラム終了時に実行され、メモリやファイルなどを確実に解放できます。

RAIIの考え方でコンストラクタとデストラクタをセットにして設計することで、片付け忘れのない安全なコードにつながります。

継承時には仮想デストラクタを忘れないこと、デストラクタから例外を投げないこともあわせて意識しておくと、より安心してC++のクラス設計ができるようになります。

クラスの定義と基本

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

URLをコピーしました!