Service Locator 更安全,因其按需创建服务、统一管理生命周期并确保析构顺序;应作为 main 内局部对象而非静态变量,注册时需区分单例/每关卡/线程局部存储期,且基础服务须最后析构以避免析构期间调用已销毁服务。

全局游戏对象(比如 AudioService、SaveManager、NetworkClient)不能靠裸指针或全局变量硬写,否则极易在关卡切换、热重载或程序退出时触发析构顺序未定义、重复释放或访问已销毁对象——这类 crash 往往出现在 main 返回之后,调试器进不去,只能靠日志猜。
为什么 Service Locator 比全局变量更安全
Service Locator 本质是一个中心化注册 + 延迟获取的单例容器,它不强制对象“一启动就构造”,而是按需创建、统一管理生命周期。关键点在于:所有服务实例都由 locator 控制创建和销毁时机,避免跨编译单元静态初始化顺序问题。
-
ServiceLocator::Get<audioservice>()</audioservice>第一次调用时才构造AudioService实例,且只构造一次 - 所有服务统一在程序退出前(比如
ServiceLocator::Shutdown()被显式调用时)按反向注册顺序析构 - 服务之间可声明依赖关系(如
LoggerService必须早于NetworkClient析构),locator 可按拓扑排序保证析构顺序
如何避免 Service Locator 自身成为静态初始化地狱
把 ServiceLocator 本身做成静态变量,等于又套了一层静态依赖。正确做法是:让它成为 main() 函数内局部对象,或由主引擎类(如 GameEngine)拥有并管理其生命周期。
- 不要写
static ServiceLocator instance;在 .cpp 文件里 - 推荐写法:
int main() { ServiceLocator locator; GameEngine engine(std::move(locator)); engine.Run(); } - 这样
locator的析构时机明确在engine之后、main结束之前,所有服务都能安全析构
服务注册时必须显式指定存储期类型
不是所有服务都该用“单例语义”。有些服务需要每关卡重建(如 LevelState),有些必须线程隔离(如 InputBuffer)。注册接口要区分:
-
RegisterSingleton<audioservice>()</audioservice>→ 全局唯一,生命周期绑定 locator -
RegisterPerLevel<levelstate>()</levelstate>→ 每次调用ResetLevel()时自动销毁旧实例、新建一个 -
RegisterThreadLocal<inputbuffer>()</inputbuffer>→ 每个线程首次访问时构造独立副本,线程结束时自动析构
漏掉这个区分,后期想支持多关卡/多线程时就得推翻重写。
最容易被忽略的坑:服务析构期间调用其他服务
假设 NetworkClient::~NetworkClient() 里调用了 LoggerService::Log("disconnecting"),而 LoggerService 已经先析构了——这就是典型的析构顺序失控。解决方案只有两个:
- 在
Shutdown()阶段显式禁止跨服务调用(例如抛出断言或静默丢弃日志) - 让高频基础服务(如
LoggerService、MemoryArena)注册为“最后析构”,并在析构函数中不依赖任何其他服务
别指望编译器或运行时帮你兜底;C++ 中析构后访问已销毁对象,行为就是未定义,不会报错,只会随机崩溃或静默数据损坏。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











