閉じる

【C++】デストラクタの実行順序|継承・メンバ変数・静的オブジェクトの規則

C++においてメモリ管理やリソースの解放を安全に行うためには、デストラクタが「どの順番で」呼び出されるかを正確に理解しておくことが不可欠です。

デストラクタの実行順序は、基本的にはコンストラクタによる構築順序の「逆順」というシンプルな原則に従いますが、クラスの継承やメンバ変数の構成、さらには静的オブジェクトの存在によってその挙動は複雑に見えることがあります。

この記事では、初心者から中級者のエンジニアに向けて、C++のデストラクタ実行順序に関するあらゆるルールを、図解とコード例を用いて詳細に解説します。

デストラクタ実行の基本原則:後入れ先出し(LIFO)

C++におけるオブジェクトの寿命管理は、スタック構造のような「後入れ先出し(LIFO: Last-In, First-Out)」のルールが徹底されています。

これは、あるオブジェクトが別のオブジェクトに依存している可能性があるため、後に作られたもの(依存している可能性があるもの)を先に解体することで、安全性を担保するという設計思想に基づいています。

ローカルオブジェクトの解体順序

関数内で定義された通常のローカルオブジェクトは、そのスコープを抜ける際に定義された順序とは逆の順序でデストラクタが呼ばれます。

まずはこの最も基本的な挙動をコードで確認しましょう。

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

class Trace {
    std::string name;
public:
    Trace(std::string n) : name(n) {
        std::cout << name << " が構築されました。" << std::endl;
    }
    ~Trace() {
        std::cout << name << " のデストラクタが実行されました。" << std::endl;
    }
};

int main() {
    std::cout << "--- main開始 ---" << std::endl;
    
    // 定義順: obj1 -> obj2 -> obj3
    Trace obj1("Object 1");
    Trace obj2("Object 2");
    Trace obj3("Object 3");

    std::cout << "--- main終了直前 ---" << std::endl;
    return 0;
}
実行結果
--- main開始 ---
Object 1 が構築されました。
Object 2 が構築されました。
Object 3 が構築されました。
--- main終了直前 ---
Object 3 のデストラクタが実行されました。
Object 2 のデストラクタが実行されました。
Object 1 のデストラクタが実行されました。

この結果からわかる通り、main関数の終了時にObject 3Object 2Object 1の順でデストラクタが呼ばれています。

もしObject 3Object 1のリソースを参照していたとしても、Object 1が先に消えることはないため、不正アクセスを防ぐことができる仕組みになっています。

クラス継承におけるデストラクタの順序

クラスの継承関係がある場合、デストラクタの実行順序はより厳格に定義されています。

オブジェクトを構築する際は「基底クラスから派生クラスへ」と進みますが、解体する際はその逆、すなわち「派生クラスから基底クラスへ」という順序で進みます。

派生クラスから基底クラスへの遡り

継承関係におけるデストラクタの挙動を具体的なコードで見てみましょう。

ここではBaseクラスを継承したDerivedクラスを使用します。

C++
#include <iostream>

class Base {
public:
    Base() { std::cout << "Base コンストラクタ" << std::endl; }
    // 継承を利用する場合は通常 virtual にすべきですが、順序確認のため記述
    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() {
    std::cout << "--- Derivedオブジェクト生成 ---" << std::endl;
    {
        Derived d;
    } // ここでスコープを抜ける
    std::cout << "--- スコープ終了 ---" << std::endl;
    return 0;
}
実行結果
--- Derivedオブジェクト生成 ---
Base コンストラクタ
Derived コンストラクタ
Derived デストラクタ
Base デストラクタ
--- スコープ終了 ---

このように、派生クラスのデストラクタが完了した後に、自動的に基底クラスのデストラクタが呼び出されます。

これは、派生クラスが基底クラスの機能に依存しているため、先に派生クラス固有の資源を片付け、その後に基底クラスの共通資源を片付けるという論理的な順序に基づいています。

仮想デストラクタ(Virtual Destructor)の重要性

継承を扱う上で絶対に忘れてはならないのが、基底クラスのデストラクタに virtual キーワードを付与することです。

もし仮想デストラクタになっていない場合、基底クラスのポインタを介して派生クラスのオブジェクトを削除したときに、派生クラスのデストラクタが呼ばれないという重大なバグ(メモリリーク)が発生します。

状態実行されるデストラクタ結果
仮想デストラクタあり派生クラス → 基底クラス正常(リソースが全て解放される)
仮想デストラクタなし基底クラスのみ異常(派生クラスの資源が漏れる)

メンバ変数の解体順序

クラスが複数のメンバ変数を持っている場合、それらのデストラクタは「クラス定義で宣言された順序の逆」で実行されます。

ここで注意が必要なのは、コンストラクタの「初期化リスト」に書いた順序ではなく、ヘッダーファイル等で変数を宣言した順序が基準になるという点です。

メンバ変数の解体実証コード

以下の例では、クラス内部に3つのメンバ変数を持ち、それぞれの解体順序を観察します。

C++
#include <iostream>

class Member {
    char id;
public:
    Member(char i) : id(i) { std::cout << "Member " << id << " 構築" << std::endl; }
    ~Member() { std::cout << "Member " << id << " 解体" << std::endl; }
};

class Container {
    // 宣言順が重要!
    Member m1;
    Member m2;
    Member m3;
public:
    // 初期化リストの順番をわざと入れ替えてみる
    Container() : m3('C'), m2('B'), m1('A') {
        std::cout << "Container 本体構築" << std::endl;
    }
    ~Container() {
        std::cout << "Container 本体解体" << std::endl;
    }
};

int main() {
    Container c;
    return 0;
}
実行結果
Member A 構築
Member B 構築
Member C 構築
Container 本体構築
Container 本体解体
Member C 解体
Member B 解体
Member A 解体

Containerのコンストラクタ初期化リストでm3, m2, m1の順に記述していても、実際の構築は宣言順のA, B, Cで行われ、解体はその逆のC, B, Aで行われます。

このように「構築と解体は完全な鏡合わせの関係」になっています。

静的オブジェクト(static)の寿命と順序

静的オブジェクト(static)やグローバルオブジェクトは、プログラムの開始時に構築され、プログラムの終了時(main関数の終了後)に解体されます。

これらの解体順序も基本は「構築の逆順」ですが、定義場所によって挙動が異なります。

グローバルオブジェクトと静的ローカル変数

