【C++】スマートポインタ(unique_ptr/shared_ptr)とは?|new/deleteを卒業する安全なメモリ管理

C++

今回は、C++でよく出てくる「スマートポインタとは何か」について解説していきます。

「スマートポインタって何?」
new したら delete しないといけないのが面倒…」
「メモリリークやクラッシュが怖い…」

こんな疑問はありませんか?

C++で new を使うと、必ずどこかで delete をしなければなりません。ですが、delete を忘れるとメモリリーク、二重に delete するとクラッシュと、手動管理はミスの温床です。スマートポインタを使うと、この後始末を自動化できます。

この記事を読み終えると、あなたはスマートポインタの意味・unique_ptrとshared_ptrの使い分け・ゲームでの使いどころ・初心者がハマりやすいミスをしっかり理解できると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

スマートポインタとは?

スマートポインタとは、使い終わったら自動で delete してくれる、賢いポインタのことです。<memory> をインクルードすると使えます。

ふつうのポインタ(生ポインタ)は自分で delete する必要がありますが、スマートポインタはスコープを抜けると自動で後始末をしてくれます。生ポインタが不安な方はポインタの基本の記事も先に読むと理解が早いです。

なぜスマートポインタが必要?

理由は、手動の new / delete はミスが起きやすいからです。特に次のような失敗がよくあります。

  • delete を忘れる → メモリリーク
  • 同じものを2回 deleteクラッシュ
  • delete後のポインタを使う → ダングリングポインタ

スマートポインタなら、これらを仕組みで防げるので、バグがぐっと減ります。

unique_ptrの基本

std::unique_ptr は、そのオブジェクトを1人だけが所有するスマートポインタです。生成には std::make_unique を使います。

#include <iostream>
#include <memory>

struct Enemy {
    Enemy()  { std::cout << "敵を生成n"; }
    ~Enemy() { std::cout << "敵を破棄n"; }
    void Attack() { std::cout << "敵の攻撃!n"; }
};

int main() {
    // make_unique で生成(delete は不要)
    std::unique_ptr<Enemy> enemy = std::make_unique<Enemy>();
    enemy->Attack();

    // 関数を抜けると自動でデストラクタが呼ばれる
    return 0;
}
敵を生成
敵の攻撃!
敵を破棄

delete を書いていないのに「敵を破棄」が出ていますね。これがスマートポインタの自動後始末です。デストラクタの動きはコンストラクタとデストラクタの記事もあわせてどうぞ。

shared_ptrの基本

std::shared_ptr は、複数人で1つのオブジェクトを共有できるスマートポインタです。何人が使っているかを参照カウントで数え、カウントが0になった瞬間に自動で解放されます。

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> a = std::make_shared<int>(10);
    std::cout << "カウント: " << a.use_count() << "n";

    {
        std::shared_ptr<int> b = a; // 所有権を共有
        std::cout << "カウント: " << a.use_count() << "n";
    } // b が消えてカウントが減る

    std::cout << "カウント: " << a.use_count() << "n";
    return 0;
}
カウント: 1
カウント: 2
カウント: 1

基本は「所有者が1人なら unique_ptr、複数で共有するなら shared_ptr」と覚えておけばOKです。ゲームでは、あるシーンや敵を1か所が管理するなら unique_ptr、複数のシステムから参照されるリソースなら shared_ptr、という使い分けになります。

【重要】私が実際にスマートポインタで困った体験談

個人開発で敵の管理を shared_ptr にしていたとき、敵とプレイヤーがお互いを shared_ptr で持ち合っていて、メモリが一生解放されないという不具合に遭遇しました。いわゆる循環参照です。

お互いが「相手を使っている」とカウントし続けるので、参照カウントが0にならず、デストラクタが呼ばれないまま敵が増え続けてメモリを圧迫していました。解決策は、片方を std::weak_ptr(所有しない参照)にすることでした。「持ち合いになったら weak_ptr」と覚えてからは詰まらなくなりました。

スマートポインタ使用時のよくある失敗例と対処法

①生のnew/deleteと混ぜる

スマートポインタに任せた後、生ポインタでも触って delete してしまうと二重解放で落ちます。生成は make_unique / make_shared に統一しましょう。

②shared_ptrの循環参照でリーク

体験談のとおり、お互いを shared_ptr で持つと解放されません。親子や持ち合いの片側は weak_ptrにします。

③.get()した生ポインタをdeleteする

.get() で取り出した生ポインタは借りているだけです。これを delete すると二重解放になります。.get() の結果は使うだけにしましょう。

注意点

  • 生成は make_unique / make_shared に統一する
  • 持ち合い・循環になる場面では weak_ptr を使う
  • まず unique_ptr を使い、共有が必要なときだけ shared_ptr

まとめ

  • スマートポインタは自動で delete してくれる賢いポインタ
  • 所有者が1人なら unique_ptr、共有なら shared_ptr
  • 循環参照には weak_ptr で対処する
  • 生成は make_unique / make_shared に統一するとミスが減る

スマートポインタは、C++のメモリ管理を一気に安全にしてくれる強力な仕組みです。まずは unique_ptr から使ってみて、new / delete を卒業していきましょう。

関連記事