スマートポインタ: メモリ管理の自動化

現代のC++開発において、生のポインタ(Raw Pointer)を使うことは稀です。スマートポインタを使ってメモリリークを撲滅しましょう。

スマートポインタ
ポインタの振る舞いを模倣しつつ、自動メモリ管理(RAII)機能を持つクラスオブジェクト。
std::unique_ptr
排他的所有権を持つスマートポインタ。コピー不可、ムーブのみ可能。最も高速。
std::shared_ptr
共有所有権を持つスマートポインタ。参照カウントで寿命を管理する。
std::weak_ptr
shared_ptrの循環参照を防ぐための、所有権を持たない弱参照ポインタ。
RAII
Resource Acquisition Is Initialization。リソースの取得と初期化、解放と破棄を紐付けるC++の慣習。

詳細解説

鍵の種類の違い (Key Types)

スマートポインタは「部屋の鍵」のようなものです。 **unique_ptr**: 「伝説の鍵」。世界に一つしかありません。誰かに渡すなら、自分の手元からはなくなります(所有権の移動)。 **shared_ptr**: 「合鍵」。何人でも持てます。全員が部屋を出て、最後の鍵が返却されたとき初めて部屋が片付けられます。 **weak_ptr**: 「のぞき穴」。部屋の中を見ることはできますが、鍵ではないので、部屋が片付けられるのを止める権限はありません。

なぜ生ポインタではダメなのか?

従来のポインタ(生ポインタ)では、プログラマが手動で `delete` を呼ぶ必要がありました。しかし、これは以下の問題を引き起こします。

  • メモリリーク: `delete` を書き忘れる、または例外発生時に `delete` が実行されない。
  • 二重解放: 同じメモリを2回 `delete` してしまう。
  • ダングリングポインタ: 解放済みのメモリにアクセスしてしまう。

スマートポインタは、変数のスコープ(生存期間)が終わると自動的にデストラクタが呼ばれ、メモリを解放してくれるため、これらの問題を根本から解決します。

3つのポインタ比較

種類 所有権 コピー オーバーヘッド 主な用途
std::unique_ptr 排他的(1人だけ) 不可(ムーブのみ) ほぼゼロ 基本はこれ。所有権が明確な場合。
std::shared_ptr 共有(複数人) 可能 あり(参照カウント) 複数のオブジェクトでリソースを共有したい場合。
std::weak_ptr なし(弱参照) 可能 あり 循環参照の防止。キャッシュの実装。

1. std::unique_ptr

std::unique_ptr Example
// unique_ptr: 唯一の所有者
// std::make_unique を使うのが一般的(C++14以降)
std::unique_ptr<int> p1 = std::make_unique<int>(100);
// コピーはできない(コンパイルエラー)
// std::unique_ptr<int> p2 = p1;
// 所有権の移動(ムーブ)は可能
std::unique_ptr<int> p3 = std::move(p1);
// p1 はもう空っぽ(nullptr)
if (!p1) {
std::cout << "p1 is empty" << std::endl;
}
// スコープを抜けると自動的にメモリ解放される

2. std::shared_ptr

std::shared_ptr Example
// shared_ptr: 共有所有者
// 参照カウント方式で管理される
std::shared_ptr<int> sp1 = std::make_shared<int>(200);
{
// コピー可能。参照カウントが増える (+1)
std::shared_ptr<int> sp2 = sp1;
std::cout << "Count: " << sp1.use_count() << std::endl; // Output: 2
}
// sp2 がスコープを抜けたのでカウントが減る (-1)
std::cout << "Count: " << sp1.use_count() << std::endl; // Output: 1
// 最後の所有者 (sp1) が消えるときにメモリ解放

3. std::weak_ptr

std::weak_ptr Example
// weak_ptr: 所有権を持たない観測者
std::shared_ptr<int> sp = std::make_shared<int>(300);
std::weak_ptr<int> wp = sp; // 参照カウントは増えない
// 中身にアクセスするには lock() して shared_ptr に昇格させる
if (std::shared_ptr<int> locked_sp = wp.lock()) {
// 参照先が生きていればここに入れる
std::cout << *locked_sp << std::endl;
} else {
// すでに解放されていた場合
std::cout << "Expired" << std::endl;
}

実践テクニック

循環参照に注意

`shared_ptr` 同士が互いを参照し合うと、参照カウントが永遠に0にならず、メモリリークが発生します(循環参照)。これを防ぐために、片方を `weak_ptr` にします。親子関係がある場合、親から子へは `shared_ptr`(または `unique_ptr`)、子から親へは `weak_ptr` を使うのが定石です。

演習課題

課題1: 所有権の移動
`std::unique_ptr` を作成し、それを別の関数に渡して、元の変数からはアクセスできなくなることを確認してください。
課題2: 共有カウンタ
`std::shared_ptr` を使って、複数の変数で一つの整数値を共有してください。変数がスコープを抜けるたびに `use_count()` が減ることを確認してください。

合格ライン

基本的には `unique_ptr` を使うべき理由を知っている
`shared_ptr` の循環参照問題と `weak_ptr` での解決法を理解している
`new` や `delete` を直接使わない(`make_unique`を使う)

参考リンク