Menu

C++のデストラクタ徹底解説: ~ClassName、RAII、後始末

デストラクタはオブジェクトが破棄されるときに自動的に実行されます。~ClassName() の構文、いつ発火するか、なぜリソースを解放するのか、そして「3の法則/5の法則」を学びましょう。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

デストラクタとは

前のページでは、オブジェクトが_生まれる_ときに実行され初期状態を整える特別な関数、コンストラクタを見ました。デストラクタはその鏡像です。オブジェクトが_死ぬ_ときに実行され、その後始末をする特別な関数です。

デストラクタはクラス名の前にチルダ(~)を付けて宣言します。引数を取らず、何も返さず、クラスにちょうど1つだけ持てます。手動で呼ぶことはほとんどありません。C++が適切なタイミングで代わりに呼んでくれます。

デストラクタのメッセージが、main が関数本体を終えた_後_、プログラムが終了する前に表示されることに注目してください。log が閉じ波括弧でスコープを抜けると、C++があなたの代わりに ~Logger() を実行します。

デストラクタが実行されるタイミング

正確なタイミングは、オブジェクトがどこに存在するかによって決まります。

  • スタック(ローカル)オブジェクトは、スコープを抜けるとき、つまりブロックの閉じ } で破棄されます。
  • ヒープオブジェクト(new で生成)は、delete を呼んだときに破棄されます。delete を忘れるとデストラクタは決して実行されず、リークが発生します。

次の例はその違いを目に見える形にしています。

オブジェクトは構築とは逆の順序で破棄されます。a が最初に作られたので、最後に死にます。このLIFO(last-in, first-out、後入れ先出し)の順序は、オブジェクト同士が依存し合うときに重要になります。

デストラクタが重要な理由: RAII

デストラクタの本当の力は、後始末を_自動的かつ例外安全_にする点にあります。すべてのコードパスでリソースを解放することを覚えておく代わりに、解放処理をデストラクタに入れ、それが確実に実行されることを言語に保証させます。このパターンはRAII(Resource Acquisition Is Initialization、リソースの確保は初期化である)と呼ばれ、現代C++の屋台骨です。

ここでは、あるクラスがヒープ上のバッファを所有します。コンストラクタで確保し、デストラクタで解放するため、呼び出し側は new/delete を一切触りません。

重要な洞察はこうです。たとえ squares の生成後に例外が投げられても、スタックは巻き戻され、~IntArray() はそれでも実行されます。この保証こそがRAIIをこれほど信頼できるものにしており、良いC++コードで剥き出しの delete をめったに書かない理由でもあります。

3の法則(と5の法則)

独自のデストラクタを持つクラスは、ほぼ必ず生のリソースを所有しており、それが隠れた危険を生みます。コンパイラが生成するコピーコンストラクタとコピー代入は_浅い_コピーを行います。つまり、指している先のバッファではなく、ポインタ自体をコピーします。これで2つのオブジェクトが同じポインタを保持し、両方のデストラクタがそれを delete するため、二重解放のクラッシュを引き起こします。

IntArray a(5);
IntArray b = a;   // 浅いコピー: a.data と b.data は同じポインタ
// スコープ終了時: b のデストラクタがバッファを解放し、
// 続いて a のデストラクタが再び解放する -> 未定義動作(二重解放)

ここから3の法則が導かれます。デストラクタ、コピーコンストラクタ、コピー代入演算子のいずれか1つを書いたなら、ほぼ確実に3つすべてが必要です。C++11以降では、ムーブコンストラクタとムーブ代入を加えた5の法則へと拡張されます。

しかし、さらに良い法則があります。0の法則です。生のリソースをそもそも一切管理しなくて済むようにクラスを設計するのです。代わりに std::vectorstd::string、スマートポインタを保持すれば、コンパイラが生成するデストラクタが無料で正しく処理してくれます。

既定では0の法則に頼りましょう。独自のデストラクタを書くのは、どの標準型もラップしてくれない生のリソースを本当に所有しているときだけにしてください。

仮想デストラクタ

基底クラスのポインタ経由でオブジェクトを delete するとき、デストラクタは virtual でなければなりません。そうでないと基底部分だけが破棄され、派生部分がリークします。これはポリモーフィックなコードで最もよくあるバグの1つであり、コンパイラは既定では警告してくれません。

~Basevirtual がないと、delete p~Base() だけを呼びます。これは未定義動作であり、オブジェクトの Derived 部分は決して後始末されません。経験則: 仮想関数を持つクラス(ポリモーフィックな基底クラス)には仮想デストラクタが必要です。 クラスを派生し始めると、なぜこれが重要なのかがはっきり分かるでしょう。

よくある間違いと落とし穴

ほとんどの人がつまずく罠がいくつかあります。

new/delete の不一致。 new[] で確保したら delete[] で解放しましょう。new[] と素の delete(またはその逆)を混ぜるのは未定義動作です。

基底クラスのデストラクタで virtual を忘れる。 上で述べたとおり、仮想デストラクタなしで基底ポインタ経由で派生オブジェクトを delete すると、派生部分がリークします。継承されることを意図したクラスを書くなら、デストラクタを仮想にしましょう。

デストラクタから例外を逃がす。 スタックの巻き戻し中に例外を投げるデストラクタは、プログラムを終了させます。現代C++ではデストラクタは暗黙的に noexcept です。後始末のコードが例外を投げないようにするか、デストラクタ内で例外を握りつぶしましょう。

必要のないデストラクタを書く。 メンバがすでに自分で後始末をするなら、空の ~ClassName() {} はノイズになるだけでなく、ムーブ操作を静かに無効化することがあります。後始末すべきものが何もないなら、デストラクタは一切書かないことです。

次へ: 継承

これでオブジェクトのライフサイクル全体を見たことになります。コンストラクタが生命を吹き込み、デストラクタが後始末をし、virtual デストラクタが、あるクラスが別のクラスの上に築かれるときにもその後始末を正しく保ちます。この最後の点は、次の大きなアイデアの予告です。それが継承で、あるクラスが別のクラスのデータと振る舞いを再利用し拡張する仕組みです。次のページでは、あるクラスを別のクラスから派生させる方法、構築と破棄が階層を通じてどのように連鎖するか、そして今学んだ要素がどう噛み合うかを示します。

よくある質問

C++におけるデストラクタとは何ですか?

デストラクタは ~ClassName() という名前の特別なメンバ関数で、オブジェクトが破棄されるとき、つまりスコープを抜けるときや delete したときに自動的に実行されます。その役割は後始末です。メモリの解放、ファイルのクローズ、あるいはオブジェクトが所有するあらゆるリソースの解放を行います。引数を取らず、戻り値の型もなく、クラスに1つだけ持てます。

C++でデストラクタはいつ実行されますか?

ローカル(スタック)オブジェクトの場合、デストラクタはスコープを抜けるとき、つまり閉じ } の位置で実行されます。new で生成したヒープオブジェクトの場合は delete を呼んだときに実行されます。メンバと基底クラスはその後、構築とは逆の順序で自動的に破棄されます。

C++では必ずデストラクタを書く必要がありますか?

いいえ。クラスが自分で後始末をするメンバ(std::stringstd::vector、スマートポインタなど)だけを持つなら、コンパイラが生成するデストラクタで十分なので、自分で書く必要はありません。独自のデストラクタが必要なのは、new で確保したメモリや開いたままのファイルハンドルといった生のリソースをクラスが所有している場合だけです。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める