フリーキーズ | 独学プログラミング

【C++】スマートポインタ入門!unique_ptr, shared_ptr, weak_ptrを初心者にもわかりやすく解説

リンドくん

リンドくん

たなべ先生、C++の勉強でよく「スマートポインタ」って聞くんですけど、普通のポインタと何が違うんですか?

たなべ

たなべ

スマートポインタは、C++プログラマーがメモリ管理で悩まされる問題を解決してくれる優れものなんだ。
従来のポインタだと「メモリリーク」や「二重削除」といった問題が起きやすかったんだよ。

C++を学習していると、必ず出会うのが「スマートポインタ」という概念です。
従来のC++では、newでメモリを確保したら必ずdeleteで解放する必要があり、この管理が複雑で多くのバグの原因となっていました。

スマートポインタは、このメモリ管理を自動化し、より安全で効率的なプログラミングを可能にする仕組みです。
C++11以降で標準搭載されたスマートポインタには、主にunique_ptrshared_ptrweak_ptrの3種類があります。

本記事では、これらのスマートポインタの基本概念から実践的な使い方まで、初心者の方でも理解できるよう段階的に解説していきます。

プログラミング学習でお悩みの方へ

HackATAは、エンジニアを目指す方のためのプログラミング学習コーチングサービスです。 経験豊富な現役エンジニアがあなたの学習をサポートします。

✓ 質問し放題

✓ β版公開中(2025年内の特別割引)

HackATAの詳細を見る

スマートポインタとは何か?基本概念を理解しよう

リンドくん

リンドくん

そもそも、なぜ従来のポインタだとダメなんですか?

たなべ

たなべ

例えば、newでメモリを確保したけどdeleteし忘れちゃったり、逆に同じメモリを2回deleteしちゃったり...そういうヒューマンエラーが起きやすいんだよね。

従来のポインタの問題点

従来のC++でのメモリ管理には、以下のような問題がありました。

メモリリーク

void problemExample()
{
    int* ptr = new int(42);
    // 何らかの処理...
    // delete ptr; を忘れると、メモリリークが発生
}

二重削除(ダブルフリー)

int* ptr = new int(42);
delete ptr;
delete ptr;  // エラー!同じメモリを2回削除

例外安全性の問題

void riskyFunction()
{
    int* ptr = new int(42);
    someFunction();  // ここで例外が発生すると...
    delete ptr;      // この行に到達せず、メモリリーク
}

スマートポインタが解決すること

スマートポインタは、これらの問題を自動的に解決してくれます。

  • 自動的なメモリ解放 → スコープを抜けるときに自動的にdeleteされる
  • 例外安全性の保証 → 例外が発生してもメモリは適切に解放される
  • 二重削除の防止 → 適切な所有権管理により、安全な削除が保証される

RAII(Resource Acquisition Is Initialization)の概念

スマートポインタは、C++の重要な設計原則であるRAIIを実現しています。
RAIIとは、リソースの取得と初期化を同時に行い、オブジェクトの寿命とリソースの寿命を同期させる手法です。

{
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    // ここでptrが作られ、メモリが確保される
    
    // 何らかの処理...
    
}  // ここでptrのデストラクタが呼ばれ、自動的にメモリが解放される

このように、スマートポインタを使うことで、メモリ管理を意識することなく安全なプログラミングが可能になるのです。

unique_ptr - 排他的所有権を持つスマートポインタ

リンドくん

リンドくん

unique_ptrっていうのは、どういう特徴があるんですか?

たなべ

たなべ

unique_ptrは名前の通り「唯一の」ポインタなんだ。
つまり、一つのオブジェクトに対して、所有者は常に一人だけという考え方だよ。

unique_ptrの基本的な使い方

unique_ptrは、排他的所有権を持つスマートポインタです。
一つのオブジェクトに対して、同時に存在できるunique_ptrは一つだけです。

#include <memory>
#include <iostream>

int main()
{
    // unique_ptrの作成(推奨方法)
    auto ptr = std::make_unique<int>(42);
    
    // 値の確認
    std::cout << "値: " << *ptr << std::endl;  // 42
    
    // 自動的にメモリが解放される
    return 0;
}

unique_ptrの移動セマンティクス

unique_ptrはコピーできませんが、移動することは可能です。

#include <memory>
#include <iostream>

void processValue(std::unique_ptr<int> ptr)
{
    std::cout << "処理中の値: " << *ptr << std::endl;
    // 関数終了時に自動的にメモリが解放される
}

int main()
{
    auto ptr = std::make_unique<int>(42);
    
    // moveで所有権を移動
    processValue(std::move(ptr));
    
    // この時点でptrはnullptrになっている
    if (ptr == nullptr) 
    {
        std::cout << "所有権が移動しました" << std::endl;
    }
    
    return 0;
}

配列での使用

unique_ptrは配列も管理できます。

#include <memory>
#include <iostream>

int main()
{
    // 配列のunique_ptr
    auto array_ptr = std::make_unique<int[]>(5);
    
    // 配列の初期化
    for (int i = 0; i < 5; ++i) 
    {
        array_ptr[i] = i * 10;
    }
    
    // 配列の表示
    for (int i = 0; i < 5; ++i) 
    {
        std::cout << array_ptr[i] << " ";
    }
    std::cout << std::endl;
    
    // 自動的に配列が解放される
    return 0;
}

unique_ptrのメリット

  • 軽量 → 生のポインタとほぼ同じメモリ使用量
  • 例外安全 → 例外が発生してもメモリリークしない
  • 明確な所有権 → 誰がメモリを所有しているかが明確
  • 移動効率 → コピーではなく移動により、効率的な所有権の移譲が可能

unique_ptrは、単一の所有者によるリソース管理が必要な場面で最適な選択肢です。

shared_ptr - 共有所有権を持つスマートポインタ

リンドくん

リンドくん

shared_ptrは複数で共有できるんですか?どういう仕組みなんでしょう?

たなべ

たなべ

そう!shared_ptr参照カウンタという仕組みを使って、「今何人がこのオブジェクトを使っているか」を数えているんだ。
全員が使い終わったときに初めてメモリが解放されるよ。

shared_ptrの基本概念

shared_ptrは、複数のポインタが同じオブジェクトを共有できるスマートポインタです。
内部的に参照カウンタを持ち、そのオブジェクトを参照しているshared_ptrの数を管理しています。

#include <memory>
#include <iostream>

int main()
{
    // shared_ptrの作成
    auto ptr1 = std::make_shared<int>(42);
    std::cout << "参照カウント: " << ptr1.use_count() << std::endl;  // 1
    
    {
        auto ptr2 = ptr1;  // コピー(参照カウント増加)
        std::cout << "参照カウント: " << ptr1.use_count() << std::endl;  // 2
        
        auto ptr3 = ptr1;  // さらにコピー
        std::cout << "参照カウント: " << ptr1.use_count() << std::endl;  // 3
        
    }  // ptr2, ptr3がスコープを抜ける
    
    std::cout << "参照カウント: " << ptr1.use_count() << std::endl;  // 1
    
    return 0;
}  // ptr1がスコープを抜けて、メモリが解放される

実践的なshared_ptrの使用例

以下は、複数のオブジェクトが同じリソースを共有する例です。

#include <memory>
#include <iostream>
#include <vector>

class Resource 
{
public:
    Resource(int id) : id_(id) 
    {
        std::cout << "Resource " << id_ << " が作成されました" << std::endl;
    }
    
    ~Resource() 
    {
        std::cout << "Resource " << id_ << " が削除されました" << std::endl;
    }
    
    void use() 
    {
        std::cout << "Resource " << id_ << " を使用中" << std::endl;
    }
    
private:
    int id_;
};

int main()
{
    std::vector<std::shared_ptr<Resource>> users;
    
    {
        auto resource = std::make_shared<Resource>(100);
        
        // 複数のユーザーが同じリソースを共有
        users.push_back(resource);
        users.push_back(resource);
        users.push_back(resource);
        
        std::cout << "参照カウント: " << resource.use_count() << std::endl;  // 4
        
    }  // resourceはスコープを抜けるが、まだ削除されない
    
    std::cout << "resourceはまだ生きています" << std::endl;
    
    // 各ユーザーがリソースを使用
    for (auto& user : users) 
    {
        user->use();
    }
    
    users.clear();  // 全てのユーザーがリソースを解放
    std::cout << "リソースが削除されました" << std::endl;
    
    return 0;
}

shared_ptrの注意点

循環参照の問題

#include <memory>
#include <iostream>

class Node 
{
public:
    std::shared_ptr<Node> next;
    
    ~Node() 
    {
        std::cout << "Nodeが削除されました" << std::endl;
    }
};

int main()
{
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    // 循環参照を作成(これは問題!)
    node1->next = node2;
    node2->next = node1;
    
    // この状態では、メモリリークが発生する可能性がある
    return 0;
}

この循環参照の問題を解決するのが、次に説明するweak_ptrです。

weak_ptr - 循環参照を解決する弱参照ポインタ

リンドくん

リンドくん

循環参照っていうのがよくわからないんですが...

たなべ

たなべ

例えば、AがBを参照して、BがAを参照している状況だね。
お互いに「相手がいるから自分は削除されない」って言い合ってて、永遠に削除されない状況になっちゃうんだ。

weak_ptrの基本概念

weak_ptrは、shared_ptrが管理するオブジェクトに対する弱い参照を提供します。
重要な点は、weak_ptrは参照カウントに影響を与えないということです。

#include <memory>
#include <iostream>

int main()
{
    std::weak_ptr<int> weak;
    
    {
        auto shared = std::make_shared<int>(42);
        weak = shared;  // weak_ptrに代入
        
        std::cout << "shared参照カウント: " << shared.use_count() << std::endl;  // 1
        std::cout << "weakは有効: " << !weak.expired() << std::endl;  // true
        
    }  // sharedがスコープを抜ける → オブジェクトが削除される
    
    std::cout << "weakは有効: " << !weak.expired() << std::endl;  // false
    
    return 0;
}

weak_ptrの安全な使用方法

weak_ptrから実際にオブジェクトにアクセスするには、lock()メソッドを使用します。

#include <memory>
#include <iostream>

int main()
{
    auto shared = std::make_shared<int>(42);
    std::weak_ptr<int> weak = shared;
    
    // weak_ptrから安全にアクセス
    if (auto locked = weak.lock()) 
    {
        std::cout << "値: " << *locked << std::endl;  // 42
        std::cout << "参照カウント: " << locked.use_count() << std::endl;  // 2
    } 
    else 
    {
        std::cout << "オブジェクトは既に削除されています" << std::endl;
    }
    
    return 0;
}

循環参照の解決例

先ほどの循環参照問題をweak_ptrで解決してみましょう。

#include <memory>
#include <iostream>

class Node 
{
public:
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> parent;  // 親への参照はweak_ptrで
    int value;
    
    Node(int val) : value(val) 
    {
        std::cout << "Node(" << value << ") が作成されました" << std::endl;
    }
    
    ~Node() 
    {
        std::cout << "Node(" << value << ") が削除されました" << std::endl;
    }
    
    void printParent() 
    {
        if (auto p = parent.lock()) 
        {
            std::cout << "親ノードの値: " << p->value << std::endl;
        } 
        else 
        {
            std::cout << "親ノードはありません" << std::endl;
        }
    }
};

int main()
{
    auto parent = std::make_shared<Node>(1);
    auto child = std::make_shared<Node>(2);
    
    // 親から子への参照(shared_ptr)
    parent->next = child;
    
    // 子から親への参照(weak_ptr)
    child->parent = parent;
    
    child->printParent();  // 親ノードの値: 1
    
    return 0;
}  // 適切にメモリが解放される

weak_ptrの適用場面

  • キャッシュの実装 → オブジェクトが他の場所で使われていなければ削除される
  • 観察者パターン → 観察される側が観察者を弱参照で持つ
  • 親子関係のあるデータ構造 → 子から親への参照

weak_ptrを使うことで、メモリリークを防ぎながら柔軟な参照関係を構築できます。

使い分けとベストプラクティス

リンドくん

リンドくん

3つのスマートポインタ、どういう基準で使い分けたらいいんでしょうか?

たなべ

たなべ

基本的には所有権の関係で決めるんだ。
一人だけが所有」ならunique_ptr、「みんなで共有」ならshared_ptr、「見てるだけ」ならweak_ptrという感じだね。

スマートポインタの選択指針

unique_ptr を選ぶべき場合

  • リソースの所有者が明確に一つだけの場合
  • パフォーマンスを重視する場合
  • ファクトリ関数の戻り値として使用する場合
