C++实现单例模式

本文介绍了C++中的单例模式,包括其优点、缺点和适用场景。详细讲解了经典实现、线程安全以及懒汉和饿汉两种模式的代码实现,帮助读者深入理解如何确保类只有一个实例。
摘要由CSDN通过智能技术生成

前言

关于单例模式的相关资料和博文非常多,原因不仅仅在于它作为设计模式的重要性,也在于各大公司笔试面试题出现概率之高让人乍舌。正因为如此,通过这篇博文的书写,加深自己对单例模式的理解,以不变应万变。

简介

单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。

//以下援引百度百科

对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。

优点

1. 由于单例模式在内存中只有一个实例,减少了内存开支,特别是**一个对象需要频繁地创建、销毁**时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

2. 减少了系统的性能开销,当**一个对象的产生需要比较多的资源**时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

3. 避免对资源的多重占用。如避免对同一个资源文件的同时写操作。

4. 单例模式可以在系统设置全局的访问点,优化和共享资源访问。

缺点

1. 单例模式一般没有接口,扩展困难。不利于测试。

使用场景

1. 在整个项目中需要一个共享访问点或共享数据。

2.创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。

3. 需要定义大量的静态常量和静态方法的环境。


代码实现

经典实现

首先介绍一种最经典的实现方式

class singleton
{
private:
	static singleton *instance;
	singleton(){}
public:
	static singleton * getinstance()
	{
		if (instance == NULL)
			instance = new singleton();
		return instance;
	}
};
singleton* singleton::instance = NULL;

通过代码我们便能够清晰的了解到单例模式设计的思想,首先单例类没有公开的构造器,其他类不能自行将其实例化得到一个实例,而必须通过它的静态方法getinstance()去创建一个实例。
那么我们该如何创建一个单例呢,有以下三种方式,这三种方式大致相同,都是通过getinstance()方法获取实例。

singleton *a = singleton::getinstance();
singleton *b = a->getinstance();
singleton &c = *singleton::getinstance();


那么上面的实现方式有缺陷吗?当然有,而且当你在面试或者笔试中写出该实现方式时,面试官一定不会满意的。
首先我们很容易发现,instance指向的空间何时释放,该实例的析构函数何时调用?如果我们想在该类析构时进行一些操作,比如关闭文件流等。上面实现的单例并不能满足这些要求。因此我们需要一种方法,让该实例能够自动删除自己。

熟悉C++的知道,在程序运行结束时,会自动回收所有全局变量和静态变量,这些变量存储在全局/静态存储区。而堆区,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。利用这个特性,我们可以在该单例中定义这样一个静态成员变量,该变量在其析构函数中删除单例类的实例,从而达到回收内存的目的。正如下面代码中的CGarbo类。

class singleton
{
private:
	static singleton *instance;
	singleton(){}
	//CGarbo类的唯一工作就是在析构函数中删除CSingleton的实例
	class CGarbo
	{
	public:
		~CGarbo()
		{
			if (singleton::instance != NULL)
				delete singleton::instance;
		}
	};
	//定义一个静态成员,在程序结束时,系统会调用它的析构函数
	static CGarbo Garbo;
public:
	static singleton * getinstance()
	{
		if (instance == NULL)
			instance = new singleton();
		return instance;
	}
};
singleton* singleton::instance = NULL;
singleton::CGarbo singleton::Garbo;

我们可以写一段测试代码来查看是否成功删除单例的实例,测试代码如下,增加了单例类的析构函数

#include<iostream>
using namespace std;

class singleton
{
private:
	static singleton *instance;
	singleton(){}
	//CGarbo类的唯一工作就是在析构函数中删除CSingleton的实例
	class CGarbo
	{
	public:
		~CGarbo()
		{
			if (singleton::instance != NULL)
				delete singleton::instance;
		}
	};
	//定义一个静态成员,在程序结束时,系统会调用它的析构函数
	static CGarbo Garbo;
public:
	static singleton * getinstance()
	{
		if (instance == NULL)
			instance = new singleton();
		return instance;
	}
	~singleton()
	{
		cout << "delete success" << endl;
	}
};
singleton* singleton::instance = NULL;
singleton::CGarbo singleton::Garbo;
int main() {
	singleton *a = singleton::getinstance();
	singleton *b = a->getinstance();
	singleton &c = *singleton::getinstance();
}