  1. グローバルオブジェクト: プログラム実行開始時に構築され、main終了後に解体されます。
  2. 静的ローカル変数: その変数が定義されている関数が最初に呼び出された時に構築され、プログラム終了時に解体されます。

静的オブジェクトの解体挙動を確認する

C++
#include <iostream>

class StaticTracker {
    const char* msg;
public:
    StaticTracker(const char* m) : msg(m) { std::cout << msg << " 構築" << std::endl; }
    ~StaticTracker() { std::cout << msg << " 解体" << std::endl; }
};

// グローバルオブジェクト
StaticTracker globalObj("Global Object");

void func() {
    // 静的ローカル変数 (最初にこの関数が呼ばれた時に構築)
    static StaticTracker localStatic("Local Static Object");
}

int main() {
    std::cout << "--- main開始 ---" << std::endl;
    func();
    std::cout << "--- main終了 ---" << std::endl;
    return 0;
}
実行結果
Global Object 構築
--- main開始 ---
Local Static Object 構築
--- main終了 ---
Local Static Object 解体
Global Object 解体

ここで注目すべきは、mainが終わった後にデストラクタが動いている点です。

また、構築されたのがGlobalLocal Staticの順であったため、解体はLocal StaticGlobalの順で行われています。

配列におけるオブジェクトの解体順序

オブジェクトの配列を生成した場合も、デストラクタの実行順序は「構築の逆順」に従います。

配列の要素はインデックス 0, 1, 2... の順に構築されるため、デストラクタはインデックスの大きい方から小さい方へ向かって呼び出されます。

C++
#include <iostream>

class Element {
    int id;
public:
    Element() : id(counter++) { std::cout << "要素 " << id << " 構築" << std::endl; }
    ~Element() { std::cout << "要素 " << id << " 解体" << std::endl; }
    static int counter;
};

int Element::counter = 0;

int main() {
    {
        std::cout << "配列生成" << std::endl;
        Element arr[3];
    }
    std::cout << "配列スコープ外" << std::endl;
    return 0;
}
実行結果
配列生成
要素 0 構築
要素 1 構築
要素 2 構築
要素 2 解体
要素 1 解体
要素 0 解体
配列スコープ外

このように、配列であってもLIFO(後入れ先出し)の原則は一切崩れません。

複数のリソースを配列で管理する場合、この順序を意識しておくことで、要素間の依存関係によるトラブルを回避できます。

デストラクタ実行順序のまとめ表

これまでの解説を整理すると、デストラクタの実行順序は以下のルールに集約されます。

対象カテゴリデストラクタの実行順序ルール
ローカル変数宣言された順序の逆順
継承関係派生クラスから始まり、基底クラスへと遡る。
メンバ変数クラス内での宣言順の逆順
静的オブジェクトプログラム全体で構築された順序の逆順(main終了後)。
配列要素インデックスの大きい方から小さい方へ。

デストラクタ設計における注意点

実行順序を理解した上で、実務でトラブルを避けるための重要なポイントが2つあります。

1. デストラクタから例外を投げない

デストラクタの実行順序が「逆順」である理由は、オブジェクト間の依存関係を安全に解消するためです。

もしデストラクタの実行中に例外が発生し、それが外部に漏れ出してしまうと、残りのオブジェクトのデストラクタが呼ばれない可能性があり、未定義動作やメモリリークの原因となります。

C++11以降、デストラクタはデフォルトで noexcept(例外を投げない)と見なされるようになっています。

2. 基底クラスのデストラクタは常に仮想化する

「継承」のセクションでも触れましたが、ポリモーフィズムを利用して派生クラスのオブジェクトを基底クラスのポインタで操作する場合、基底クラスのデストラクタが virtual でなければなりません。

これを怠ると、派生クラス側のデストラクタがスキップされるという、C++で最も有名なバグの一つに遭遇することになります。

まとめ

C++のデストラクタ実行順序は、一見複雑そうに見えて実は非常に一貫したルールに基づいています。

その核心は「構築したときと正反対の順序で壊す」という一点に尽きます。

ローカル変数なら宣言の逆順、継承なら下から上(派生から基底)、メンバ変数なら宣言の逆順といったルールは、すべて「先に作られたものが後のものの基盤になっている可能性がある」という安全策から来ています。

この挙動を正しく把握しておくことで、リソースの二重解放や不正アクセスといった、デバッグの困難な不具合を未然に防ぐことができるようになります。

オブジェクトの寿命を意識したプログラミングは、C++の真の力を引き出すための第一歩です。

日頃から「どの順番で消えるか」をイメージしながらコードを書く習慣をつけていきましょう。

クラスの定義と基本

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

URLをコピーしました!