C言語 › C++ › 第31回 デストラクタと RAII
第31回 デストラクタと RAII ★目玉
オブジェクトがスコープを抜けるときに自動で呼ばれる終了処理 がデストラクタ。これをリソース管理 に応用したのが C++ 最大の発明 RAII(Resource Acquisition Is Initialization) 。C の malloc/free・fopen/fclose・pthread_mutex_lock/unlock といった「対になる処理の後半を忘れる問題 」を、コンパイラに任せて根絶する仕組みです。
このページで押さえること
✅ 最低限ここだけ覚える
デストラクタ = ~ClassName() で書く
オブジェクトがスコープを抜ける瞬間 に自動で呼ばれる
RAII = 「リソース取得をコンストラクタ、解放をデストラクタ 」で管理
vector / string / unique_ptr などは全部 RAII 実装
⭐ 余裕があれば読む
スタック上とヒープ上でタイミングの違い
スコープガード(std::lock_guard 的な型)
例外安全性と RAII の関係
仮想デストラクタ(継承時、第 36 回)
目次
1. まず触ってみる ― スコープアニメ
2. デストラクタの基本
3. RAII ― C++ 最大の発明
4. 破棄の順序
5. 実戦:よくある RAII クラス
1. まず触ってみる ― スコープアニメ
下のボタンで、コードを 1 行ずつ進めてみてください。「{」でオブジェクトが作られ、「}」で自動的に 破棄される様子が見えます。
▶ オブジェクトのライフサイクル
▶ Step
⏩ Run
↺ リセット
step 0 / 10
class Greeter {
std::string name_;
public:
Greeter(std::string n) : name_(std::move(n)) { log("ctor " + name_); }
~Greeter() { log("dtor " + name_); }
};
int main() {
Greeter alice("Alice"); // ← ctor
{
Greeter bob("Bob"); // ← ctor
} // ← bob の dtor
Greeter carol("Carol"); // ← ctor
return 0;
} // ← carol, alice の dtor(逆順)
今生きているオブジェクト: (なし)
最小コード
first_dtor.cpp 最小例
class Greeter {
std::string name_;
public :
Greeter (std::string n) : name_(std::move (n)) {
std::cout << "Hello, " << name_ << "\n" ;
}
~Greeter () { // ← デストラクタ
std::cout << "Bye, " << name_ << "\n" ;
}
};
int main () {
Greeter g{"World" };
// Hello, World ← ここで ctor
} // Bye, World ← ここで dtor(自動)
ここまでで覚えること(3 つ):
デストラクタは ~ClassName()、引数・戻り値なし
スコープ終了で自動 に呼ばれる(free や fclose を手で書かなくていい)
複数のオブジェクトは作った逆順 で破棄される
よくある素朴な疑問
Q. 明示的に破棄したいときは?
→ スコープを切る({ ... } ブロックで囲む)のが定石。ヒープ割り当てなら delete(スマートポインタ推奨)、手動 dtor 呼び出しはしない のが鉄則。
Q. 例外が起きても呼ばれる?
→ 呼ばれます。 これが RAII の強みで、try/catch を書かなくてもリソースが漏れません(スタック巻き戻し)。
Q. デストラクタ書かないとどうなる?
→ コンパイラが暗黙のデストラクタ を用意する。メンバ変数(vector や string など)は自分でデストラクタを持つので連鎖的に破棄 される。なので大多数のクラスは自作デストラクタ不要 です。
2. デストラクタの基本
構文 基本
class Foo {
public :
~Foo () { // 名前はクラス名に ~ を付ける
// 終了処理
}
};
// 特徴:
// ・引数なし(オーバーロード不可)
// ・戻り値なし
// ・1 つのクラスに 1 つだけ
呼ばれるタイミング 自動
// ① ローカル変数: スコープ終了時
void f () {
Foo a;
} // ← ここで a.~Foo()
// ② new したオブジェクト: delete 時
Foo * p = new Foo ();
delete p; // ← ここで ~Foo()
// ③ コンテナの要素: コンテナ自身が管理
std::vector <Foo > v;
v.push_back (Foo {});
// v が壊れるとき要素もすべて ~Foo()
暗黙のデストラクタ
何も書かなければコンパイラが空のデストラクタ を生成します(各メンバを自動で破棄)。あなた自身のクラスでも、中身が vector / string / unique_ptr のようなメンバだけならデストラクタを書く必要はありません 。
Rule of Zero: 自作のリソース管理をせず、STL 型をメンバとして使うだけ なら、デストラクタ・コピー・ムーブのすべてがコンパイラ任せで安全。現代 C++ の最良の設計原則です(詳細は第 29 回)。
3. RAII ― C++ 最大の発明
RAII (Resource Acquisition Is Initialization) = 「資源の取得は初期化で、解放はデストラクタで 」。対になる処理を型 に閉じ込めて、忘れる・順序ミスを言語機構で防ぎます。
C の手動管理 忘れる・失敗する
FILE * fp = fopen ("data.txt" , "r" );
if (!fp) return ;
// 処理...
if (error) {
fclose (fp); // ← 忘れがち
return ;
}
// 処理...
fclose (fp); // ← すべての return で書く必要
C++ の RAII 自動・安全
std::ifstream file{"data.txt" };
if (!file) return ;
// 処理...
if (error) return ; // ← file は自動でクローズ
// 処理...
// スコープ終了で file.~ifstream() が自動で呼ばれ fclose 相当の処理
RAII の基本パターン
自作 RAII テンプレ
class FileHandle {
FILE * fp_;
public :
// 取得はコンストラクタ
explicit FileHandle (const char * path, const char * mode)
: fp_(fopen (path, mode))
{
if (!fp_) throw std::runtime_error ("open failed" );
}
// 解放はデストラクタ
~FileHandle () {
if (fp_) fclose (fp_);
}
// 通常はコピー禁止(所有権を明確にするため)
FileHandle (const FileHandle &) = delete ;
FileHandle & operator =(const FileHandle &) = delete ;
FILE * get () const { return fp_; }
};
void process () {
FileHandle f{"data.txt" , "r" };
// 処理... 例外が出ても fclose 相当は必ず走る
} // ← スコープ終了で自動 close
STL の代表的な RAII 型
std::vector / std::string ― ヒープメモリ
std::unique_ptr / std::shared_ptr ― 任意のリソース(STEP 6)
std::ifstream / std::ofstream ― ファイルハンドル
std::lock_guard / std::unique_lock ― mutex ロック
std::thread(join しないと std::terminate するが) ― スレッド
RAII の真価は「例外安全」: 例外が投げられてスタックが巻き戻されても、途中で作られたローカル変数のデストラクタは必ず呼ばれます 。だから C の goto cleanup: パターンや「エラー処理の後始末」を書く必要がありません。これが C++ が採用している例外機構の本質。
4. 破棄の順序
複数のオブジェクトが破棄される順序は決まっています。予測できるからこそ安全に使えます。
ローカル変数 ― 作成と逆順
LIFO Last In First Out
void f () {
Obj a; // ctor a
Obj b; // ctor b
Obj c; // ctor c
} // dtor c → dtor b → dtor a (逆順)
クラスメンバ ― 宣言順(コンストラクト)/逆順(デストラクト)
メンバの順 宣言順で生成、逆順で破棄
class Container {
A a_; // 1. ctor a_
B b_; // 2. ctor b_
C c_; // 3. ctor c_
}; // 破棄時は c_ → b_ → a_ (逆順)
vector の要素 ― 末尾から破棄
vector は末尾から順に 要素のデストラクタを呼びます。これはメモリアクセスの局所性を保つため。
⭐
ここまでで RAII の基本は OK
最後は実用的な RAII 型の例。応用が効くようになります。
5. 実戦:よくある RAII クラス
① std::lock_guard ― mutex の自動解放
スレッド安全 並行処理
#include <mutex>
std::mutex mtx;
void safe_inc (int & counter) {
std::lock_guard <std::mutex > lock{mtx}; // 取得
++counter;
} // ← スコープ終了で自動 unlock、例外でも安全
② ScopedTimer ― 時間計測
経過時間を自動で出力 デバッグ用
class Timer {
using clock = std::chrono::steady_clock ;
clock ::time_point start_ = clock ::now ();
std::string label_;
public :
explicit Timer (std::string l) : label_(std::move (l)) {}
~Timer () {
auto elapsed = clock ::now () - start_;
std::cout << label_ << ": "
<< std::chrono::duration_cast <std::chrono::milliseconds >(elapsed).count ()
<< " ms\n" ;
}
};
void my_function () {
Timer t{"my_function" };
// 処理...
} // 終了時に経過時間が勝手に出る
③ ScopeExit ― 任意の後始末を予約
defer 風 汎用
template <class F >
class ScopeExit {
F f_;
public :
explicit ScopeExit (F f) : f_(std::move (f)) {}
~ScopeExit () { f_(); }
};
// 使い方:
void doit () {
ScopeExit g{[]{ std::cout << "cleanup\n" ; }};
// 処理...
} // 終了時に cleanup が走る(例外でも)
モダン C++ のエッセンス: 「対になる処理(開く/閉じる、ロックする/解除する、開始する/終了する)」はすべて RAII でラップする。これが例外安全なコードを自然に書けてしまう 秘訣です。
確認クイズ
デストラクタと RAII を 4 問で確認。
Q1. デストラクタについて正しいのは?
引数を受け取れる
複数定義できる(オーバーロード)
引数なし・戻り値なし・クラスに 1 つだけ
static にできる
デストラクタは引数を取れず 、オーバーロードもできず、1 クラスに 1 つ。名前は ~ClassName()、戻り値なし。呼ばれるタイミングが決まっているので引数を渡しようがない、という理由。
Q2. 次のコードの出力として正しい順序は?{ Logger a{"A"}; Logger b{"B"}; }
ctor A, ctor B, dtor A, dtor B
ctor A, ctor B, dtor A and B 同時
ctor A, ctor B, dtor B, dtor A
ctor B, ctor A, dtor A, dtor B
順序は不定
コンストラクタは書いた順 、デストラクタは逆順 (LIFO)。この規則があるから、B が A に依存しているような場合でも、B が A より先に壊されるので安全。
Q3. RAII の核心として最も正しいのは?
クラスの継承を必須にする
メモリ確保をヒープで行うこと
資源取得をコンストラクタ、解放をデストラクタに結び付ける
例外を投げない
RAII = Resource Acquisition Is Initialization。取得(ctor)と解放(dtor)を型に閉じ込める ことで、後始末忘れや順序ミスを防ぎます。例外が飛んでもスコープ終了は起きるので、自動的に例外安全になる。
Q4. 次の関数で、例外が起きた場合にリソースが確実に解放されるのは?
void f() { FILE* fp = fopen("x","r"); /* 処理 */ fclose(fp); }
void f() { char* p = new char[100]; /* 処理 */ delete[] p; }
void f() { std::ifstream fs{"x"}; /* 処理 */ }
void f() { pthread_mutex_lock(&mtx); /* 処理 */ pthread_mutex_unlock(&mtx); }
①②④は、処理の途中で例外が投げられると fclose/delete/unlock に到達せずリソースリーク。③の std::ifstream は RAII で、スコープ終了時に自動でクローズ(例外でも)。①は std::ifstream、②は std::unique_ptr、④は std::lock_guard に置き換えるのがモダン流儀。