一、类的静态成员(static)
在介绍单例模式之前,首先需要了解静态成员变量和静态成员函数。单例模式中保证一个类只能产生一个实例,确保该类的唯一性。主要就是依赖类的静态成员函数/静态成员变量的特殊性质。
1、静态成员变量(static)
(1)我们可以使用静态成员变量来实现多个对象实例共享数据的目标。静态成员变量属于类,但不属于某个具体的类,即使创建多个对象,也只为静态成员变量分配一份内存,所有对象使用的都是这份内存的数据。当某个对象修改了静态成员变量,也会影响到其它对象实例。
(2)静态成员变量必须在类声明的外部进行初始化,声明时不需要关键字static。具体形式为:
<data_type> <class_type>::<value_name> = <true_value>。
(3)静态成员变量的内存不是在类声明时分配的,也不是在创建对象时分配的,而是在类外初始化时分配的,没有在类外初始化的静态成员变量是不能使用的。
(4)静态成员变量可以通过三种方式来访问:通过对象访问、通过对象指针访问、通过类作用域访问,三种方式等效。
(5)静态成员变量不占对象的内存,而是在所有对象之外开辟内存,即使不创建对象实例也可以访问。静态成员变量和普通的静态变量类似,都是在内存分区的全局数据区分配内存。
2、静态成员函数(static)
(1)静态成员函数相当于带有命名空间的全局函数,静态函数不需要实例化就可以被调用,不会也不可以调用或操纵非静态成员。
(2)在C++中,静态成员函数的主要目的是访问静态成员。
(3)和静态成员变量类似,静态成员函数在声明时要加 static,在定义时不能加 static。静态成员函数可以通过类来调用,也可以通过对象来调用。
(4)全局的静态函数与普通函数的区别是:用static修饰的函数,限定在本源码文件中,不能被本源码文件以外的代码文件调用。而普通的函数,默认是extern的,也就是说它可以被其它代码文件调用。
二、单例模式
单例模式主要分为懒汉式和饿汉式两种,两者之间的区别在于创建实例的时间不同:
- 懒汉式:在系统运行时,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例。(需要考虑线程安全,避免两个线程同时需要使用该实例,同时创建)
- 饿汉式:在系统一运行,就初始化创建实例,当需要使用实例时,直接调用即可。 (本身就是线程安全的)
单例模式下的类的特点:
- 构造函数和析构函数都是privtae类型的,目的是禁止外部构造和析构;
- 拷贝构造和赋值构造函数是private类型的,目的是禁止外部拷贝和赋值,确保实例的唯一性;
- 类里面有个获取实例的静态函数,可以全局访问,当其它对象需要访问该共享数据时,调用该静态函数获取。
1、懒汉式(线程不安全)
/*【单例模式指在整个系统生命周期里,保证一个类只能产生一个实例,确保该类的唯一性】*/
class SingleInstance {
public:
//静态成员函数,获取单例对象
static SingleInstance* GetInstance();
//静态成员函数,释放单例对象
static void deleteInstance();
//打印对象实例地址
void print();
private:
//将构造函数和析构函数私有化,禁止外部构造和析构
SingleInstance();
~SingleInstance();
//将其拷贝构造函数和赋值构造函数私有化,禁止外部拷贝和赋值
SingleInstance(const SingleInstance& single_instance);
const SingleInstance& operator=(const SingleInstance& single_instance);
private:
//将实例对象声明为静态成员变量,只能在第一次进行初始化
//之所以将对象设置为静态成员变量,是为了让多个线程能够共享该对象实例
static SingleInstance* m_SingleInstance;
};
//静态成员变量必须在类外进行初始化,没有初始化的静态成员变量不能使用
//初始化方式:<data_type> <class>::<data_name> = <true_data>
SingleInstance* SingleInstance::m_SingleInstance = nullptr;
/*
* 通过静态成员函数获取静态成员变量——单例对象
*/
SingleInstance* SingleInstance::GetInstance()
{
/*此处没有加锁,所以可能多个线程会同时为m_SingleInstance分配内存,造成m_SingleInstance地址不一致*/
if (m_SingleInstance == nullptr)
{
m_SingleInstance = new SingleInstance;
}
return m_SingleInstance;
}
/*
* 通过静态成员函数释放静态成员变量——单例对象
*/
void SingleInstance::deleteInstance()
{
if (m_SingleInstance != nullptr)
{
delete m_SingleInstance;
//释放后的指针必须赋空,避免出现野指针
m_SingleInstance = nullptr;
}
}
/*
* 打印对象实例的地址
*/
void SingleInstance::print()
{
cout << "我的实际地址是:" << this << endl;
}
/*
* 构造函数
*/
SingleInstance::SingleInstance()
{
cout << "我是构造函数" << endl;
}
/*
* 析构函数
*/
SingleInstance::~SingleInstance()
{
cout << "我是析构函数"<<endl;
}
/*-------------------------------------线程函数-------------------------------------*/
void PrintHello()
{
//调用单实例对象的打印函数,打印实例地址
SingleInstance::GetInstance()->print();
auto id = std::this_thread::get_id();
cout << "Hi,我是线程ID:[" << id << "]" << endl;
}
/*主函数*/
int main()
{
cout << "开始创建线程--------------------------" << endl;
for (int i = 0; i < 5; i++)
{
cout << "main() : 创建线程:[" << i << "]" << std::endl;
thread th(PrintHello);
th.join();
}
// 手动释放单实例的资源
SingleInstance::deleteInstance();
cout << "main() : 结束! " << std::endl;
return 0;
}
由于在上述代码,在使用静态成员函数GetInstance()获取单例时,没有加锁操作,所以当多个线程同时第一次需要使用该对象实例,造成多个线程同时创建该实例,导致出现线程安全问题。
2、懒汉式(加锁——线程安全)
/*【单例模式指在整个系统生命周期里,保证一个类只能产生一个实例,确保该类的唯一性】*/
class SingleInstance {
public:
//静态成员函数,获取单例对象
static SingleInstance* GetInstance();
//静态成员函数,释放单例对象
static void deleteInstance();
//打印对象实例地址
void print();
private:
//将构造函数和析构函数私有化,禁止外部构造和析构
SingleInstance();
~SingleInstance();
//将其拷贝构造函数和赋值构造函数私有化,禁止外部拷贝和赋值
SingleInstance(const SingleInstance& single_instance);
const SingleInstance& operator=(const SingleInstance& single_instance);
private:
//将实例对象声明为静态成员变量,只能在第一次进行初始化
//之所以将对象设置为静态成员变量,是为了让多个线程能够共享该对象实例
static SingleInstance* m_SingleInstance;
/*懒汉模式加锁,防止多个线程同时判断单例对象为nullptr*/
static mutex singleMutex;
};
//静态成员变量必须在类外进行初始化,没有初始化的静态成员变量不能使用
//初始化方式:<data_type> <class>::<data_name> = <true_data>
SingleInstance* SingleInstance::m_SingleInstance = nullptr;
//初始化懒汉式锁
mutex SingleInstance::singleMutex;
/*
* 通过静态成员函数获取静态成员变量——单例对象【懒汉模式加锁】
*/
SingleInstance* SingleInstance::GetInstance()
{
/*懒汉模式加锁*/
if (m_SingleInstance == nullptr)
{
//【加锁——独占锁】,只有当前线程结束,其它线程才能对该锁singleMutex进行加锁
unique_lock<mutex> lock(singleMutex);
if (m_SingleInstance == nullptr)
{
m_SingleInstance = new SingleInstance;
}
}
return m_SingleInstance;
}
上述代码中在使用静态成员函数GetInstance()获取单例时,先判断该实例是否为空,如果为空需要首次创建,则对其进行lock加锁,待当前线程使用完该实例后,自动解锁。
3、饿汉式
/*【单例模式指在整个系统生命周期里,保证一个类只能产生一个实例,确保该类的唯一性】*/
class SingleInstance {
public:
//静态成员函数,获取单例对象
static SingleInstance* GetInstance();
//静态成员函数,释放单例对象
static void deleteInstance();
//打印对象实例地址
void print();
private:
//将构造函数和析构函数私有化,禁止外部构造和析构
SingleInstance();
~SingleInstance();
//将其拷贝构造函数和赋值构造函数私有化,禁止外部拷贝和赋值
SingleInstance(const SingleInstance& single_instance);
const SingleInstance& operator=(const SingleInstance& single_instance);
private:
//将实例对象声明为静态成员变量,只能在第一次进行初始化
//之所以将对象设置为静态成员变量,是为了让多个线程能够共享该对象实例
static SingleInstance* m_SingleInstance;
};
//静态成员变量必须在类外进行初始化,没有初始化的静态成员变量不能使用
//初始化方式:<data_type> <class>::<data_name> = <true_data>
//系统一运行,立马对该静态成员变量进行初始化
SingleInstance* SingleInstance::m_SingleInstance = new SingleInstance;
/*
* 通过静态成员函数获取静态成员变量——单例对象
*/
SingleInstance* SingleInstance::GetInstance()
{
//将静态成员变量直接返回
return m_SingleInstance;
}
饿汉式在程序一开始就构造函数初始化了,所以本身就线程安全的。当后面开辟多个线程时,也是共享该实例。
4、局部静态变量(C++11 线程安全)
由于局部静态变量具有:多次调用,仅首次初始化有效;生存期为整个对象的使用期;位于全局变量区,多线程间共享。
C++11保证局部静态变量线程安全,C++98不能保证局部静态变量线程安全。
Single &Single::GetInstance()
{
// 局部静态特性的方式实现单实例
static Single signal;
return signal;
}
三、特点与选择
- 懒汉式是以时间换空间,适应于访问量较小时;推荐使用内部静态变量的懒汉单例,代码量少
- 饿汉式是以空间换时间,适应于访问量较大时,或者线程比较多的的情况