之前在这篇文章中简单的介绍了一下单例模式的作用和应用C++中单例模式详解_c++单例模式的作用-CSDN博客,今天我将在在本文梳理单例模式从C++98到C++11及以后的演变过程,探讨其不同实现方式的优劣,并介绍在现代C++中的最佳实践。
简单来说,单例模式(Singleton Pattern)是一种设计模式,它能保证一个类在整个程序运行期间,只有一个实例存在 。
这种唯一性的保证在特定场景下至关重要。例如,对于一个数据库连接管理器 Manager,如果系统中存在多个实例,不同模块可能会通过不同实例进行操作,从而引发数据状态不一致或资源竞争的问题 。通过将 Manager 设计为单例,所有模块都通过唯一的访问点来与数据库交互,这不仅能保证数据和状态的统一,还能有效规避资源浪费 。
总结而言,单例模式主要具备两大价值:
因此,该模式广泛应用于配置管理、日志系统、设备驱动、数据库连接池等需要全局唯一实例的场景中 。
//通过静态成员变量实现单例
//懒汉式
class Single2
{
private:
Single2(){}
Single2(const Single2 &) = delete;
Single2 &operator=(const Single2 &) = delete;
public:
static Single2 &GetInst(){
static Single2 single;
return single;
}
};
它的核心原理就是利用了函数局部静态变量的特性:它只会被初始化一次 。无论你调用 GetInst() 多少次,single 这个静态实例只会在第一次调用时被创建。
调用代码:
void test_single2(){
//多线程情况下可能存在问题
cout << "s1 addr is " << &Single2::GetInst() << endl;
cout << "s2 addr is " << &Single2::GetInst() << endl;
}
程序输出:
s1 addr is 0x7f8a1b402a10
s2 addr is 0x7f8a1b402a10
可以看到,两次获取到的实例地址是完全一样的。
需要注意的是,在 C++98 的年代,这种写法在多线程环境下是不安全的,可能会因为并发导致创建出多个实例 。但是随着 C++11 标准的到来,编译器对这里做了优化,保证了局部静态变量的初始化是线程安全的 。所以,在 C++11 及之后的版本,这已成为实现单例最受推崇的方式之一,兼具简洁与安全。
这种方式定义一个静态的类指针,并在程序启动时就立刻进行初始化,因此被称为“饿汉式”。
由于实例在主线程启动、其他业务线程开始前就已完成初始化,它自然地避免了多线程环境下的竞争问题。
//饿汉式
class Single2Hungry{
private:
Single2Hungry(){}
Single2Hungry(const Single2Hungry &) = delete;
Single2Hungry &operator=(const Single2Hungry &) = delete;
public:
static Single2Hungry *GetInst(){
if (single == nullptr){
single = new Single2Hungry();
}
return single;
}
private:
static Single2Hungry *single;
};
初始化和调用:
//饿汉式初始化,在.cpp文件中
Single2Hungry *Single2Hungry::single = Single2Hungry::GetInst();
void thread_func_s2(int i){
cout << "this is thread " << i << endl;
cout << "inst is " << Single2Hungry::GetInst() << endl;
}
void test_single2hungry(){
cout << "s1 addr is " << Single2Hungry::GetInst() << endl;
cout << "s2 addr is " << Single2Hungry::GetInst() << endl;
for (int i = 0; i < 3; i++){
thread tid(thread_func_s2, i);
tid.join();
}
}
int main(){
test_single2hungry();
}
程序输出:
s1 addr is 0x7fb3d6c00f00
s2 addr is 0x7fb3d6c00f00
this is thread 0
inst is 0x7fb3d6c00f00
this is thread 1
inst is 0x7fb3d6c00f00
this is thread 2
inst is 0x7fb3d6c00f00
饿汉式的优点是实现简单且线程安全。但其缺点也很明显:无论后续是否使用,实例在程序启动时都会被创建,可能造成不必要的资源开销。此外,通过裸指针 new 创建的实例,其内存释放时机难以管理,在复杂的多线程程序中极易引发内存泄漏或重复释放的严重问题。
与“饿汉”相对的就是“懒汉”,即只在第一次需要用的时候才去创建实例 。这能节省资源,但直接写在多线程下是有问题的。为解决其在多线程下的安全问题,一种名为双重检查锁定(Double-Checked Locking)的优化技巧应运而生。
//懒汉式指针,带双重检查锁定
class SinglePointer{
private:
SinglePointer(){}
SinglePointer(const SinglePointer &) = delete;
SinglePointer &operator=(const SinglePointer &) = delete;
public:
static SinglePointer *GetInst(){
// 第一次检查
if (single != nullptr){
return single;
}
s_mutex.lock();
// 第二次检查
if (single != nullptr){
s_mutex.unlock();
return single;
}
single = new SinglePointer();
s_mutex.unlock();
return single;
}
private:
static SinglePointer *single;
static mutex s_mutex;
};
//在.cpp文件中定义
SinglePointer *SinglePointer::single = nullptr;
std::mutex SinglePointer::s_mutex;
调用代码:
void thread_func_lazy(int i){
cout << "this is lazy thread " << i << endl;
cout << "inst is " << SinglePointer::GetInst() << endl;
}
void test_singlelazy(){
for (int i = 0; i < 3; i++){
thread tid(thread_func_lazy, i);
tid.join();
}
}
程序输出:
this is lazy thread 0
inst is 0x7f9e8a00bc00
this is lazy thread 1
inst is 0x7f9e8a00bc00
this is lazy thread 2
inst is 0x7f9e8a00bc00
该模式试图通过减少锁的持有时间来提升性能。然而,这种实现在C++中是存在严重缺陷的。new 操作并非原子性,它大致包含三个步骤:
编译器和处理器出于优化目的,可能对指令进行重排,导致第3步先于第2步完成 。若此时另一线程访问,它会获取一个非空但指向未完全构造对象的指针,进而引发未定义行为 。
为了安全地实现懒汉式加载,C++11 提供了 std::once_flag 和 std::call_once。call_once 能确保一个函数(或 lambda 表达式)在多线程环境下只被成功调用一次 。
// Singleton.h
#include
#include
class SingletonOnceFlag{
public:
static SingletonOnceFlag* getInstance(){
static std::once_flag flag;
std::call_once(flag, []{
_instance = new SingletonOnceFlag();
});
return _instance;
}
void PrintAddress() {
std::cout << _instance << std::endl;
}
~SingletonOnceFlag() {
std::cout << "this is singleton destruct" << std::endl;
}
private:
SingletonOnceFlag() = default;
SingletonOnceFlag(const SingletonOnceFlag&) = delete;
SingletonOnceFlag& operator=(const SingletonOnceFlag& st) = delete;
static SingletonOnceFlag* _instance;
};
// Singleton.cpp
#include "Singleton.h"
SingletonOnceFlag *SingletonOnceFlag::_instance = nullptr;
这样就完美解决了线程安全问题,但内存管理的问题依然存在。此时,std::shared_ptr 智能指针成为了理想的解决方案,它能实现所有权的共享和内存的自动回收。
智能指针版本:
// Singleton.h (智能指针版)
#include
class SingletonOnceFlag{
public:
static std::shared_ptr getInstance(){
static std::once_flag flag;
std::call_once(flag, []{
// 注意这里不能用 make_shared,因为构造函数是私有的
_instance = std::shared_ptr(new SingletonOnceFlag());
});
return _instance;
}
//... 其他部分相同
private:
//...
static std::shared_ptr _instance;
};
// Singleton.cpp (智能指针版)
#include "Singleton.h"
std::shared_ptr SingletonOnceFlag::_instance = nullptr;
测试代码:
#include "Singleton.h"
#include
#include
int main() {
std::mutex mtx;
std::thread t1([&](){
auto inst = SingletonOnceFlag::getInstance();
std::lock_guard lock(mtx);
inst->PrintAddress();
});
std::thread t2([&](){
auto inst = SingletonOnceFlag::getInstance();
std::lock_guard lock(mtx);
inst->PrintAddress();
});
t1.join();
t2.join();
return 0;
}
程序输出 (析构函数被正确调用):
0x7fde7b408c20
0x7fde7b408c20
this is singleton destruct
有些大佬追求极致的封装,他们会把析构函数也设为private,防止外部不小心 delete 掉单例实例 。但这样 shared_ptr 默认的删除器就无法调用析构了。解决办法:我们可以给 shared_ptr 指定一个自定义的删除器(Deleter),通常是一个函数对象(仿函数)。这个删除器类被声明为单例类的友元(friend),这样它就有了调用私有析构函数的权限。
// Singleton.h
class SingleAutoSafe; // 前置声明
// 辅助删除器
class SafeDeletor{
public:
void operator()(SingleAutoSafe *sf){
std::cout << "this is safe deleter operator()" << std::endl;
delete sf;
}
};
class SingleAutoSafe{
public:
static std::shared_ptr getInstance(){
static std::once_flag flag;
std::call_once(flag, []{
_instance = std::shared_ptr(new SingleAutoSafe(), SafeDeletor());
});
return _instance;
}
// 声明友元类,让 SafeDeletor 可以访问私有成员
friend class SafeDeletor;
private:
SingleAutoSafe() = default;
// 析构函数现在是私有的了
~SingleAutoSafe() {
std::cout << "this is singleton destruct" << std::endl;
}
// ...
static std::shared_ptr _instance;
};
程序输出:
0x7f8c0a509d30
0x7f8c0a509d30
this is safe deleter operator()
可以看到,程序结束时,shared_ptr 调用了我们的 SafeDeletor,从而安全地销毁了实例。这种方式提供了最强的封装性。
在大型项目中,为每个需要单例的类重复编写样板代码是低效的。更优雅的方案是定义一个通用的单例模板基类。任何类只需继承该基类,便能自动获得单例特性。这通常通过奇异递归模板模式实现,即派生类将自身作为模板参数传递给基类。
单例基类实现:
// Singleton.h
#include
#include
template
class Singleton {
protected:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton& st) = delete;
virtual ~Singleton() {
std::cout << "this is singleton destruct" << std::endl;
}
static std::shared_ptr _instance;
public:
static std::shared_ptr GetInstance() {
static std::once_flag s_flag;
std::call_once(s_flag, []() {
// new T 这里能成功,因为子类将基类设为了友元
_instance = std::shared_ptr(new T);
});
return _instance;
}
void PrintAddress() {
std::cout << _instance.get() << std::endl;
}
};
template
std::shared_ptr Singleton::_instance = nullptr;
使用这个模板基类:
现在,如果我们想让一个网络管理类 SingleNet 成为单例,只需要这样做:
// SingleNet.h
#include "Singleton.h"
// CRTP: SingleNet 继承了以自己为模板参数的 Singleton
class SingleNet : public Singleton{
// 将基类模板实例化后设为友元,这样基类的 GetInstance 才能 new 出 SingleNet
friend class Singleton;
private:
SingleNet() = default;
~SingleNet() {
std::cout << "SingleNet destruct " << std::endl;
}
};
测试代码:
// main.cpp
int main() {
std::thread t1([&](){
SingleNet::GetInstance()->PrintAddress();
});
std::thread t2([&](){
SingleNet::GetInstance()->PrintAddress();
});
t1.join();
t2.join();
return 0;
}
程序输出:
0x7f9a2d409f40
0x7f9a2d409f40
SingleNet destruct
this is singleton destruct
我们几乎没写任何单例相关的逻辑,只通过一次继承和一句友元声明,就让 SingleNet 变成了一个线程安全的、自动回收内存的单例类。这就是泛型编程的强大之处。
总结
本文介绍了单例模式从传统到现代的多种实现方式。可总结为: