单例模式是非常简单且基础的设计模式之一。麻雀虽小五脏俱全,在单例模式的设计过程中依然涉及到众多的知识点,而且一个优秀的单例模式并不是一蹴而就完成的,其中涉及到内存泄漏、多线程以及泛型编程等知识点。因此,本文从网上搜集了些许资料,逐步从简单的单例模式出发,对其进行了不断的优化。
单例模式是指该类只能有一个对象。因此,该类的构造函数、析构函数、复制构造函数以及赋值运算符都必须是private的,并且需要提供一个创建对象的公共接口。由于在程序运行期间,这个对象必须一致存在,所以这个对象以及公共接口都必须是static性质的,只有当程序运行结束之后,static对象才会被销毁。
根据对象创建的时机划分,主要分为懒汉模式以及饿汉模式。懒汉模式是指只有当调用公共接口的时候,我们才定义这个类对象;饿汉模式是指,我们在定义类的时候(在调用公共接口之前)就已经把这个类对象给定义出来了。
1 第一版:一个简单的单例模式#include
using namespace std;
class Singleton{
private:
Singleton(){cout << "Singleton 构造函数" << endl;}
~Singleton(){
cout << "Singleton 析构函数" << endl;
}
Singleton(const Singleton& _instance){}
Singleton& operator= (const Singleton& _instance){}
static Singleton * instance;
public:
static Singleton* init(){
if(!instance){
instance = new Singleton();
}
return instance;
}
void show(){
cout << "singletion show function." << endl;
}
};
// 1. 懒汉模式
Singleton* Singleton::instance = nullptr;
// 2. 饿汉模式
// Singleton* Singleton::instance = new Singleton();
int main(){
Singleton::init()->show();
return 0;
}
上述代码就实现了一个简单的单例模式,但是这个单例模式存在两方面的缺点:内存泄漏与多线程的问题。由于在类的内部存在指针,而该类的析构函数是private,这就导致在程序结束的时候,不能够释放new出来的指针变量,从而导致了内存泄漏。多线程的问题是指当多个线程运行到init函数中,if语句中会判读当前是否需要创建新的对象,如果此时多个线程同时判断出需要创建一个新的对象,那么就会导致该程序创建多个对象,就违背了单例模式的初衷了。因此,后续的改进将主要针对这两点进行改进。
补充知识点:
1.1 局部变量、全局变量、静态局部变量以及静态全局变量的区别局部变量与静态局部变量:局部变量只在定义的代码块中有效,当程序执行完代码块之后就会被释放掉,局部变量保存在栈内存空间中。静态局部变量也是在定义的代码块中有效,但是在整个程序执行过程中,静态局部变量只会分配一次内存。
全局变量与与静态全局变量:全局变量在整个工程文件中都可见,而且直到程序运行结束之后才会被销毁,保存在内存中的静态区域。静态全局变量只在定义的文件中可见,也保存在内存汇总的静态区域中。
1.2 静态成员变量与静态成员函数静态成员变量:多个对象公用一个静态成员变量,并且静态成员变量只能够在类外进行定义,或者在类内声明的时候就直接进行定义了。
静态成员函数:静态成员函数表示该成员函数独立于对象之外,与对象的无关,可以直接通过类名进行调用。因此,静态成员函数只能只能调用静态成员函数,无法调用非静态成员函数。
2 第二版:atexit()解决内存泄漏问题由于Singleton类的析构函数是private的,因此无法在程序结束之前对new出来的指针进行释放,因此就需要额外提供一个函数来释放指针。atexit()函数就可以解决上述问题。
#include
#include
using namespace std;
class Singleton{
private:
Singleton(){cout << "Singleton 构造函数" << endl;}
~Singleton(){
cout << "Singleton 析构函数" << endl;
}
Singleton(const Singleton& _instance){}
Singleton& operator= (const Singleton& _instance){}
static Singleton * instance;
static void destroy(){
if(instance){
delete instance;
instance = nullptr;
}
}
public:
static Singleton* init(){
if(!instance){
instance = new Singleton();
}
atexit(destroy);
return instance;
}
void show(){
cout << "singletion show function." << endl;
}
};
// 1. 懒汉模式
Singleton* Singleton::instance = nullptr;
// 2. 饿汉模式
// Singleton* Singleton::instance = new Singleton();
int main(){
Singleton::init()->show();
return 0;
}
在第二版代码中,添加了destroy()方法,并在init的时候指定在程序运行之前执行的destroy()函数,用于释放new出来的指针。这样,就会执行执行函数的析构函数,进行变量的释放,下面是程序执行的结果:
只是点:
2.1 什么时候会调用类的析构函数?(1)当类对象销毁的时候:如果此时的析构函数是private的话,表明系统无法自动销毁new出来的指针变量。
(2) 使用delete关键字的时候:使用delete的时候会调用析构函数,与析构函数的属性无关。
(3)继承关系与包含关系:当B是A的子类或者B是A的一个成员变量的时候,当B销毁的时候,不仅会调用B的析构函数,而且还会调用A的析构函数。
3 第三版:借刀杀人解决内存泄漏问题由于我们希望在程序运行结束之后调用析构函数,因此我们可以在Singleton类中再创建一个子类,并且将该子类的对象作为Singleton的成员变量。在该类的析构函数中调用Singleton的析构函数,这样就会使得当该子类对象销毁的时候,就会调用Singleton的析构函数进行内存释放了。
#include
#include
using namespace std;
class Singleton{
private:
Singleton(){cout << "Singleton 构造函数" << endl;}
~Singleton(){
cout << "Singleton 析构函数" << endl;
}
Singleton(const Singleton& _instance){}
Singleton& operator= (const Singleton& _instance){}
static Singleton * instance;
class Destroy{
public:
~Destroy(){
if(instance){
delete instance;
instance = nullptr;
}
}
};
static Destroy destroy;
public:
static Singleton* init(){
if(!instance){
instance = new Singleton();
}
return instance;
}
void show(){
cout << "singletion show function." << endl;
}
};
// 1. 懒汉模式
Singleton* Singleton::instance = nullptr;
// 2. 饿汉模式
// Singleton* Singleton::instance = new Singleton();
Singleton::Destroy Singleton::destroy;
int main(){
Singleton::init()->show();
return 0;
}
有上述代码可知,我们借助Destroy类的析构函数“杀掉了”,这就是孙子兵法中的“借刀杀人”。
4 第四版:互斥量解决多线程的问题在多线程机制中,需要对创建对象的临界区进行加锁处理,而锁的颗粒度会直接影响整个程序运行的效率。如果直接将互斥锁加载if语句之前,而只有在第一次的时候才需要进行加锁,那么则会浪费很大的时间资源。因此,我们需要在if语句里面进行加锁处理。
#include
#include
#include
using namespace std;
class Singleton{
private:
Singleton(){cout << "Singleton 构造函数" << endl;}
~Singleton(){
cout << "Singleton 析构函数" << endl;
}
Singleton(const Singleton& _instance){}
Singleton& operator= (const Singleton& _instance){}
static Singleton * instance;
class Destroy{
public:
~Destroy(){
if(instance){
delete instance;
instance = nullptr;
}
}
};
static Destroy destroy;
static mutex my_mutex;
public:
static Singleton* init(){
// lock_guard my_lock(_mutex);
// 效率太低,因为每次都需要判断,所以不必要上锁的次数太多
if(!instance){
lock_guard my_lock(my_mutex);
instance = new Singleton();
}
return instance;
}
void show(){
cout << "singletion show function." << endl;
}
};
// 1. 懒汉模式
Singleton* Singleton::instance = nullptr;
// 2. 饿汉模式
// Singleton* Singleton::instance = new Singleton();
Singleton::Destroy Singleton::destroy;
mutex Singleton::my_mutex;
int main(){
Singleton::init()->show();
return 0;
}
然而,上述代码依然存在一定的问题:比如当多个线程同时运行到lock_guard
if(!instance){
lock_guard my_lock(my_mutex);
if(!instance){
instance = new Singleton();
}
}
其中,lock_guard是管理互斥量mutex的对象,当lock_guard创建的时候,mutex就上锁了;当lock_guard对象销毁的时候,mutex就解锁了。就不需要我们在对mutex进行上锁与解锁了。
5 第五版:memory order解决cpu与编译器重排代码的问题在c++中,new一个对象并不是原子的 *** 作,这其中涉及到:
(1)分配一个内存空间
(2)调用类的构造函数
(3)让指针指向内存空间进行赋值
而在实际的运行过程中,cpu以及编译器会对上述三个步骤进行重排,所以实际中并不是严格按照上面三个步骤逐步进行运行的。这就会导致,线程在执行完(1)和(3)之后就返回了对象,但是步骤(2)并没有执行,这就会报错。因此,我们需要给new对象这一语句进行 *** 作,之后当其严格完成三个步骤才能够之后后续的代码。这其中就涉及到内存重排的顺序:
#include
#include
#include
#include
using namespace std;
class Singleton{
private:
Singleton(){cout << "Singleton 构造函数" << endl;}
~Singleton(){
cout << "Singleton 析构函数" << endl;
}
Singleton(const Singleton& _instance){}
Singleton& operator= (const Singleton& _instance){}
class Destroy{
public:
~Destroy(){
if(instance){
delete instance;
instance = nullptr;
}
}
};
static atomic instance;
static Destroy destroy;
static mutex my_mutex;
public:
static Singleton* init(){
// lock_guard my_lock(_mutex);效率太低,因为每次都需要判断,所以不必要上锁的次数太多
if(!instance){
lock_guard my_lock(my_mutex);
Singleton* tmp = instance.load(memory_order_relaxed);
atomic_thread_fence(memory_order_acquire);
if(!instance){
instance = new Singleton();
atomic_thread_fence(memory_order_release);
}
}
return instance;
}
void show(){
cout << "singletion show function." << endl;
}
};
// 1. 懒汉模式
atomic Singleton::instance;
// 2. 饿汉模式
// Singleton* Singleton::instance = new Singleton();
Singleton::Destroy Singleton::destroy;
mutex Singleton::my_mutex;
int main(){
Singleton::init()->show();
return 0;
}
6 第六版:局部静态变量解决上述两个问题
从上面的内存泄漏以及多线程的问题不难发现,其主要原因是我们在串讲对象的时候使用到了指针的形式。因此,为了避免上述问题,我们可以使用变量的形式代替指针的形式,这样就可以有效的避免内存泄漏以及多线程的问题了。
下面就是饿汉模式的代码,我们使用全局静态变量变量进行初始化。
#include
using namespace std;
class Singleton{
private:
Singleton(){cout << "Singleton 构造函数" << endl;}
~Singleton(){
cout << "Singleton 析构函数" << endl;
}
Singleton(const Singleton& _instance){}
Singleton& operator= (const Singleton& _instance){}
static Singleton instance;
public:
static Singleton* init(){
return &instance;
}
void show(){
cout << "singletion show function." << endl;
}
};
// 1. 饿汉模式
Singleton Singleton::instance;
int main(){
Singleton::init()->show();
return 0;
}
下面就是懒汉模型的代码,我们使用局部静态变量进行初始化:
#include
using namespace std;
class Singleton{
private:
Singleton(){cout << "Singleton 构造函数" << endl;}
~Singleton(){
cout << "Singleton 析构函数" << endl;
}
Singleton(const Singleton& _instance){}
Singleton& operator= (const Singleton& _instance){}
public:
static Singleton* init(){
// 2.懒汉模式
static Singleton instance;
return &instance;
}
void show(){
cout << "singletion show function." << endl;
}
};
int main(){
Singleton::init()->show();
return 0;
}
然而,在使用静态变量的时候,系统会随机初始化静态变量。如果两个类互相有对方的实例,那么在实际应用的过程中就会造成一个对象还未初始化的问题。而且,在懒汉模式下,局部静态变量的初始化也不是原子性的,也存在多线程的问题,该问题可参考这个博客。
7 第七版:Boost库的终极版本-借花献佛在第三版中,我们就说过可以再创建一个类,在其析构函数中使用“借刀杀人”的方法调用private的构造函数。为了避免静态变量出现初始化顺序以及多线程的问题,我们同样可以使用“借花献佛”的思想,再创建一个类,在其构造函数中创建类的对象。
#include
using namespace std;
class Singleton{
private:
Singleton(){cout << "Singleton 构造函数" << endl;}
~Singleton(){
cout << "Singleton 析构函数" << endl;
}
Singleton(const Singleton& _instance){}
Singleton& operator= (const Singleton& _instance){}
class Creator{
public:
Creator(){
Singleton::init();
};
};
static Creator creator;
public:
static Singleton* init(){
// 2.懒汉模式
static Singleton instance;
return &instance;
}
void show(){
cout << "singletion show function." << endl;
}
};
Singleton::Creator Singleton::creator;
int main(){
Singleton::init()->show();
return 0;
}
这主要的原因就是在main函数执行之前,就已经对static变量进行了初始化。而该类的模板类实现代码如下:
#include
using namespace std;
template
class Singleton{
protected:
Singleton(){cout << "Singleton 构造函数" << endl;}
~Singleton(){
cout << "Singleton 析构函数" << endl;
}
Singleton(const Singleton& _instance){}
Singleton& operator= (const Singleton& _instance){}
class Creator{
public:
Creator(){
Singleton::init();
};
};
static Creator creator;
public:
static T* init(){
// 2.懒汉模式
static T instance;
return &instance;
}
void show(){
cout << "singletion show function." << endl;
}
};
class Instance{
public:
Instance(){};
~Instance(){};
void show(){
cout << "instance show funcion" << endl;
}
friend class Singleton;
};
int main(){
Instance* instance = Singleton::init();
instance->show();
return 0;
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)