C++特殊类的设计: 单例模式 (饿汉 + 懒汉)

单例模式是C++中很重要的一种设计模式, 这次我们就来聊聊单例模式的设计, 从饿汉和懒汉两种模式出发去实现单例模式,

实现单例模式之前, 建议大家先看一看其他C++中的特殊类的设计, 有关后面的设计思想

这里附上链接: C++特殊类的设计: 只能在堆/栈上创建对象, 不能被继承的类


单例模式

很简单, 设计一个类, 只能创建一个对象就是单例模式


饿汉模式

饿汉模式: 保证程序启动前, 对象就存在

要想使对象在程序启动前就存在, 我们可以用静态成员

静态成员在main函数执行之前就已经存在

要想实现单例模式, 首先我们不能让他随意调用构造和拷贝构造函数
构造函数私有化 + 防拷贝 (设置拷贝构造为delete)
然后根据上面说的, 我们要在类中定义一个静态成员变量, 也就我们要的唯一的对象
注意静态成员要在类外进行初始化, 初始化静态成员就即调用了构造函数
然后我们还要定义一个静态的公有接口, 返回我们的静态对象
注意一定要返回引用, 因为拷贝构造已经被设置成delete, 无法返回值

具体代码如下:

//1. 饿汉模式: 保证程序启动前, 对象就存在
class Singleton {
public:
	//公有的静态方法: 获取唯一的静态对象
	static Singleton& getojb() {
		return _sl;
	}
private:
	//构造函数私有化
	Singleton() {
		cout << "Singleton" << endl;
	}

	//防拷贝
	Singleton(const Singleton&) = delete;
	
	//静态成员
	static Singleton _sl;
};

//静态数据先于主函数存在
//静态成员初始化, 在主函数之前调用构造
Singleton Singleton::_sl;

void test3() {
	Singleton& ref = Singleton::getojb();
}

饿汉模式的优缺点也很明显

  • 优点 : 简单, 容易实现
  • 缺点 : 可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。

懒汉模式

懒汉模式: 使用的时候再创建 (延迟加载)

首先还是禁止随意调用构造和拷贝构造
构造函数私有化 + 防拷贝 (设置拷贝构造为delete)
然后提供一个公有接口, 只在第一次调用时创建对象并返回
那么要怎么做呢?
我们可以定义一个静态指针, 唯一标识一块空间, 初始化为空
每次判断指针是否为空, 为空则标识第一次创建对象, 不为空则不创建

代码如下:

//2. 懒汉模式: 使用的时候再创建
class Singleton2 {
public:
	//静态公有方法, 只有第一次调用时创建对象
	static Singleton2* getobj() {
		//判断标记指针是否为空, 空则创建对象, 不为空就直接返回
			if (_ptr == nullptr) {
				_ptr = new Singleton2;
			}
		return _ptr;
	}
private:
	//构造函数私有化
	Singleton2() {
		cout << "Single()" << endl;
	}

	//防拷贝
	Singleton2(const Singleton2&) = delete;

	//静态指针, 标记空间
	static Singleton2* _ptr;
};
//初始化静态指针
Singleton2* Singleton2::_ptr = nullptr;

线程安全

上述代码乍一看么得问题, 但是如果放在多线程的情况下

所有线程"同时"进入到这个函数, 发现指针都是空, 那么每个线程都会申请一块空间创建对象

虽然每个线程都会创建对象, 但是我们的静态指针最后只会拿到最后一个线程创建的对象, 之前创建的都被覆盖

这就造成了内存泄漏, 之前申请的空间没有释放但是丢掉了

非常危险 ! ! !

那么要避免上述情况, 就要让静态指针的判断成为一个原子操作
具体操作就是在判断之前加锁, 之后解锁, 保证操作原子性

这样虽然能解决问题, 但是如果每次来都进行加锁解锁, 这锁也太重了~

严重降低了程序的效率, 所以我们要进行优化

这里采取的措施是在加锁前再进行一次判断

我们先来看一下代码, 之后会详细说明为什么加一个判断

//2. 懒汉模式: 使用的时候再创建
class Singleton2 {
public:
	//静态公有方法, 只有第一次调用时创建对象
	static Singleton2* getobj() {
		//判断标记指针是否为空, 空则创建对象, 不为空就直接返回
		//为了保证线程安全, 需要在判断前加锁, 阻塞其他线程
		//每次加锁时间消耗太大的, 进行优化: 只在第一次创建的时候加锁
		if (_ptr == nullptr) {
			_mtx.lock();
			if (_ptr == nullptr) {
				_ptr = new Singleton2;
			}
			_mtx.unlock();
		}
		return _ptr;
	}
private:
	//构造函数私有化
	Singleton2() {
		cout << "Single()" << endl;
	}

	//防拷贝
	Singleton2(const Singleton2&) = delete;

	//静态指针, 标记空间
	static Singleton2* _ptr;

	//静态锁
	static mutex _mtx;
};
//初始化静态指针和锁
Singleton2* Singleton2::_ptr = nullptr;
mutex Singleton2::_mtx;

下面给大家说明这个加这个 if 能干嘛

两个if判断, 外面的if为了提高效率, 里面的if为了保证单例
第一次创建对象, 此时指针为空, 所有线程都进入第一个if
然后加锁阻塞了其他线程, 只有一个线程进入第二个if, 创建对象
解锁之后其他线程来到第二个if, 此时ptr已经不为空, 其他的线程无法进入if创建对象, 而是直接返回, 这时第一波完成
第二波到来ptr不为空, 第一个if都进不来, 不用加锁解锁, 提高了效率

我们可以看到, 加了一个if判断, 就能让加锁只在线程第一波进入if时加锁解锁, 之后就进不了第一个if, 从而极大的减少了加锁解锁的次数, 极大的提高了代码的性能…


OK, 到这里就结束啦~
大家有问题欢迎评论区提出, 一起学习一起提高 !

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殇&璃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值