用 raw 指针管理实体易导致悬空指针或重复释放,因其无所有权语义;应优先用 std::unique_ptr 独占管理,共享场景慎用 std::shared_ptr,大规模系统推荐 entityid + 查表机制。

用 raw 指针管理实体对象为什么容易出问题
直接用 Entity* 存储游戏实体(比如敌人、道具)看似简单,但极易导致悬空指针或重复释放。典型场景是:A 系统删除了 enemy1,B 系统还在用它的指针调用 update() —— 程序立刻崩溃或行为不可预测。
根本原因不是指针本身错,而是 raw 指针不携带所有权语义,编译器和运行时都不帮你检查生命周期。哪怕你加了 if (ptr) { ptr->update(); },也挡不住 ptr 指向已释放内存的“假非空”状态。
- 不要把 raw 指针当“安全引用”用,尤其跨系统(渲染、物理、AI)传递时
- 避免在容器里存裸指针(如
std::vector<entity></entity>),除非你能 100% 保证所有指针指向的对象生存期长于容器 - 如果必须用 raw 指针(比如性能敏感的 inner loop),只用于短暂访问,且确保其来源是明确的所有者(如
EntityManager::get(id)返回的引用包装器)
std::unique_ptr 是最常用的起点
当你需要一个实体由单一系统独占管理(比如关卡加载器创建并销毁本关所有敌人),std::unique_ptr<entity></entity> 是首选。它强制移动语义,杜绝浅拷贝误用,析构时自动 delete。
示例:实体管理器内部用 std::vector<:unique_ptr>></:unique_ptr> 存储,对外只提供 ID 或 const 引用:
class EntityManager {
std::vector<:unique_ptr>> entities_;
public:
EntityId create(std::unique_ptr<entity> e) {
entities_.push_back(std::move(e));
return EntityId{entities_.size() - 1};
}
Entity& get(EntityId id) { return *entities_[id.index]; }
const Entity& get(EntityId id) const { return *entities_[id.index]; }
};</entity></:unique_ptr>
-
std::unique_ptr不支持拷贝,但支持移动——这正好匹配“实体创建后移交管理权”的逻辑 - 对外暴露
Entity&而非Entity*,避免用户意外保存指针;若需临时指针,用&mgr.get(id)显式取地址 - 注意:
std::vector::erase()会触发unique_ptr析构,自动清理内存,无需手动delete
std::shared_ptr 只在真正需要共享所有权时用
比如一个特效粒子同时被渲染系统和音效系统持有:渲染要读位置,音效要读距离。这时两个系统都可能“认为自己负责生命周期”,std::shared_ptr 就比手工计数更可靠。
但代价明显:引用计数原子操作有开销;循环引用会导致内存泄漏(A 持有 B 的 shared_ptr,B 也持有 A 的)。
- 优先用
std::weak_ptr打破循环:比如 AI 系统观察目标,用weak_ptr<entity></entity>,访问前调用lock()检查是否还存活 - 避免在 hot path(如每帧 update 循环)中频繁调用
shared_ptr::lock()或构造新shared_ptr,考虑缓存或改用其他机制 - 不要用
shared_ptr管理全局唯一对象(如玩家角色),那属于设计信号:应该用单例或直接引用,而非共享所有权
实体 ID + 查表比指针更健壮
大型游戏引擎(Unity、Unreal)实际很少暴露原始指针给上层逻辑。它们用整数 ID(如 uint32_t)代替指针,运行时通过哈希表或稀疏数组查到实体数据。好处是:ID 不会悬空,序列化/网络同步天然友好,内存可重用(删除实体后 ID 可回收)。
最小可行实现可以是:
struct EntityId { uint32_t index; uint32_t generation; };
class EntityManager {
std::vector<:unique_ptr>> pool_;
std::vector<uint32_t> free_list_; // 空闲槽位索引
std::vector<uint32_t> generations_; // 每个槽位的代数
};</uint32_t></uint32_t></:unique_ptr>
- ID 中的
generation字段用来检测“野 ID”:即使槽位被复用,旧 ID 的 generation 已不匹配,查表返回空或断言失败 - 这种方案下,系统之间只传
EntityId,完全规避指针生命周期问题 - 性能关键路径(如 ECS 的 component 访问)通常进一步优化为 SOA 布局和连续内存,此时连“查表”都尽量避免,而是用 archetype + chunk 索引
指针管理的本质不是语法选择,而是厘清谁创建、谁销毁、谁持有、谁只是临时访问。从 unique_ptr 入手,再根据具体共享需求谨慎升级到 shared_ptr 或彻底转向 ID 机制,比一开始就追求“通用智能指针方案”更实际。真正的坑往往不在指针类型,而在没想清楚实体的归属边界。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











