C++のデストラクタは、オブジェクトが役目を終えたときに自動的に呼ばれ、リソースを解放するための特別な関数です。
初心者の方でも迷わないように、役割、書き方、呼ばれるタイミング、注意点、実用的な小さな例を通して、「後片付けの基礎」を丁寧に解説します。
RAII(Resource Acquisition Is Initialization)という考え方の入口として、確実に片付ける習慣を身につけましょう。
デストラクタとは? 後片付けの基礎
役割とコンストラクタとの違い
デストラクタは、クラスのオブジェクトが破棄されるときに実行される特別なメンバ関数です。
目的はリソースの回収で、動的に確保したメモリや開いたファイル、OSのハンドルなどを必ず返却します。
コンストラクタが初期化を担当するのに対し、デストラクタは対になる後片付けを担当します。
- コンストラクタ:
ClassName()
。オブジェクト生成時に呼ばれ、初期化を行います。 - デストラクタ:
~ClassName()
。オブジェクト破棄時に呼ばれ、解放を行います。
デストラクタは自動で呼ばれます。
呼び忘れが起きない仕組みがC++の強みです。
オブジェクトの寿命と後片付け
オブジェクトの寿命は生成方法により変わります。
スコープ内で作った自動変数はスコープを抜けたら破棄され、new
で作った動的オブジェクトはdelete
時に破棄されます。
静的記憶域のオブジェクト(グローバル変数や関数内static)はプログラム終了時に破棄されます。
いずれの場合もデストラクタは確実に呼ばれます。
デストラクタの書き方とルール
構文(~ClassName())の基本
デストラクタはクラス名の前にチルダ(~
)を付けて定義します。
アクセス指定は通常public
です。
#include <iostream>
class Buffer {
public:
Buffer() { // コンストラクタ
std::cout << "Buffer constructed\n";
// 実際にはメモリ確保などの初期化を行うことがあります
}
~Buffer() { // デストラクタ
std::cout << "Buffer destructed\n";
// ここでメモリやハンドルを解放します
}
};
int main() {
Buffer buf; // スコープを抜けると自動的にデストラクタが呼ばれます
std::cout << "in main\n";
}
Buffer constructed
in main
Buffer destructed
デフォルトのデストラクタ(自動生成)
デストラクタを明示的に書かなくても、コンパイラは自動生成します。
自動生成デストラクタはメンバのデストラクタを順番に呼ぶだけです。
自分で確保したメモリや開いたファイルがある場合は必ず明示的にデストラクタを定義し、後片付けを実装します。
できないこと(引数/戻り値/オーバーロード不可)
デストラクタは以下の制約があります。
- 引数を取れません。シグネチャは常に
~ClassName()
です。 - 戻り値を持てません。
- オーバーロードできません(同名の別パラメータを定義不可)。
- 例外を投げるべきではありません(詳細は後述)。投げるとプログラムが終了する危険があります。
片付ける対象の例(メモリ/ファイル/ハンドル)
デストラクタで解放すべき典型例を整理します。
対象 | 例 | 解放手段の例 |
---|---|---|
動的メモリ | new で確保した配列やオブジェクト | delete , delete[] |
ファイル | std::FILE* , OSのファイル記述子 | fclose , close |
OSハンドル/ソケット | ウィンドウハンドル, ソケットFD | OS固有のCloseHandle , closesocket |
ミューテックス/ロック | ロックの取得 | アンロック(解放) |
一時的な登録 | コールバック登録, 一時ファイル | 登録解除, 削除 |
「確保したら必ず対になる解放」をデストラクタに集約すると、ミスが減ります。
デストラクタが呼ばれるタイミング
スコープを抜けるとき(自動変数)
ブロックを抜けるとローカルオブジェクトが破棄され、デストラクタが呼ばれます。
#include <iostream>
struct Tracer {
const char* name;
Tracer(const char* n) : name(n) { std::cout << "ctor: " << name << "\n"; }
~Tracer() { std::cout << "dtor: " << name << "\n"; }
};
int main() {
std::cout << "enter main\n";
{
Tracer t("local");
std::cout << "in block\n";
} // ここでtのデストラクタが呼ばれる
std::cout << "leave main\n";
}
enter main
ctor: local
in block
dtor: local
leave main
delete 時(動的オブジェクト)
new
で作ったオブジェクトはdelete
した瞬間にデストラクタが呼ばれます。
#include <iostream>
struct Tracer {
const char* name;
Tracer(const char* n) : name(n) { std::cout << "ctor: " << name << "\n"; }
~Tracer() { std::cout << "dtor: " << name << "\n"; }
};
int main() {
Tracer* p = new Tracer("heap");
std::cout << "using heap object\n";
delete p; // ここでデストラクタが呼ばれる
}
ctor: heap
using heap object
dtor: heap
プログラム終了時(静的オブジェクト)
グローバルオブジェクトや関数内static
は、プログラム終了時に破棄されます。
#include <iostream>
struct Tracer {
const char* name;
Tracer(const char* n) : name(n) { std::cout << "ctor: " << name << "\n"; }
~Tracer() { std::cout << "dtor: " << name << "\n"; }
};
Tracer g("global"); // 静的記憶域
int main() {
std::cout << "main start\n";
static Tracer s("function-static");
std::cout << "main end\n";
}
出力例(代表的な順序):
ctor: global
main start
ctor: function-static
main end
dtor: function-static
dtor: global
メンバと配列の破棄順序
メンバは宣言順に構築、逆順に破棄されます。
配列は高いインデックスから逆順に破棄されます。
#include <iostream>
struct T {
int id;
T(int i) : id(i) { std::cout << "T ctor " << id << "\n"; }
~T() { std::cout << "T dtor " << id << "\n"; }
};
struct Holder {
T a; // 1番目に宣言
T b; // 2番目に宣言
Holder() : a(1), b(2) { std::cout << "Holder ctor\n"; }
~Holder() { std::cout << "Holder dtor\n"; }
};
int main() {
Holder h; // a→b の順に構築、b→a の順に破棄
std::cout << "--- array ---\n";
T arr[3] = { T(10), T(11), T(12) }; // 10→11→12 の順に構築
// スコープを抜けると 12→11→10 の順に破棄
}
T ctor 1
T ctor 2
Holder ctor
--- array ---
T ctor 10
T ctor 11
T ctor 12
Holder dtor
T dtor 2
T dtor 1
T dtor 12
T dtor 11
T dtor 10
継承の順序(派生→基底)
継承関係では、派生クラスのデストラクタが先、次に基底クラスのデストラクタが呼ばれます。
多態的に使う基底クラスは必ず仮想デストラクタにします。
#include <iostream>
struct Base {
virtual ~Base() { std::cout << "Base dtor\n"; } // 仮想デストラクタ
};
struct Derived : Base {
~Derived() { std::cout << "Derived dtor\n"; }
};
int main() {
Base* p = new Derived();
delete p; // Derived→Base の順にデストラクタが呼ばれる
}
Derived dtor
Base dtor
基底クラスのデストラクタが仮想でないのに、基底クラスのポインタで派生クラスをdeleteすると未定義動作です。
学習初期は「基底をポリモーフィックに使うならvirtual ~Base()
」を合言葉にしてください。
例外発生時も呼ばれる
スコープを巻き戻す際にもデストラクタは必ず呼ばれます。
これがRAIIの要です。
#include <iostream>
#include <stdexcept>
struct Guard {
Guard() { std::cout << "Guard acquire\n"; }
~Guard() { std::cout << "Guard release\n"; }
};
void work() {
Guard g; // 例外でも確実に解放される
std::cout << "do work\n";
throw std::runtime_error("oops");
}
int main() {
try {
work();
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n";
}
}
Guard acquire
do work
Guard release
caught: oops
使い方のコツとシンプルな例(C++初心者向け)
安全な後片付けの書き方
デストラクタでは以下を心がけます。
文章で流れを整理します。
まず、二重解放を避けるために、所有しているポインタやハンドルを解放後にnullptr
や無効値に設定します。
次に、例外を投げないようにし、必要なら内部でcatchしてログだけ残します。
最後に、所有権の責任を明確にし、どのクラスが解放すべきかを決めます。
迷ったら「そのクラスが自分で確保したものは自分で片付ける」というルールに従うと安全です。
ファイルを自動で閉じるクラスの例
C++標準のstd::ifstream
などは自動で閉じますが、学習のためにstd::FILE*
をラップしてみます。
スコープを抜ければ自動でfclose
されます。
#include <cstdio>
#include <stdexcept>
#include <iostream>
class FileGuard {
public:
explicit FileGuard(const char* path, const char* mode)
: fp_(std::fopen(path, mode)) {
if (!fp_) {
// ここで例外を投げると、コンストラクタ失敗になり、デストラクタは呼ばれません
throw std::runtime_error("failed to open file");
}
std::cout << "opened: " << path << "\n";
}
~FileGuard() noexcept { // 例外を投げない
if (fp_) {
std::fclose(fp_);
std::cout << "closed file\n";
fp_ = nullptr;
}
}
// 書き込みヘルパ
void writeLine(const char* s) {
std::fprintf(fp_, "%s\n", s);
std::cout << "wrote: " << s << "\n";
}
// コピー禁止(二重closeを防ぐ)
FileGuard(const FileGuard&) = delete;
FileGuard& operator=(const FileGuard&) = delete;
private:
std::FILE* fp_;
};
int main() {
try {
// スコープを抜ければ自動で閉じる
FileGuard fg("sample.txt", "w");
fg.writeLine("hello");
fg.writeLine("world");
std::cout << "done\n";
} catch (const std::exception& e) {
std::cout << "open failed: " << e.what() << "\n";
}
}
opened: sample.txt
wrote: hello
wrote: world
done
closed file
このように、「確保はコンストラクタ、解放はデストラクタ」に集約すると、例外や早期returnでも漏れなく後片付けできます。
new/delete の注意
newとdeleteの対応を間違えると未定義動作やメモリリークになります。
以下の点を押さえましょう。
new
にはdelete
、new[]
にはdelete[]
を対応させます。- 同じポインタを二度
delete
しないように、解放後はnullptr
代入が有効です。 - nullptrへの
delete
は安全なので、条件分岐は不要です。 - 多態的に扱うときは基底クラスに仮想デストラクタを付けます。
実例で配列と多態を併せて確認します。
#include <iostream>
struct Base {
virtual ~Base() { std::cout << "Base dtor\n"; }
};
struct Derived : Base {
~Derived() { std::cout << "Derived dtor\n"; }
};
int main() {
// new/delete の対応
int* a = new int(42);
delete a; // OK
Derived* arr = new Derived[2];
delete[] arr; // 配列は delete[] が必要
// 多態 + virtual dtor
Base* p = new Derived();
delete p; // Derived→Base の順で安全に破棄
}
Derived dtor
Derived dtor
Base dtor
Derived dtor
Base dtor
基底のデストラクタが仮想でない場合のBase* p = new Derived; delete p;
は未定義動作で危険です。
ここでは安全な版のみ実行しています。
まとめ
デストラクタは、C++における後片付けの自動化を担う重要機能です。
スコープ終了、delete、プログラム終了時、例外時のいずれでも確実に呼ばれ、メンバは逆順、継承は派生→基底の順で破棄されます。
書き方は~ClassName()
のみで、引数や戻り値は持てません。
自動生成デストラクタで足りないときは、メモリやファイル、ハンドルといった確保したリソースの解放を実装します。
特に多態では仮想デストラクタを忘れないでください。
RAIIの考え方で「確保はコンストラクタ、解放はデストラクタ」に徹すれば、例外やバグに強い堅牢なコードになります。