C++单例模式
先简单介绍一下单例模式:
单例模式(Singletion Pattern)是一种软件开发中的设计模式,属于创建型模式(也称工厂模式,封装对象的创建过程,使客户端可以透明地创建对象,而不需要关心对象的内部实现细节)。单例模式确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。单例模式通常用于管理共享资源,如数据库连接、文件系统、硬件设备等,或者在多个线程之间共享数据。
由上面的定义,我们也可以知道单例模式的几个特点:
- 全局唯一性:确保一个类只有一个实例,并提供一个全局访问点。
- 自我实例化:单例类负责创建自己的唯一实例。
- 延迟初始化:单例实例通常在第一次使用时创建,而不是在程序启东市。
- 可访问性:单例类通常提供一个静态的访问方法,允许客户端访问其唯一实例。
实现单例模式需要以下几个步骤:
- 私有构造函数:防止外部通过new直接创建类的实例。
- 静态私有实例变量:保存类的唯一实例。
- 静态公有方法:提供一个全局访问点用于获取唯一实例。
简单的单例模式(懒汉Lazy Singleton)
#include <iostream>class Singleton{
private://私有构造函数Singleton(){}//静态私有实例变量static Singleton* instance;
public://禁止拷贝构造函数和赋值操作Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;//公有静态方法,提取全局访问点static Singleton* getInstance(){//判断是否第一次调用if(instance = nullptr){instance = new Singleton();}return instance;}//实例方法void dosomething(){std::cout<<"Hello world"<<std:;endl;}
}// 初始化静态私有实例变量
Singleton * Singleton::instance = nullptr;int main(){//获取单例实例Singleton* singleton = Singleton::getInstance();//使用单例实例singleton->dosomething();
}
这个简单实例也称为懒汉单例模式(Lazy Singleton),单例在第一次使用才会被初始化,或称为延迟初始化,也正因为仅在第一次使用时创建单例实例,能够节省资源,但在某些情况下,可能会导致内存泄漏。举例如下:
- 单例实例通常为静态成员变量,静态成员变量的生命周期与程序的生命周期相同,若单例实例持有资源(动态分配的内存、文件描述符等),而这些资源在程序运行期间未正确释放,会导致内存泄漏。
- 单例实例是静态的,它的销毁通常需要在程序结束时通过显式的代码来完成。如果忘记编写这样的销毁代码,持有的资源不会释放。
这里提供一个会造成内存泄漏的例子
class Singleton {
private:static Singleton* instance;// 假设单例持有动态分配的内存int* data;Singleton() {data = new int[100]; // 动态分配内存}public:static Singleton* getInstance() {if (instance == nullptr) {instance = new Singleton();}return instance;}// 公有析构函数,虽然是公有的,但由于是私有的构造函数,外部无法创建实例~Singleton() {delete[] data; // 释放动态分配的内存}
};Singleton* Singleton::instance = nullptr;int main() {// 使用单例Singleton* singleton = Singleton::getInstance();// 单例实例持有的动态分配的内存没有释放return 0;
}
Singleton持有一个指向动态分配数组的指针data,由于没有提供销毁单例实例的函数,当程序结束时,data指向的内存未被释放,从而导致内存泄漏。
针对这一问题,有以下两种解决方案:
1.使用智能指针
2.使用静态的嵌套类对象
智能指针
上述代码可以改为:
#include <memory> // 包含智能指针的头文件class Singleton {
private:static std::unique_ptr<Singleton> instance; // 使用智能指针管理单例实例int* data;// 私有构造函数Singleton() {data = new int[100]; // 动态分配内存}// 禁止拷贝构造函数和赋值操作符Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;public:// 静态公有方法,提供全局访问点static Singleton* getInstance() {if (instance.get() == nullptr) {instance.reset(new Singleton()); // 使用智能指针管理单例实例}return instance.get();}// 公有析构函数,虽然是公有的,但由于是私有的构造函数,外部无法创建实例~Singleton() {delete[] data; // 释放动态分配的内存}
};// 初始化静态成员变量
std::unique_ptr<Singleton> Singleton::instance = nullptr;int main() {// 使用单例Singleton* singleton = Singleton::getInstance();// 程序结束时,智能指针会自动释放单例实例,包括其持有的动态分配的内存return 0;
}
嵌套类
class Singleton {
private:static Singleton* instance; // 静态指针,用于存储单例的唯一实例int* data; // 指向动态分配内存的指针// 私有构造函数,只能内部调用Singleton() {data = new int[100]; // 动态分配内存}// 私有析构函数,只能内部调用~Singleton() {delete[] data; // 释放动态分配的内存,防止内存泄漏}// 内部类,用于在程序结束时自动销毁单例实例class Deletor {public:// 析构函数,在程序结束时调用~Deletor() {if (Singleton::instance != nullptr)delete Singleton::instance; // 如果单例实例存在,则删除它}};// 静态成员变量,用于确保Deletor的析构函数在程序结束时被调用static Deletor deletor;public:// 静态公有方法,提供全局访问点,用于获取单例实例static Singleton* getInstance() {if (instance == nullptr) { // 如果单例实例尚未创建instance = new Singleton(); }return instance;}
};// 初始化静态成员变量
Singleton* Singleton::instance = nullptr; // 初始化为nullptr,表示单例实例尚未创建
Singleton::Deletor Singleton::deletor; // 创建Deletor实例,以便在程序结束时调用其析构函数// 主函数
int main() {Singleton* singleton = Singleton::getInstance(); // 获取单例实例// 使用单例实例...return 0; // 程序结束,Deletor的析构函数将被调用,自动销毁单例实例
}
在程序运行结束时,系统会调用静态成员deletor的析构函数,该析构函数会删除单例的唯一实例。用这种方法释放单例对象。
- 在单例类内部定义专有的嵌套类。
- 在单例类内定义私有的专门用于释放的静态成员
- 利用程序在结束时析构全局变量的特性,选择最终的释放时机。
对于内存泄漏的检测,考虑使用valgrind
多线程下的单例模式
这些代码在单线程下环境是正确的,但是当处于多线程环境时,会发生竞争。最简单的解决竞争的方式是加入锁机制来保护临界区。
于是就有了最简单的多线程环境下的单例模式代码:
#include <iostream>class Singleton{
private://私有构造函数Singleton(){}//静态私有实例变量static Singleton* instance;mutex mutex;
public://禁止拷贝构造函数和赋值操作Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;//公有静态方法,提取全局访问点static Singleton* getInstance(){//判断是否第一次调用//上锁 mutex.lock();if(instance = nullptr){instance = new Singleton();}//解锁mutex.unlock();return instance;}//实例方法void dosomething(){std::cout<<"Hello world"<<std:;endl;}
}// 初始化静态私有实例变量
Singleton * Singleton::instance = nullptr;int main(){//获取单例实例Singleton* singleton = Singleton::getInstance();//使用单例实例singleton->dosomething();
}
Double Checked Locking(DCL)
考虑到线程安全仅存在于第一次初始化(new)过程中,而在后续获取该实例时并不会遇到,也就没有必要再使用lock。使用双重检查锁定(Double-Checked Locking)来确保单例实例的线程安全,主要目的时减少加锁的次数。在细节上:
- 第一次检查:在getinstance方法中,首先检查instance是否为nullptr,若是,则继续执行、
- 加锁:使用std::lock_guard来确保mutex在作用域内被正确地加锁和解锁。这种方法被称为作用域锁,因为它在lock_guard对象的作用域结束时自动解锁。
- 第二次检查:在加锁前,再次检查instance是否为nullptr,若仍为nullptr,则加锁并创建单例实例。
- 创建实例:在加锁后,如果instance仍为nullptr,则创建实例。
- 返回实例:无论是否创建了新实例,最终都返回instance指针。
static Singleton* getInstance() {if (instance == nullptr) {std::lock_guard<std::mutex> lock(mutex); // 作用域锁if (instance == nullptr) {instance = new Singleton(); // 创建单例实例}}return instance;
}
“加入DCL后,其实还是有问题的,关于memory model。在某些内存模型中(虽然不常见)或者是由于编译器的优化以及运行时优化等等原因,使得instance虽然已经不是nullptr但是其所指对象还没有完成构造,这种情况下,另一个线程如果调用getInstance()就有可能使用到一个不完全初始化的对象。换句话说,就是代码中第2行:if(instance == NULL)和第六行instance = new Singleton();没有正确的同步,在某种情况下会出现new返回了地址赋值给instance变量而Singleton此时还没有构造完全,当另一个线程随后运行到第2行时将不会进入if从而返回了不完全的实例对象给用户使用,造成了严重的错误。在C++11没有出来的时候,只能靠插入两个memory barrier(内存屏障)来解决这个错误,但是C++11引进了memory model,提供了Atomic实现内存的同步访问,即不同线程总是获取对象修改前或修改后的值,无法在对象修改期间获得该对象。”
C++ 单例模式 - 知乎 (zhihu.com)
C++11规定了local static在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性。在C++11标准下,《Effective C++》提出了一种更优雅的单例模式实现,使用函数内的 local static 对象。这样,只有当第一次访问
getInstance()
方法时才创建实例。这种方法也被称为Meyers' Singleton。C++0x之后该实现是线程安全的,C++0x之前仍需加锁。
Meyers’ Singleton(也称为Singleton模式或懒汉式Singleton模式)是一种单例模式实现,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。与传统的懒汉式单例模式不同,Meyers’ Singleton在C++中通过使用内部静态类来实现,这是一种在C++11之前常用的方法,因为它能够保证线程安全,同时避免在全局范围内声明静态对象。
Meyers’ Singleton的关键点在于它使用了内部静态类来延迟实例的创建,直到首次使用时才创建实例。内部静态类在类定义时创建,因此它不会被全局实例化,从而避免了全局初始化的开销。
class Singleton {
public:static Singleton& getInstance() {// 内部静态类,确保在类定义时创建,而不是在全局范围内实例化static Singleton instance;return instance;}private:// 私有构造函数,防止外部通过new创建对象实例Singleton() {};// 私有拷贝构造函数,防止外部复制对象实例Singleton(const Singleton&) = delete;// 私有拷贝赋值运算符,防止外部复制对象实例Singleton& operator=(const Singleton&) = delete;
};
getInstance
方法返回一个内部静态类Singleton
的实例。由于Singleton
是一个内部静态类,它不会在全局范围内实例化,而是在类定义时实例化。这意味着只有在首次调用getInstance
方法时,才会创建单例实例。
Meyers’ Singleton模式提供了一种线程安全的懒汉式单例实现,它避免了全局初始化的开销,并且通过内部静态类保证了线程安全。在C++11之后,可以使用std::call_once
或std::once_flag
来更简洁地实现单例模式。
饿汉式单例模式
单例实例在程序运行时被立即执行初始化,它确保一个类只有一个实例,并且在类加载时立即创建这个实例。饿汉式单例模式的特点是实例在类加载时就创建,因此不需要在第一次使用时创建,这使得饿汉式单例模式在类加载时就会占用一定的内存。
饿汉式单例模式的优点是简单且线程安全,因为实例在类加载时就已经创建,所以不会出现线程竞争的问题。然而,它的缺点是资源利用率不高,因为实例在类加载时就创建了,即使你从未使用过这个单例。
class Singleton {
private:// 私有构造函数,防止外部通过new创建对象实例Singleton() {}// 私有拷贝构造函数,防止外部复制对象实例Singleton(const Singleton&) = delete;// 私有拷贝赋值运算符,防止外部复制对象实例Singleton& operator=(const Singleton&) = delete;public:// 静态成员变量,在程序启动时创建单例实例static const Singleton& getInstance() {return instance;}private:// 静态成员变量,类加载时初始化static const Singleton instance;
};// 在类外初始化静态成员变量
const Singleton Singleton::instance;
-
静态常量成员变量:
instance
是一个静态常量成员变量,它在程序启动时创建,并且在整个程序运行期间都存在。 -
构造函数和析构函数:由于
instance
是一个常量,因此必须在类外部进行初始化,并且只能通过常量表达式进行初始化。 -
线程安全:饿汉式单例模式在程序启动时就创建了单例实例,因此它本身就是线程安全的,不需要担心多线程下的初始化问题。
-
常量引用返回:
getInstance
方法返回的是对单例实例的常量引用,这防止了对单例实例的修改。 -
不需要显式析构函数:由于
instance
是静态常量成员,它的生命周期由系统管理,不需要显式析构。
参考文章:
C++程序员们,快来写最简洁的单例模式吧 - 老司机 - 博客园 (cnblogs.com)
C++中的单例模式 | 神奕的博客 (songlee24.github.io)