i007.cc

i007.cc

优先队列-降维打击

05.价值资料

“shared_ptr 和 unique_ptr 怎么选

这道题考的不只是 API 用法,而是你对所有权语义的理解。从根本原则讲起。


核心问题:所有权归谁

选哪种智能指针,本质上是在回答一个问题:这个对象的生命周期由谁决定?

cpp
// 所有权的三种模式:

// 1. 独占所有权:只有一个持有者,持有者销毁则对象销毁
std::unique_ptr<Texture> texture = load("player.png");

// 2. 共享所有权:多个持有者,最后一个销毁时对象销毁
std::shared_ptr<Shader> shader = std::make_shared<Shader>("pbr.glsl");

// 3. 无所有权的观察:我想引用它,但不影响它的生命周期
std::weak_ptr<Player> target_ref = player_shared_ptr;

 

在写下 shared_ptr 之前,先问自己:这里真的是共享所有权吗?还是只是”需要在多处使用”? 这两件事不一样。


unique_ptr:零开销,应该是默认选择

内部只有一个裸指针,和裸指针完全一样大,析构时自动 delete没有任何运行时开销

cpp
// 大小对比(64位系统)
sizeof(int*)                    // 8 bytes
sizeof(std::unique_ptr<int>)    // 8 bytes,完全相同
sizeof(std::shared_ptr<int>)    // 16 bytes,两个指针

// unique_ptr 的自定义 deleter 不增加大小(如果是无状态的)
auto deleter = [](FILE* f){ fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> file(fopen("a.txt","r"), deleter);
sizeof(file);  // 仍然是 8 bytes(空 lambda 被 EBO 优化掉)

 

unique_ptr 的典型场景:

cpp
// 1. 工厂函数:明确转移所有权给调用方
std::unique_ptr<Enemy> EnemyFactory::create(EnemyType type) {
    switch(type) {
        case BOSS:   return std::make_unique<BossEnemy>();
        case MINION: return std::make_unique<MinionEnemy>();
    }
}

// 2. 类的成员:RAII 管理子对象生命周期
class Scene {
    std::unique_ptr<PhysicsWorld>  physics_;
    std::unique_ptr<RenderSystem>  renderer_;
    std::vector<std::unique_ptr<GameObject>> objects_;
    // Scene 析构时,所有子对象自动销毁,无需手动 delete
};

// 3. Pimpl 惯用法:隐藏实现细节
class AudioEngine {
    struct Impl;
    std::unique_ptr<Impl> impl_;   // 编译防火墙
public:
    AudioEngine();
    ~AudioEngine();  // 必须在 .cpp 中 default(Impl 完整定义之后)
};

 


shared_ptr:有真实代价,按需使用

shared_ptr 不只是多了引用计数那么简单,完整的开销列表:

内存布局:
  shared_ptr 对象本身 = 2 个指针(16 bytes)
    ├── ptr to object     → 指向实际对象
    └── ptr to control block → 指向控制块

  控制块(堆上单独分配):
    ├── use_count    (atomic<long>) ← 强引用计数
    ├── weak_count   (atomic<long>) ← 弱引用计数
    ├── deleter      (function)
    └── allocator    (function)

 

make_shared 的优化:把对象和控制块分配在同一块内存,减少一次 new,提升 cache locality:

cpp
// 两次内存分配(object + control block 各一次)
std::shared_ptr<Mesh> m1(new Mesh());

// 一次内存分配(object 和 control block 连续存放)
std::shared_ptr<Mesh> m2 = std::make_shared<Mesh>();
// 推荐:性能更好,也更安全(异常安全)

 

引用计数的代价:每次拷贝/销毁 shared_ptr 都是原子操作,在多核高竞争场景下代价显著:

cpp
void process(std::shared_ptr<Mesh> mesh) {  // 拷贝:原子加1
    mesh->draw();
}   // 析构:原子减1,为0则 delete

// 游戏主循环里每帧调用:
for (int i = 0; i < 10000; i++) {
    process(mesh_list[i]);   // 每次调用两次原子操作
}
// 10000 次原子操作,在高竞争时每次可能耗费几百个时钟周期

 

shared_ptr 的合理场景:

cpp
// 1. 资源真正被多个所有者共享:材质被多个 Mesh 使用
class ResourceManager {
    std::unordered_map<std::string, std::shared_ptr<Texture>> cache_;

public:
    std::shared_ptr<Texture> get(const std::string& path) {
        auto it = cache_.find(path);
        if (it != cache_.end()) return it->second;  // 共享同一份资源
        auto tex = std::make_shared<Texture>(path);
        cache_[path] = tex;
        return tex;
    }
};

// mesh_A 和 mesh_B 共享同一张贴图,任何一个存在,贴图就存在
auto tex = resource_mgr.get("grass.png");
mesh_A->set_texture(tex);
mesh_B->set_texture(tex);
// tex 离开作用域,但贴图还活着(mesh_A/B 还持有它)

// 2. 生命周期真的不确定,无法静态分析归属
// 比如:异步加载,加载完成前对象可能已经被销毁
auto weak_scene = std::weak_ptr<Scene>(current_scene);
async_load("map.bin", [weak_scene](Data data) {
    auto scene = weak_scene.lock();  // 场景还在吗?
    if (!scene) return;              // 已经切换场景了,丢弃结果
    scene->apply(data);
});

 


weak_ptr:解决两类问题

问题一:循环引用

cpp
// 错误:循环引用导致内存泄漏
struct Node {
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;   // shared_ptr 循环引用!
};
// A→B,B→A,两者引用计数永远不为 0,永远不释放

// 正确:单向 shared_ptr,反向用 weak_ptr
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node>   prev;   // 不增加引用计数
};

 

问题二:观察者不想影响生命周期

cpp
// AI 瞄准目标:AI 不应该"持有"玩家,玩家死亡后 AI 应该放弃目标
class EnemyAI {
    std::weak_ptr<Player> target_;   // 不持有,只观察

public:
    void set_target(std::shared_ptr<Player> player) {
        target_ = player;   // 不增加引用计数
    }

    void update() {
        auto target = target_.lock();  // 原子操作:安全地尝试获取
        if (!target) {
            // 玩家已死亡/离开,切换状态
            switch_to_patrol();
            return;
        }
        move_towards(target->position());
    }
};

 


游戏里最常见的错误模式

错误一:滥用 shared_ptr 作为”安全的裸指针”

cpp
// 常见错误:只是想传引用,却用 shared_ptr 拷贝
void render_mesh(std::shared_ptr<Mesh> mesh) {  // 每次调用:原子加减
    mesh->draw();
}

// 正确:不涉及所有权,传引用或裸指针
void render_mesh(const Mesh& mesh) {   // 零开销
    mesh.draw();
}
void render_mesh(Mesh* mesh) {         // 也可以,语义是"借用,不拥有"
    mesh->draw();
}

 

错误二:从 this 构造 shared_ptr

cpp
class Bullet : public std::enable_shared_from_this<Bullet> {
public:
    void register_self() {
        // 错误:new shared_ptr(this) 会创建独立的控制块,double free
        auto sp = std::shared_ptr<Bullet>(this);

        // 正确:必须继承 enable_shared_from_this 后使用
        auto sp = shared_from_this();
    }
};

 

错误三:高频对象用 shared_ptr

cpp
// 子弹、粒子:每帧创建销毁成百上千个,shared_ptr 是灾难
class ParticleSystem {
    // 错误:每个粒子用 shared_ptr
    std::vector<std::shared_ptr<Particle>> particles_;

    // 正确:用对象池 + 索引/裸指针
    std::vector<Particle> pool_;        // 预分配
    std::vector<int>      active_ids_;  // 活跃粒子的索引
};

 


决策树

需要管理对象生命周期?
    │
    ├─ 否 → 裸指针或引用(借用,不拥有)
    │
    └─ 是 → 所有权是共享的吗?
                │
                ├─ 否(单一所有者) → unique_ptr  ← 默认选择
                │
                └─ 是(多个所有者) → shared_ptr
                        │
                        └─ 有人只想观察不想拥有?→ weak_ptr

 


面试时的完整回答思路

先说原则:两者区别是所有权语义,不是”shared_ptr 更安全”。unique_ptr 应该是默认选择,shared_ptr 只在所有权真正共享时才用。

再说开销unique_ptr 零开销,shared_ptr 有控制块(16字节对象 + 堆上控制块)和原子引用计数。make_shared 把两者合并分配,是必须养成的习惯。

然后说游戏里的实践:资源管理器用 shared_ptr 缓存共享资源(贴图/着色器);场景对象用 unique_ptr;AI 目标引用用 weak_ptr;高频小对象(粒子/子弹)避开所有智能指针,用对象池。

最后如果有真实项目里用过的案例,直接说,比理论描述有力得多。

发表回复