// ファクトリー関数の例
std::unique_ptr<Database> createDatabase(const std::string& type)
{
    if (type == "sqlite") 
    {
        return std::make_unique<SQLiteDatabase>();
    } 
    else if (type == "mysql") 
    {
        return std::make_unique<MySQLDatabase>();
    }
    return nullptr;
}

shared_ptr を選ぶべき場合

  • 複数のオブジェクトが同じリソースを共有する必要がある場合
  • オブジェクトの寿命が複雑で、いつ削除すべきかが明確でない場合
class TextureManager 
{
private:
    std::map<std::string, std::shared_ptr<Texture>> textures_;
    
public:
    std::shared_ptr<Texture> getTexture(const std::string& filename) 
    {
        auto it = textures_.find(filename);
        if (it != textures_.end()) 
        {
            return it->second;  // 既存のテクスチャを共有
        }
        
        auto texture = std::make_shared<Texture>(filename);
        textures_[filename] = texture;
        return texture;
    }
};

weak_ptr を選ぶべき場合

  • 循環参照を避けたい場合
  • キャッシュやオプショナルな参照として使用する場合

パフォーマンスの考慮事項

コストの比較

  • unique_ptr = ほぼゼロコスト(生ポインタとほぼ同等)
  • shared_ptr = 参照カウントの管理によるオーバーヘッドあり
  • weak_ptr = shared_ptrと併用時のみ使用可能

推奨されるコーディングパターン

class GameEngine 
{
private:
    std::unique_ptr<Renderer> renderer_;        // エンジンが単独所有
    std::shared_ptr<AudioSystem> audioSystem_; // 複数システムで共有
    std::weak_ptr<Scene> currentScene_;        // シーンへの弱参照

public:
    GameEngine() 
    {
        renderer_ = std::make_unique<Renderer>();
        audioSystem_ = std::make_shared<AudioSystem>();
    }
    
    void setCurrentScene(std::shared_ptr<Scene> scene) 
    {
        currentScene_ = scene;
    }
    
    void update() 
    {
        if (auto scene = currentScene_.lock()) 
        {
            scene->update();
        }
    }
};

よくある間違いと対策

make_uniqueとmake_sharedを使用する

// 推奨されない方法
std::unique_ptr<int> ptr(new int(42));

// 推奨される方法
auto ptr = std::make_unique<int>(42);

生ポインタとの混在を避ける

// 危険な使用方法
int* raw_ptr = new int(42);
std::shared_ptr<int> shared1(raw_ptr);
std::shared_ptr<int> shared2(raw_ptr);  // 二重削除のリスク!

// 安全な使用方法
auto shared1 = std::make_shared<int>(42);
auto shared2 = shared1;  // 適切なコピー

適切なスマートポインタの選択により、メモリ安全性を確保しながら効率的なプログラムを作成できます。

まとめ

リンドくん

リンドくん

スマートポインタって最初は複雑に感じたけど、メモリ管理が自動化されるなら使わない手はないですね!

たなべ

たなべ

その通り!現代のC++開発ではスマートポインタは必須のスキルなんだ。
最初は慣れないかもしれないけど、使い慣れると生ポインタには戻れなくなるよ。

この記事では、C++のスマートポインタについて基本概念から実践的な使い方まで詳しく解説してきました。

重要なポイントの振り返り

  • unique_ptr → 排他的所有権、軽量で効率的
  • shared_ptr → 共有所有権、参照カウントによる自動管理
  • weak_ptr → 弱参照、循環参照の問題を解決

スマートポインタのメリット

  • メモリリークの防止
  • 例外安全性の向上
  • 明確な所有権の表現
  • 自動的なリソース管理

スマートポインタは、C++11以降の現代的なプログラミングにおいて欠かせない技術です。
最初は従来のポインタに慣れている方には違和感があるかもしれませんが、安全で保守性の高いコードを書くためには必須のスキルと言えるでしょう。

実際のプロジェクトでスマートポインタを活用することで、メモリ管理の悩みから解放され、より本質的なプログラムロジックに集中できるようになります。
ぜひ今日からあなたのC++プログラムにスマートポインタを取り入れて、より安全で効率的な開発を実践してみてください。

この記事をシェア

関連するコンテンツ