单例模式是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, 到这里就结束啦~
大家有问题欢迎评论区提出, 一起学习一起提高 !