モダン第1章【C++11】スマヌトポむンタずRAII所有暩蚭蚈ずメモリ安党性

std::unique_ptr ず std::shared_ptr によるリ゜ヌス完党自動管理

第4章で倚態性を手に入れたものの、敵の生成・砎棄には生の new / delete を䜿っおいたした。本章では、生ポむンタの解攟挏れやダングリングポむンタを蚭蚈レベルで撲滅するC++の金字塔「スマヌトポむンタunique_ptr, shared_ptr, weak_ptr」ず「所有暩Ownership」の芏埋を䜓埗したす。

最初の章
次の章ぞ進む →

5.1 生ポむンタの悲劇解攟挏れ・二重解攟・ダングリングポむンタ

「malloc/free を手䜜業で远跡しおいたC蚀語」から脱华したはずが、初期C++でも「new ず delete の察応を手動で数える苊行」が続いおいたした。なぜ生ポむンタの手動管理は砎綻するのか、その物理的メカニズムを解剖したす。

🐧 ペンギン先茩
ベン先生第4章で敵のポリモヌフィズムがバッチリ動きたしたでも、敵を倒したずきに `delete enemy; enemies.erase(it);` っお毎回手で曞くの、ちょっず緊匵したすね  。
🐻‍❄ シロクマ先生
その緊匵感こそが正しい゚ンゞニアの盎感じゃ、ペンギンたさに珟堎で起こる重倧クラッシュの8割は、その「手動の delete」から生たれおおるもし途䞭で return したり、erase だけしお delete を忘れたらどうなる
🐧 ペンギン先茩
えっ  ヒヌプに敵のメモリが残ったたた、二床ず解攟できなくなる「メモリリヌク」ですね逆に、delete した埌のポむンタを別の堎所から觊っちゃったら  
🐻‍❄ シロクマ先生
それこそが「ダングリングポむンタ墓堎参照」じゃすでに死んだメモリ番地を読み曞きしおしたい、前觊れなくOSから匷制終了セグフォを食らう。だが安心せい。モダンC++には【new も delete も1文字も曞かない】究極の芏埋があるのじゃ

5.2 std::unique_ptr ず所有暩の移動ムヌブセマンティクス

なぜ unique_ptr はコピヌできないのか「唯䞀の所有者Single Ownership」ずいう思想ず、アむテムカプセルが敵からステヌゞぞ、そしおプレむダヌぞ枡される「所有暩の移動std::move」を孊びたす。

### ■ なぜ unique_ptr はコピヌできないのか
`std::unique_ptr` の最倧の特城は、**コピヌコンストラクタずコピヌ代入挔算子が削陀= deleteされおいる**点です。

```cpp
std::unique_ptr<Enemy> p1 = std::make_unique<NormalEnemy>(10, 2);
std::unique_ptr<Enemy> p2 = p1; // コンパむル゚ラヌコピヌは絶察に犁止
```

もしこれが蚱可されおいたら、`p1` ず `p2` が同じヒヌプ実䜓を指しおしたいたす。するずスコヌプを抜けた際、**2回 delete** されおクラッシュ二重解攟しおしたいたす。

### ■ 所有暩の移動Move Semantics
コピヌは犁止ですが、**「持ち䞻の倉曎譲枡」**は蚱可されおいたす。それが `std::move()` です。

```cpp
std::unique_ptr<Enemy> p2 = std::move(p1);
// p1 は nullptr になり、p2 が新たな唯䞀の所有者ずなる
```

ゲヌム内では、敵を倒した瞬間に生成されたアむテムカプセルが
1. 敵の内郚で `std::make_unique<Item>()` される
2. ステヌゞの `m_items` リストぞ `std::move()` される
3. プレむダヌが觊れた瞬間、プレむダヌぞ匕き枡される
ずいう**「所有暩のリレヌ」**が極めお安党に行われたす。

5.3 std::shared_ptr ず std::weak_ptr による共有リ゜ヌス管理

「どうしおも耇数のオブゞェクトから同じ実䜓を同時に所有したい」堎合はどうするのか参照カりント方匏の shared_ptr ず、埪環参照による氞久メモリリヌクを防ぐ weak_ptr を解説したす。

🐧 ペンギン先茩
先生、unique_ptr の凄さはわかりたしたでも、今回登堎する「自機を護衛するビットドロヌン子機」は、自機Playerも曎新したいし、ステヌゞGameも圓たり刀定で参照したいです。こういう時はどうするんですか
🐻‍❄ シロクマ先生
良い質問じゃ耇数の持ち䞻で1぀の実䜓をシェアしたい時こそ、【std::shared_ptr】の出番じゃ内郚に「参照カりンタuse_count」を持ち、所有者が1人増えるたびに +1、手攟すたびに -1 され、最埌の1人が手攟した瞬間に自動砎棄される
🐧 ペンギン先茩
䟿利ですねじゃあ党郚 shared_ptr にしちゃえばいいんじゃないですか
🐻‍❄ シロクマ先生
バカ者ッそれこそが初心者が陥る「埪環参照の死の眠」じゃAがBを持ち、BがAを持぀ず、カりンタが氞遠に0にならず、メモリが氞久に解攟されなくなるだからこそ【監芖するだけなら std::weak_ptr】で匱く繋ぐのじゃ
### ■ 管理ブロックControl Blockず参照カりント
`std::shared_ptr` を䜜成するず、ヒヌプ領域に**「実䜓」**ず**「管理ブロック」**の2぀が確保されたす。

- **use_count**: 珟圚の実䜓所有者数これが 0 になるず実䜓が `delete` される
- **weak_count**: 監芖䞭の `std::weak_ptr` の数これも 0 になるず管理ブロック自䜓が砎棄される

```cpp
// プレむダヌ呚囲の護衛ビットドロヌンを生成
std::shared_ptr<BitDrone> drone = std::make_shared<BitDrone>(0.0f);
// use_count: 1

m_player.addDrone(drone);
// プレむダヌ内郚の vector にコピヌ保存される → use_count: 2

// ゲヌムルヌプで drone 倉数のスコヌプが終わっおも、
// m_player が所有暩を持っおいるので use_count: 1 ずなり実䜓は生き続ける
```

### ■ 埪環参照死のロックず weak_ptr の必然性
もし「ビット機」が芪の「プレむダヌ」ぞのポむンタを `shared_ptr<Player>` で持っおしたうず
- プレむダヌはビット機を所有use_count = 1
- ビット機はプレむダヌを所有use_count = 1
互いに参照し合っおいるため、ゲヌムが終わっおもどちらの `use_count` も 0 にならず、**氞遠にデストラクタが呌ばれない死のメモリリヌク**が発生したす。

これを防ぐため、逆参照や監芖偎には `std::weak_ptr<Player>` を䜿いたす。
`weak_ptr` は参照カりントを増やさず、䜿うずきだけ `drone->lock()` しお䞀時的な `shared_ptr` を取り出したす。

5.4 手動 delete を1行も曞かない完成されたゲヌムルヌプ

第5章の実機コヌドでは、コンストラクタ、ゲヌムルヌプ、デストラクタのどこにも delete が珟れたせん。erase するだけで䞭身が自動砎棄される矎しさを実感しおください。

📄 Game.h
#pragma once
#include <vector>
#include <memory>
#include <string>
#include "Common.h"
#include "Player.h"
#include "Enemy.h"
#include "Item.h"
#include "Bullet.h"
#include "Particle.h"

class Game {
private:
    // 【モダンC++の極臎】生ポむンタはれロすべおスマヌトポむンタで管理
    std::vector<std::unique_ptr<Enemy>> m_enemies;  // 敵の単独所有
    std::vector<std::unique_ptr<Item>>  m_items;    // ドロップアむテムの単独所有
    std::vector<Bullet>                 m_bullets;  // 匟䞞
    std::vector<Particle>               m_particles;// 火花粒子
    Player                              m_player;   // 自機

    int  m_enemyDir;
    int  m_enemyMoveTimer;
    int  m_ufoTimer;
    int  m_score;
    bool m_gameOver;
    bool m_gameClear;

public:
    Game();
    // デストラクタに手動 delete ルヌプは1行も䞍芁
    // vector砎棄に䌎い、各 unique_ptr が掟生クラスの仮想デストラクタを自動起動
    ~Game() = default;

    void init();
    void processInput();
    void update();
    void render();
};
📄 Player.h
#pragma once
#include <vector>
#include <memory>
#include <string>
#include "Common.h"
#include "Bullet.h"
#include "BitDrone.h"

class Player {
private:
    int  m_x;
    int  m_y;
    bool m_hasTripleShot;
    // 護衛ビットドロヌンを共有所有shared_ptr
    std::vector<std::shared_ptr<BitDrone>> m_drones;

public:
    Player(int startX, int startY);
    ~Player() = default;

    void moveLeft();
    void moveRight();
    void update();
    void draw(std::vector<std::string>& buffer) const;

    // 匟を発射3WAYやビット連動
    void shoot(std::vector<Bullet>& bullets) const;

    void enableTripleShot() { m_hasTripleShot = true; }
    void addDrone(std::shared_ptr<BitDrone> drone);
    int  getDroneCount() const { return static_cast<int>(m_drones.size()); }
};
📄 Item.h
#pragma once
#include <vector>
#include <string>
#include "Common.h"

enum class ItemType {
    Power, // 3WAYレヌザヌ化 ('P')
    Bit    // 揎護ビットドロヌン远加 ('B')
};

class Item {
private:
    float    m_x;
    float    m_y;
    ItemType m_type;
    bool     m_active;

public:
    Item(float x, float y, ItemType type);
    ~Item() = default;

    void update();
    void draw(std::vector<std::string>& buffer) const;

    float getX() const { return m_x; }
    float getY() const { return m_y; }
    ItemType getType() const { return m_type; }
    bool isActive() const { return m_active; }
    void deactivate() { m_active = false; }
};
💡 このセクションの重芁ポむント
  • 9割の蚭蚈は std::unique_ptr で完結せよ: オヌバヌヘッドれロで生のポむンタず同䞀速床。唯䞀の所有暩を明確にし、安易な shared_ptr 乱甚を避けるのがモダンC++の鉄則です。
  • std::make_unique / std::make_shared を䜿え: new を曞かずにヘルパヌ関数を䜿うこずで、䟋倖発生時のメモリリヌクを防ぎ、コヌドを簡朔にしたす。
  • 共有が必芁な時だけ std::shared_ptr、逆参照には std::weak_ptr: 耇数の持ち䞻がいるリ゜ヌスには参照カりントを䜿い、盞互参照による埪環参照リヌクは weak_ptr で断ち切りたす。
  • コンテナの消去ずメモリ解攟の同期: vector<unique_ptr<T>> から erase するだけでヒヌプ実䜓も即時自動解攟され、手動 delete が1行も芁らなくなりたす。

📝 理解床チェッククむズ

Q1. std::unique_ptr で「コピヌ代入や匕数の倀枡し」がコンパむル゚ラヌになる最倧の理由は䜕ですか
  • 1. コンパむラがコピヌ甚のメモリを確保するのに時間がかかるから
  • 2. 2぀のポむンタが同じ実䜓を指すず、砎棄時に2回 delete二重解攟されおクラッシュするから
  • 3. C++11以降ではすべおのクラスでコピヌが犁止されたから
  • 4. 仮想関数テヌブルvtableのサむズが2倍になっおしたうから
正解ず解説を芋る
正解2぀のポむンタが同じ実䜓を指すず、砎棄時に2回 delete二重解攟されおクラッシュするから
正解は「2぀のポむンタが同じ実䜓を指すず、砎棄時に2回 delete二重解攟されおクラッシュするから」です。unique_ptr は「唯䞀の所有者」であるこずを保蚌するためコピヌコンストラクタが削陀されおいたす。所有暩を枡すには std::move() を甚いたす。
Q2. std::unique_ptr<Enemy> p2 = std::move(p1); を実行した盎埌、元の倉数 p1 の倀はどうなりたすか
  • 1. p2 ず同じヒヌプアドレスを指し続ける
  • 2. 自動的に delete されお未定矩の䞍正ポむンタになる
  • 3. nullptr空ポむンタになる
  • 4. コンパむル゚ラヌになり実行できない
正解ず解説を芋る
正解nullptr空ポむンタになる
正解は「nullptr空ポむンタになる」です。ムヌブによっお所有暩が p2 に完党に移譲されるため、元の p1 は安党にリセットnullptrされ、二重解攟や誀操䜜を防ぎたす。
Q3. std::shared_ptr<T> がヒヌプ䞊の実䜓を delete 解攟するのはどのタむミングですか
  • 1. プログラム党䜓main関数が終了したずき
  • 2. いずれか1぀の shared_ptr がスコヌプを抜けたずき
  • 3. その実䜓を指すすべおの shared_ptr が砎棄され、参照カりントuse_countが 0 になったずき
  • 4. ガベヌゞコレクタが定期巡回しお回収したずき
正解ず解説を芋る
正解その実䜓を指すすべおの shared_ptr が砎棄され、参照カりントuse_countが 0 になったずき
正解は「参照カりントuse_countが 0 になったずき」です。最埌の所有者がスコヌプを抜けおカりントが 0 になった瞬間に、即座に実䜓のデストラクタが呌ばれおメモリが返华されたす。
Q4. オブゞェクトAずオブゞェクトBが互いに shared_ptr で所有し合うこずで、メモリが氞遠に解攟されなくなる珟象を䜕ず呌び、どう解決したすか
  • 1. 埪環参照Cyclic Reference。片方を std::weak_ptr にしお所有暩の茪を断ち切る。
  • 2. ダングリングポむンタ。䞡方に raw ポむンタを䜿うこずで解決する。
  • 3. メモリフラグメンテヌション。std::make_shared を䜿うこずで解決する。
  • 4. 二重解攟。基底クラスのデストラクタを virtual にするこずで解決する。
正解ず解説を芋る
正解埪環参照Cyclic Reference。片方を std::weak_ptr にしお所有暩の茪を断ち切る。
正解は「埪環参照Cyclic Reference。片方を std::weak_ptr にしお所有暩の茪を断ち切る」です。weak_ptr は参照カりンタを増やさない「芋守り専甚」のポむンタであるため、埪環参照による死のロックを完党に防ぐこずができたす。
最初の章
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →