“shared_ptr 和 unique_ptr 怎么选
这道题考的不只是 API 用法,而是你对所有权语义的理解。从根本原则讲起。
核心问题:所有权归谁
选哪种智能指针,本质上是在回答一个问题:这个对象的生命周期由谁决定?
// 所有权的三种模式:
// 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,没有任何运行时开销:
// 大小对比(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 的典型场景:
// 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:
// 两次内存分配(object + control block 各一次) std::shared_ptr<Mesh> m1(new Mesh()); // 一次内存分配(object 和 control block 连续存放) std::shared_ptr<Mesh> m2 = std::make_shared<Mesh>(); // 推荐:性能更好,也更安全(异常安全)
引用计数的代价:每次拷贝/销毁 shared_ptr 都是原子操作,在多核高竞争场景下代价显著:
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 的合理场景:
// 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:解决两类问题
问题一:循环引用
// 错误:循环引用导致内存泄漏
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; // 不增加引用计数
};
问题二:观察者不想影响生命周期
// 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 作为”安全的裸指针”
// 常见错误:只是想传引用,却用 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
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
// 子弹、粒子:每帧创建销毁成百上千个,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;高频小对象(粒子/子弹)避开所有智能指针,用对象池。
最后如果有真实项目里用过的案例,直接说,比理论描述有力得多。