运行结果:

delete success

线程安全

在前面实现的经典单例模式中,实例删除问题已经解决了,但还不够。一个很严重的问题就是,该实现方式时线程不安全的。假设单例还未初始化,有两个线程同时调用getinstance方法,这时执行 instance == NULL 肯定为真,然后两个线程都初始化一个单例,最后得到的指针并不是指向同一个地方,不满足单例类的定义了。

所以为了满足线程安全,我们需要做一些微小的工作,使用了Linux的互斥锁,在构造函数中初始化。

class singleton
{
private:
	static singleton *instance;
	singleton(){ pthread_mutex_init(&mutex,NULL); }
	//CGarbo类的唯一工作就是在析构函数中删除CSingleton的实例
	class CGarbo
	{
	public:
		~CGarbo()
		{
			if (singleton::instance != NULL)
				delete singleton::instance;
		}
	};
	//定义一个静态成员,在程序结束时,系统会调用它的析构函数
	static CGarbo Garbo;
	static pthread_mutex_t mutex;
public:
	static singleton * getinstance()
	{
		if (instance == NULL)
		{
			pthread_mutex_lock(&mutex);
			if (instance == NULL)
				instance = new singleton();
			pthread_mutex_unlock(&mutex);
		}
		return instance;
	}
};
singleton* singleton::instance = NULL;
singleton::CGarbo singleton::Garbo;
pthread_mutex_t singleton::mutex;

懒汉和饿汉

解决了线程问题后,才算是一个完整的单例模式。接下来介绍的只是不同的实现方式,大同小异,所要考虑的也不外乎是线程安全和内存管理等问题。
有关单例模式的实现方式分为两种: 懒汉模式饿汉模式
懒汉模式
顾名思义,懒汉模式就是程序足够懒,只有在需要使用实例时候才创建实例,是一种以 时间换空间的做法。上面的经典实现即懒汉模式,只有在getinstance的时候才新建实例。经典实现中需要考虑线程安全和内存释放,这里再介绍一种懒汉实现方式, 不需要考虑内存释放问题,简化了代码。

class singleton
{
private:
	static singleton *instance;
	singleton(){ pthread_mutex_init(&mutex, NULL); }
	static pthread_mutex_t mutex;
public:
	static singleton * getinstance()
	{
		pthread_mutex_lock(&mutex);
		static singleton instance;
		pthread_mutex_unlock(&mutex);
		return &instance;
	}
};
pthread_mutex_t singleton::mutex;

相信你一眼就看出,这种改进只不过是使用了局部静态变量,然后返回改变量的引用。这么一个微小的改动就能节省很多的代码。
饿汉模式
类比于懒汉模式,饿汉模式就是程序非常饿,在刚开始就初始化了一个实例,是一种以 空间换时间的做法。正是由于该特性,懒汉模式不 需要考虑线程安全的问题,但是依旧需要考虑实例删除的问题。

class singleton
{
private:
	static singleton *instance;
	singleton(){}
	//CGarbo类的唯一工作就是在析构函数中删除CSingleton的实例
	class CGarbo
	{
	public:
		~CGarbo()
		{
			if (singleton::instance != NULL)
				delete singleton::instance;
		}
	};
	//定义一个静态成员,在程序结束时,系统会调用它的析构函数
	static CGarbo Garbo;
public:
	static singleton * getinstance()
	{
		return instance;
	}
};
singleton* singleton::instance = new singleton;
singleton::CGarbo singleton::Garbo;


总结

无论是懒汉还是饿汉还是什么其他的实现方式,除了基本的单例模式的属性以外,需要考虑的无外乎线程安全和内存管理(实例删除)的问题,正确的处理好这些要点,那么你的单例模式就一定是合格的。










评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值