C++单例模式的n种实现总结

面试官:“接下来,说说C++实现一个单例模式的思路吧”。
这个问题我曾经看过,舒了一口气,冷静了一下缓缓答道:“所谓单例模式,就是一个类在程序的过程中只能有一个实例对象,不能随意的创建对象实例,因此,首先就是要把构造函数设置为private,这样用户代码无法创建一个对象,还对象只能在类内创建。”

面试官:“嗯,然后呢?”
我:“同时还要定义一个public+static函数接口,供外部访问该对象,因此还要定义一个私有的static的指针。”
面试官:“嗯,你写一下吧。”

// Version 1
class Singleton{
public:
	static Singleton* getInstance() {
		if (p == nullptr) {
			p = new Singleton();
		}
		return p;
	}
private:
	Singleton() = default;
	static Singleton *p;
};
Singleton* Singleton::p = nullptr;

面试官微笑着说:“你解释一下设计思路吧。”
我:“提供一个公有static函数接口,在这个函数内先判断是否已经创建过实例,如果有,则直接返回,否则就new一个。”
面试官:“你觉得你这个设计有什么问题吗?”
我:“emmmm,有两个问题,一个是线程安全问题,还有一个是内存释放的问题。所谓线程安全问题,就是若多线程同时调用getInstance(),线程A判断完p==nullptr之后准备开始new了,但此时由于线程调度切换到B,B此时也发现p是nullptr,也打算开始new了,因此就产生了冲突。”
面试官:“嗯,那你说说怎么解决吧”。
我:“解决的方法有两种,第一种就是加锁,以Linux来说,这样写”

// Version 2
class Singleton{
public:
	static Singleton* getInstance() {
		if (p == nullptr) {
			pthread_mutex_lock(&mutex);
			if (p == nullptr)
				p = new Singleton();
			ptread_mutex_unlock(&mutex);
		}
		return p;
	}
private:
	Singleton() {
		pthread_mutex_init(&mutex);
	}
	static Singleton *p;
	static pthread_mutex_t mutex;
};
Singleton* Singleton::p = nullptr;
pthread_mutex_t Singleton::mutex;

面试官:“嗯,能解释一下为什么有两次if判断吗?”
我:“是这样的,因此加锁其实是一个蛮大的开销,如果我仅在第一层if判断外面加锁,那么每次调用getInstance都会申请锁(不管其是不是要new一个实例),因此多线程下效率不高。而在实例创建之后,我们调用getInstance就不需要锁了,这样写效率更高。”
面试官:“嗯,你刚刚说线程安全还有一种方法,能说明一下吗?”
我:“可以,另外一种方法就不是考虑线程同步问题了,而是从实例化的时机去考虑。其实我现在写的方法叫懒汉模式,就是说直到我第一次调用getInstance才会实例化一个对象,这种方法的有点就是直到调用才会分配内存,可以加快程序的启动速度。而还有一种方法是饿汉模式,就是说在static成员初始化的时候即实例化,这样在类定义之后就实例化了一个对象,无论我有没有调用getInstance,这个对象都存在内存里。由于其是先于main函数之前完成的实例化,因此是线程安全的。可以这样修改。”

// Version3
class Singleton{
public:
	static Singleton* getInstance() {
		return p;
	}
private:
	Singleton() {}
	static Singleton *p;
};
Singleton* Singleton::p = new Singleton();

我:“可以看到这种方法不需要进行线程同步操作,因此适合访问量较大的多线程情况。”
面试官:“嗯,你刚刚提到内存泄露问题,可以解释一下吗?”
我:“在之前提到的办法里new了一个对象,但是我却没有delete掉这部分主动申请的资源,从而造成了内存泄露。”
面试官来了一句:“那我在自己在最后delete掉这部分资源不行吗?”
我:“可以,但没必要,因为这样做谁也保不准最后给忘记了,但我们有更好的方法,让操作系统自动去管理。因为我们知道,当进程结束时,静态对象的生命周期随之结束,其析构函数会被调用来释放对象。因此我们可以定义一个内嵌类,专门负责delete。”

// Version 4
class Singleton{
public:
	static Singleton* getInstance() {
		return p;
	}
private:
	Singleton() = default;
	Singleton(const Singleton&);
	Singleton& operator=(const Singleton&);
	static Singleton *p;
	class GarbageCollector {
		public:
			~GarbageCollector() {
				if (Singleton::p) {
					delete p;
					p = nullptr;
				}
			}
	};
	static GarbageCollector gc;
};
// 初始化 
Singleton* Singleton::p = new Singleton();
Singleton::GarbageCollector Singleton::gc;

我又接着说道:“该内嵌类定义为私有,防止其余代码访问。解决内存泄露还有一种方法,就是我不用new去实例化一个对象,而是使用静态(局部)对象,那么就交给编译器自动释放了。”

// Version 5
class Singleton {
	// ...
private:
	static Singleton obj;
};
Singleton Singleton::obj;
OR
class Singleton{
public:
	Singleton* getInstance() {
		static Singleton obj; // 局部static对象
		return &obj;
	}
};

写完之后,我接着道:“一个是饿汉模式,一个是懒汉模式,由于是静态的对象,因此存储在静态存储区,当程序结束后由操作系统自动释放。”
面试官接着问:“不过你这里的懒汉模式应该还是有线程安全的问题的吧。”
我:“在C++11之前,局部static对象初始化是先判断标志位初始化是否为1,若为1则说明已经初始化过,若不为1则说明第一次则调用构造函数进行初始化。因此,这里面存在线程安全问题,即一个线程刚把标志位置1还没来得及初始化,这时候另一个线程进来发现哦已经被初始化过了就直接返回了该对象,但实际上并没有被初始化过。但是C++11中对此做了优化,确保了局部static对象初始化的线程安全性。”
面试官:“嗯,那你还有什么要说的吗?”
我:“总结一下吧,单例模式一般有两种设计方案,一个是懒汉模式,即直到第一次调用getInstace接口才会初始化实例,从而有延迟初始化的效果,提高了程序的启动速度。缺点是多线程情况下需要考虑线程安全。另一种饿汉模式,在程序启动时就完成对象实例化,分配内存空间,是线程安全,适用于多线程访问量较大的情况。如果单例模式内部是用静态对象指针,则需要解决内存释放问题,经常通过内嵌一个类,在该类析构函数中完成delete操作,同时Singleton类定义一个该内嵌类的私有静态对象,从而让编译期自动回收。如果采用静态对象,则会由编译器自动回收。当然了,也可以通过C++的智能指针来实现内存的自动回收,但是智能指针也是要调用析构函数的,所以该类的析构函数要声明为public,如果把析构函数声明成为private,就要自己重新定义一个static成员函数,传给shared_ptr,指定其为释放资源调用的函数。”
面试官:“好,本次面试就到这里吧,等下hr会电话告诉你面试结果。”
我:“好的,面试官再见。”

参考

1.面试之Singleton
这篇博客的深度太高了,后面的很多东西都看的一知半解,特此马克,以后值得再次阅读。
2.可以用C++智能指针实现
3.C++单例模式的几种研究
这里面提到了饿汉模式下的竞争问题,也就是说C++内部的全局静态变量他初始化的顺序是不一样的,因此提到了一个辅助类,在该辅助类中调用构造函数。
4.static机制与单例模式
5.C++11之前局部静态对象构造是不具有线程安全性的
6.C++11单例模式内存释放的几种方法
主要有手动调用函数,但容易忘记;通过C标准库的atexit()函数注册释放函数;通过内嵌类的静态类对象来调用;
7. 单例模式是否可以写成模板类呢?是否可重用呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值