C++: スマートポインタ応用
最終更新:2026-08-31
レッスン45では、ムーブセマンティクスについて学びました。
ここでは、スマートポインタ——C++メモリ管理の救世主について学びます。
スマートポインタを使用すれば、
new/deleteを忘れてもメモリリークしません。
1. スマートポインタの基本
(1) 1.1 スマートポインタとは?
スマートポインタは動的メモリを管理するテンプレートクラスで、RAIIでメモリを自動解放します。
スマートポインタの種類:
| スマートポインタ | 機能 | 使用場面 |
|---|---|---|
unique_ptr |
専有所有権 | 1つのオーナーのみ |
shared_ptr |
共有所有権 | 複数のオーナー |
weak_ptr |
非所有参照 | 循環参照の解決 |
2. unique_ptr
(1) 2.1 基本的な使い方
unique_ptr は専有所有権のスマートポインタで、コピー不可、ムーブ可能です。
▶ サンプル 2:コード例(難易度 ⭐)
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> p1(new int(42));
std::cout << *p1 << std::endl; // 出力:42
// std::unique_ptr<int> p2 = p1; // ❌ エラー!コピー不可
std::unique_ptr<int> p2 = std::move(p1); // ✅ ムーブ可能
std::cout << *p2 << std::endl; // 出力:42
return 0;
} // p2が自動解放
(2) 2.2 カスタムデリータ
▶ サンプル:unique_ptrでファイル管理(難易度 ⭐⭐)
#include <iostream>
#include <memory>
#include <cstdio>
// カスタムデリータ
struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
fclose(fp);
std::cout << "ファイルを閉じました" << std::endl;
}
}
};
int main() {
std::unique_ptr<FILE, FileDeleter> file(fopen("test.txt", "w"));
// fcloseを呼ぶ必要なし、unique_ptrがFileDeleterで自動処理
return 0;
}
3. shared_ptr
(1) 3.1 基本的な使い方
shared_ptr は共有所有権のスマートポインタで、参照カウントでメモリを管理します。
▶ サンプル:shared_ptrの基本(難易度 ⭐)
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> p1 = std::make_shared<int>(42);
std::cout << "参照カウント:" << p1.use_count() << std::endl; // 1
{
std::shared_ptr<int> p2 = p1; // 共有、カウント+1
std::cout << "参照カウント:" << p1.use_count() << std::endl; // 2
} // p2が破棄、カウント-1
std::cout << "参照カウント:" << p1.use_count() << std::endl; // 1
return 0;
}
(2) 3.2 make_shared vs new
推奨: std::make_shared を使用、new は避ける。
| 特徴 | new |
make_shared |
|---|---|---|
| 例外安全 | 危険 | 安全 |
| パフォーマンス | 2回割り当て | 1回割り当て |
| コード | 長い | 簡潔 |
// 推奨
auto p1 = std::make_shared<int>(42);
// 非推奨
std::shared_ptr<int> p2(new int(42));
4. weak_ptr
(1) 4.1 なぜweak_ptrが必要?
問題: shared_ptr の循環参照は、メモリリークを引き起こします。
▶ サンプル:循環参照(難易度 ⭐⭐⭐)
#include <iostream>
#include <memory>
struct Node {
std::shared_ptr<Node> next; // 危険!
~Node() { std::cout << "Node destroyed" << std::endl; }
};
int main() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2; // n1がn2を参照
n2->next = n1; // n2がn1を参照(循環参照)
return 0;
} // ❌ n1とn2が解放されない(メモリリーク)
(2) 4.2 weak_ptrで循環参照を解決
解決策: shared_ptr を weak_ptr に変更。
struct Node {
std::weak_ptr<Node> next; // weak_ptr、参照カウントを増やさない
~Node() { std::cout << "Node destroyed" << std::endl; }
};
int main() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2; // 参照カウントに影響なし
n2->next = n1;
return 0;
} // ✅ n1とn2が正常に解放される
5. スマートポインタの選び方
(1) 5.1 選び方の基準
| 使用場面 | 推奨 |
|---|---|
| 専有所有権 | unique_ptr |
| 共有所有権 | shared_ptr |
| オブザーバー | weak_ptr (非所有ポインタ) |
| 配列 | unique_ptr<T[]> |
(2) 5.2 よくある間違い
| エラー | 説明 |
|---|---|
| 生ポインタでshared_ptrを作成 | 二重削除の危険 |
| 生ポインタとスマートポインタの混在 | RAIIの利点を失う |
| 手動でdeleteを呼ぶ | スマートポインタが管理 |
6. 実践:スマートポインタでリソース管理
▶ サンプル 1:スマートポインタでデータベース接続管理(難易度 ⭐⭐⭐)
#include <iostream>
#include <memory>
// データベース接続クラス
class DatabaseConnection {
public:
DatabaseConnection() {
std::cout << "データベース接続を開きました" << std::endl;
}
~DatabaseConnection() {
std::cout << "データベース接続を閉じました" << std::endl;
}
void query(const std::string& sql) {
std::cout << "SQL実行:" << sql << std::endl;
}
};
int main() {
// unique_ptrで管理
std::unique_ptr<DatabaseConnection> conn(new DatabaseConnection());
conn->query("SELECT * FROM users");
return 0;
} // 自動的に接続を閉じる
▶ サンプル 3:unique_ptrのカスタムデリータ(難易度 ⭐⭐)
#include <iostream>
#include <memory>
#include <cstdio>
struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
fclose(fp);
std::cout << "File closed automatically" << std::endl;
}
}
};
int main() {
// unique_ptr with custom deleter for FILE*
std::unique_ptr<FILE, FileDeleter> file(fopen("test.txt", "w"));
if (file) {
fprintf(file.get(), "Hello, Smart Pointer!");
}
// File automatically closed when file goes out of scope
return 0;
}
出力:
TEXT 📖 参照専用File closed automatically
❓ よくある質問
Q:unique_ptrとshared_ptrの使い分けは? A:
unique_ptrは所有権が1つの場合(軽量)、shared_ptrは所有権を共有する場合(オーバーヘッドあり)。デフォルトはunique_ptr。
Q:生ポインタを使う場面は? A: - 非所有アクセス(オブザーバー)ならOK
- C言語APIとの連携
- パフォーマンスが重要(ただし要注意)
Q:shared_ptrはスレッドセーフ? A:- 参照カウントの変更はスレッドセーフ
- ただしオブジェクトへのアクセスはスレッドセーフではない(ミューテックス必要)
📖 まとめ
| トピック | 要点 |
|---|---|
| unique_ptr | 専有所有権、軽量 |
| shared_ptr | 共有所有権、参照カウント |
| weak_ptr | 非所有参照、循環参照の解決 |
| make_shared | 推奨、例外安全 |
| カスタムデリータ | unique_ptrで使用、shared_ptrでも可能 |
📝 練習問題
-
初級(難易度 ⭐):
unique_ptrで動的に確保したintを管理し、make_uniqueを使用。unique_ptrをコピーしようとして(コンパイルエラー)、ムーブを使用。 -
中級(難易度 ⭐⭐):
shared_ptrで共有オブジェクトを実装。shared_ptrを複数作成し、use_count()を出力して参照カウントを観察。 -
上級(難易度 ⭐⭐⭐):
weak_ptrでshared_ptrの循環参照問題を解決。AクラスとBクラスが相互にshared_ptrを持ち、メモリリークを観察。weak_ptrで解決。
unique_ptrは専有所有権、コピー不可shared_ptrは共有所有権、参照カウントで管理weak_ptrはshared_ptrを監視、循環参照を回避make_unique/make_sharedは例外安全の推奨- カスタムデリータで特殊なリソース解放
次のレッスン:テンプレートメタプログラミング(#47)