最稳妥的单例指针实现是使用静态局部变量(c++11起线程安全)或显式声明static指针并配合std::call_once初始化;必须初始化为nullptr、避免重复分配、禁止delete this、不提供destroy()除非明确设计可重置。

单例的指针实现必须用静态局部变量或 static 指针
直接返回栈上对象地址不行,返回堆上对象又得手动管理生命周期——所以最稳妥的方式是让指针本身静态化,且指向的对象也由静态存储期保证存活。C++11 以后推荐用静态局部变量隐式线程安全初始化,但若必须用指针(比如需要延迟构造或虚析构),就得显式声明 static 指针。
常见错误是写成:MyClass* getInstance() { static MyClass instance; return &instance; } ——这返回的是地址,不是指针类型单例;更糟的是有人写 return new MyClass() 却不记录、不释放,造成内存泄漏。
-
static指针必须初始化为nullptr,否则未定义行为 - 检查是否为
nullptr后再new,避免重复分配 - 不要在析构函数里
delete this——单例生命周期应与程序一致,析构时机不可控
线程安全必须靠 std::call_once 或 C++11 静态初始化
多个线程同时调用 getInstance() 时,仅靠 if (ptr == nullptr) 无法保证原子性,可能创建多个实例。C++11 之前常用双重检查锁定(Double-Checked Locking),但在缺少内存屏障时有隐患;C++11 起标准库提供 std::call_once 和 std::once_flag,这才是可靠做法。
错误示范:if (!ptr) ptr = new MyClass(); ——这不是原子操作,ptr 的读、判断、赋值之间可能被其他线程插入。
- 用
static std::once_flag flag配合std::call_once(flag, []{ ptr = new MyClass(); }) - 不能把
std::call_once放在类外全局作用域,必须在函数内每次调用都触发它 - 如果用静态局部变量(
static MyClass instance;),编译器自动生成线程安全初始化代码,但那是对象而非指针——要指针就得自己管
delete 必须显式控制,且通常不该在 getInstance() 里做
单例指针的释放时机是个陷阱:在 main() 返回后、全局对象析构期间调用 delete 很危险,容易引发静态析构顺序问题(Static Initialization Order Fiasco)。更常见的是用户误以为“调用一次 delete 就能重置单例”,结果后续访问空指针崩溃。
典型错误:void destroy() { delete ptr; ptr = nullptr; } 然后在别的地方再次调用 getInstance() ——此时会重新 new,但若类有静态成员或依赖全局状态,可能已损坏。
- 除非明确设计为可重置(如测试场景),否则不要提供
destroy() - 若必须销毁,应在
main()结束前、所有依赖单例的模块清理完毕后手动调用 - 用
atexit()注册销毁函数风险极高,不推荐
继承和多态下指针单例需注意 virtual 析构和内存布局
当基类单例被继承,且通过基类指针访问时,若基类析构函数非 virtual,delete 基类指针会导致派生类部分未析构。而指针单例常用于工厂或框架入口,很容易出现这种场景。
另一个坑是:不同编译单元中分别定义了 static MyClass* ptr,链接时不会报错,但每个 TU 拥有独立副本——单例就失效了。
- 基类析构函数必须声明为
virtual,哪怕为空 -
static指针定义必须放在 .cpp 文件里,不能在头文件中extern+ 定义混用 - 若用模板实现泛型单例,每个实例化类型都有独立静态指针,不是跨类型的单例
实际中最容易被忽略的,是把指针单例当成“可随时重建”的工具来用——它本质是全局状态,一旦初始化,其生命周期、线程可见性、析构顺序都脱离常规控制。写完 getInstance() 后,务必确认:有没有多线程竞争?有没有跨编译单元引用?析构是否引入依赖环?
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











