单例模式只涉及到一个单一的类,该类让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。
借用https://refactoringguru.cn/design-patterns/singleton中的图就是:

单例模式包含如下角色:
单例模式优点:
单例模式缺点:
单例设计模式分为两种:
饿汉模式的优点是线程安全,但它的一个很明显缺点是单例创建后不一定会立即被使用,会造成一定的内存浪费。
除此以外,饿汉模式还有一个潜在的问题,那就是如果程序中有多个单例类,它们会面临静态初始化顺序问题,即全局变量会在 main 函数之前初始化完成,但是多个全局变量之间并没有确定的初始化顺序。这在面对嵌套单例时更为明显,外层单例类很有可能先于内层单例类构造,我们在编写代码时也不能假设某个全局变量在另一个全局变量之前初始化完成。
#include /* 通过静态变量实现的单例类 */
class Cat {
private:/* 让构造函数私有以避免类被实例化 */Cat() = default;/* 类对应的唯一的对象 */static Cat *m_instance;public:/* 实例对象的唯一访问方式 */static Cat *getInstance() {return m_instance;}
};/* 静态成员变量需要类内定义类外初始化 */
Cat *Cat::m_instance = new Cat;int main() {Cat *dragonLi = Cat::getInstance();Cat *ragdoll = Cat::getInstance();std::cout << dragonLi << std::endl;std::cout << ragdoll << std::endl;delete dragonLi;return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
0x6000007dc040
0x6000007dc040
atreus@MacBook-Pro %
#include using namespace std;/* 通过静态变量实现的单例类 */
class Cat {
private:static Cat *instance; // 单例类对应的唯一的对象/* 让构造函数私有以避免类被实例化 */Cat() {cout << "Cat()" << endl;}public:/* 实例对象的唯一访问方式 */static Cat *getInstance() {// 多线程场景下此处需要加锁instance = (instance == nullptr) ? new Cat : instance;return instance;}~Cat() {cout << "~Cat()" << endl;}
};/* 静态成员变量需要类内定义类外初始化 */
Cat *Cat::instance = nullptr;int main() {Cat *dragonLi = Cat::getInstance();Cat *ragdoll = Cat::getInstance();std::cout << dragonLi << std::endl;std::cout << ragdoll << std::endl;delete dragonLi;// delete ragdoll; // 由于两个猫咪实际上是一个单例 所以会发生内存的重复释放return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
Cat()
0x600001044040
0x600001044040
~Cat()
atreus@MacBook-Pro %
这种实现方法主要存在两个问题:
对于内存安全的问题,一种解决方案是将单例模式与 RAII 机制结合使用,通过增加一个 RAII 类来保证内存安全;另一种更简洁的解决方法则是基于静态局部变量来实现单例类(Meyers Singleton)。
/* 以 RAII 方式来控制单例 */
class CatStore {
public:Cat *m_cat;explicit CatStore(Cat *cat) {m_cat = cat;}~CatStore() {delete m_cat;}Cat *getCat() const {return m_cat;}
};int main() {CatStore catStore(Cat::getInstance());Cat *dragonLi = catStore.getCat();Cat *ragdoll = catStore.getCat();std::cout << dragonLi << std::endl;std::cout << ragdoll << std::endl;return 0;
}
#include using namespace std;/* 通过静态变量实现的单例类 */
class Cat {
private:/* 让构造函数私有以避免类被实例化 */Cat() {cout << "Cat()" << endl;}~Cat() {cout << "~Cat()" << endl;}public:/* 实例对象的唯一访问方式 */static Cat *getInstance() {static Cat instance; // 静态成员函数里的静态局部变量等效于类的静态成员变量return &instance;}
};int main() {{Cat *dragonLi = Cat::getInstance();std::cout << dragonLi << std::endl;}Cat *ragdoll = Cat::getInstance();std::cout << ragdoll << std::endl;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
Cat()
0x100e10000
0x100e10000
~Cat()
atreus@MacBook-Pro %
首先,对于静态局部变量来说,static 关键字并没有改变它的局部作用域,当定义它的函数或者语句块结束时(如上图中的大括号结束),作用域也随之结束。
但是当静态局部变量离开作用域后,它并没有销毁,仍然驻留在内存当中,只是暂时无法被访问,只要我们再次调用 getInstance() 方法,就能重新得到这个静态局部变量。
当程序结束时,该静态局部变量的内存会被自动释放,单例类也会被自动析构,因此内存安全得以保证,上面主函数的执行结果也印证了这一点。
其次,对于线程安全的问题,GCC 等编译器已经支持了静态变量构造和析构函数的多线程安全。以构造函数为例,对于局部静态变量,多线程调用时,首先构造静态变量的线程先加锁,其他线程等锁,因此线程安全也得以保证。
实际上,静态局部变量的多线程安全是与编译选项 -fno-threadsafe-statics 直接挂钩的,而此选项在不同编译器中都默认打开。
上一篇:【设计模式】状态模式