文章目录
- 25. 单例模式
- 25.1. 饿汉式单例模式
- 25.2. 懒汉式单例模式
- 25.2.1. 解决方案1
- 25.2.2. 解决方案2 (推荐写法)
运行在VS2022,x86,Debug下。
25. 单例模式
-
单例即该类只能有一个实例。
-
应用:如在游戏开发中,可以使用单例模式来管理各种资源,确保这些资源在整个游戏中只被加载和管理一次。
-
实现
- 将构造函数和析构函数私有,不允许外部构造或析构类对象。
- 将拷贝构造函数、赋值运算符、移动构造函数和移动赋值运算符删除,不允许复制或移动类对象。
- 需要有一个静态函数接口,返回唯一的静态实例。
-
分类
- 饿汉式单例模式:在main()开始前,实例就已经存在了。
- 懒汉式单例模式:在第一次调用获取实例时才创建实例。
25.1. 饿汉式单例模式
- 代码如下。
class Singleton
{
private:
Singleton() {} //私有构造函数
~Singleton() {} //私有析构函数
Singleton(const Singleton&) = delete; //删除拷贝构造函数
Singleton& operator=(const Singleton&) = delete; //删除赋值运算符
Singleton(Singleton&&) = delete; //删除移动构造函数
Singleton & operator=(Singleton&&) = delete; //删除移动赋值运算符
private:
static Singleton instance; //静态成员变量,存储实例
public:
static Singleton* getInstance() //静态成员函数,获取实例
{
return &instance;
}
};
Singleton Singleton::instance; //静态成员变量实例化
int main()
{
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
return 0;
}
- 在main()处设置断点,监视窗口如下,实例的内存地址为0x00E0C138。
- 总结
- 优点:线程安全,因为程序运行时就已经生成唯一的实例。
- 缺点:不是按需创建实例。
25.2. 懒汉式单例模式
- 新增一个静态函数接口,用于释放实例内存。
- 代码如下。
class Singleton
{
private:
Singleton(){} //私有构造函数
~Singleton(){} //私有析构函数
Singleton(const Singleton&) = delete; //删除拷贝构造函数
Singleton& operator=(const Singleton&) = delete; //删除赋值运算符
Singleton(Singleton&&) = delete; //删除移动构造函数
Singleton & operator=(Singleton&&) = delete; //删除移动赋值运算符
private:
static Singleton* instance; //静态成员变量,存储实例
public:
static Singleton* getInstance() //静态成员函数,获取实例
{
if (instance == nullptr)
instance = new Singleton();
return instance;
}
static void deleteInstance() //静态成员函数,释放实例
{
if (instance != nullptr)
{
delete instance;
instance = nullptr;
}
}
};
Singleton* Singleton::instance = nullptr; //静态成员变量定义和初始化
int main()
{
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
Singleton::deleteInstance();
return 0;
}
- 在main()处设置断点,监视窗口如下,此时未生成实例。
- 第一次调用getInstance()获取实例时,生成实例,实例的内存地址为0x00E16A08。
- 总结
- 优点:按需创建实例。
- 缺点:
- 不是线程安全,如两个线程同时调用getInstance()获取实例,同时运行到判断instance是否为nullptr的if语句,并且instance并未创建,那么两个线程都会创建一个实例。
- 内存释放问题,在程序执行结束时,调用deleteInstance()释放实例内存,但是要确保delete之后,没有代码再调用getInstance()或者访问已释放的内存,存在安全隐患。
25.2.1. 解决方案1
- 针对线程安全问题,加锁。
- 针对内存释放问题,对于全局变量或静态变量,main()返回后会调用析构函数。基于此,可以定义一个静态成员变量,用于释放实例内存。
- 代码如下。
mutex m;
class Singleton
{
private:
Singleton() {} //私有构造函数
~Singleton() {} //私有析构函数
Singleton(const Singleton&) = delete; //删除拷贝构造函数
Singleton& operator=(const Singleton&) = delete; //删除赋值运算符
Singleton(Singleton&&) = delete; //删除移动构造函数
Singleton& operator=(Singleton&&) = delete; //删除移动赋值运算符
static Singleton* instance; //静态成员变量,存储实例
class Garbo //在析构函数中释放实例
{
public:
~Garbo()
{
if (Singleton::instance != nullptr)
{
delete instance;
instance = nullptr;
}
}
};
static Garbo garbo; //静态成员变量,在程序执行结束时,系统会调用它的析构函数
public:
static Singleton* getInstance() //静态成员函数,获取实例
{
if (instance == nullptr) //加锁前判断,这样如果实例存在,就不需加锁了
{
lock_guard<mutex>lock(m); //创建实例前加锁
if (instance == nullptr)
instance = new Singleton();
}
return instance;
}
};
Singleton* Singleton::instance = nullptr; //静态成员变量定义和初始化
int main()
{
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
return 0;
}
25.2.2. 解决方案2 (推荐写法)
- 在静态函数接口里,定义局部静态变量,存储实例。那么它会在第一次调用获取实例时才创建,可以按需创建实例。同时内部__Init_thread_header()和_Init_thread_footer()可以保证局部静态变量的初始化是线程安全的。
- 代码如下。
class Singleton
{
private:
Singleton() {} //私有构造函数
~Singleton() {} //私有析构函数
Singleton(const Singleton&) = delete; //删除拷贝构造函数
Singleton& operator=(const Singleton&) = delete; //删除赋值运算符
Singleton(Singleton&&) = delete; //删除移动构造函数
Singleton& operator=(Singleton&&) = delete; //删除移动赋值运算符
public:
static Singleton* getInstance() //静态成员函数,获取实例
{
static Singleton instance; //局部静态成员变量,存储实例
return &instance;
}
};
int main()
{
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
return 0;
}
- 反汇编分析如下。