当前位置: 首页 > news >正文

[设计模式]C++单例模式的几种写法以及通用模板

之前在这篇文章中简单的介绍了一下单例模式的作用和应用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 操作并非原子性,它大致包含三个步骤:

  •         1. 分配内存;
    •         2. 调用构造函数;
      •         3. 赋值给指针 。

编译器和处理器出于优化目的,可能对指令进行重排,导致第3步先于第2步完成 。若此时另一线程访问,它会获取一个非空但指向未完全构造对象的指针,进而引发未定义行为 。

 C++11的现代解决方案:once_flag 与智能指针

为了安全地实现懒汉式加载,C++11 提供了 std::once_flag 和 std::call_once。call_once 能确保一个函数(或 lambda 表达式)在多线程环境下只被成功调用一次 。

// Singleton.h
#include <mutex>
#include <iostream>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 <memory>class SingletonOnceFlag{
public:static std::shared_ptr<SingletonOnceFlag> getInstance(){static std::once_flag flag;std::call_once(flag, []{// 注意这里不能用 make_shared,因为构造函数是私有的_instance = std::shared_ptr<SingletonOnceFlag>(new SingletonOnceFlag());});return _instance;}//... 其他部分相同private://...static std::shared_ptr<SingletonOnceFlag> _instance;
};// Singleton.cpp (智能指针版)#include "Singleton.h"std::shared_ptr<SingletonOnceFlag> SingletonOnceFlag::_instance = nullptr;

测试代码:

#include "Singleton.h"
#include <thread>
#include <mutex>int main() {std::mutex mtx;std::thread t1([&](){auto inst = SingletonOnceFlag::getInstance();std::lock_guard<std::mutex> lock(mtx);inst->PrintAddress();});std::thread t2([&](){auto inst = SingletonOnceFlag::getInstance();std::lock_guard<std::mutex> 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<SingleAutoSafe> getInstance(){static std::once_flag flag;std::call_once(flag, []{_instance = std::shared_ptr<SingleAutoSafe>(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<SingleAutoSafe> _instance;};

程序输出:

0x7f8c0a509d30
0x7f8c0a509d30
this is safe deleter operator()

可以看到,程序结束时,shared_ptr 调用了我们的 SafeDeletor,从而安全地销毁了实例。这种方式提供了最强的封装性。


终极方案:基于CRTP的通用单例模板

在大型项目中,为每个需要单例的类重复编写样板代码是低效的。更优雅的方案是定义一个通用的单例模板基类。任何类只需继承该基类,便能自动获得单例特性。这通常通过奇异递归模板模式实现,即派生类将自身作为模板参数传递给基类。

单例基类实现:

// Singleton.h
#include <memory>
#include <mutex>template <typename T>
class Singleton {
protected:Singleton() = default;Singleton(const Singleton<T>&) = delete;Singleton& operator=(const Singleton<T>& st) = delete;virtual ~Singleton() {std::cout << "this is singleton destruct" << std::endl;}static std::shared_ptr<T> _instance;public:static std::shared_ptr<T> GetInstance() {static std::once_flag s_flag;std::call_once(s_flag, []() {// new T 这里能成功,因为子类将基类设为了友元_instance = std::shared_ptr<T>(new T);});return _instance;}void PrintAddress() {std::cout << _instance.get() << std::endl;}
};template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;

使用这个模板基类:

现在,如果我们想让一个网络管理类 SingleNet 成为单例,只需要这样做:

// SingleNet.h
#include "Singleton.h"// CRTP: SingleNet 继承了以自己为模板参数的 Singleton
class SingleNet : public Singleton<SingleNet>{// 将基类模板实例化后设为友元,这样基类的 GetInstance 才能 new 出 SingleNetfriend class Singleton<SingleNet>;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 变成了一个线程安全的、自动回收内存的单例类。这就是泛型编程的强大之处。


总结

本文介绍了单例模式从传统到现代的多种实现方式。可总结为:

  • 日常开发:对于C++11及以上版本,局部静态变量法是实现单例的首选,它兼具代码简洁性与线程安全性。
  • 深入理解:了解饿汉式、懒汉式及双重检查锁定的历史与缺陷,对于理解并发编程中的陷阱至关重要。
  • 企业级实践:在大型项目中,基于智能指针CRTP 的通用单例模板是最佳实践,它能提供类型安全、自动内存管理和最高的代码复用性。
http://www.lryc.cn/news/589471.html

相关文章:

  • Kubernetes 架构原理与集群环境部署
  • 降本增效!自动化UI测试平台TestComplete并行测试亮点
  • 2025最新国产用例管理工具评测:Gitee Test、禅道、蓝凌测试、TestOps 哪家更懂研发协同?
  • ESLint 除了在packages.json还能在哪里配置?
  • 实测两款效率工具:驾考刷题和证件照处理的免费方案
  • CF37E Trial for Chief 题解
  • 【LeetCode 热题 100】226. 翻转二叉树——DFS
  • Python 数据建模与分析项目实战预备 Day 6 - 多模型对比与交叉验证验证策略
  • Zookeeper入门安装与使用详解
  • CAS单点登录架构详解
  • 关于实习的经验贴
  • 鸿蒙和Android知识点
  • 软件测试面试经历分享?
  • iOS App 上架工具选型与跨平台开发 iOS 上架流程优化实录
  • 文心一言4.5企业级部署实战:多模态能力与Docker容器化测评
  • x86版的ubuntu上使用qemu运行arm版ubuntu
  • PHP连接MySQL数据库的多种方法及专业级错误处理指南
  • Postgresql源码(147)Nestloop流程与Instrumentation简单分析
  • python实现自动化sql布尔盲注(二分查找)
  • 03 51单片机之独立按键控制LED状态
  • 论文 视黄素与细胞修复
  • 小型客厅如何装修设计?
  • 微信小程序开发-桌面端和移动端UI表现不一致问题记录
  • [ROS 系列学习教程] ROS动作通讯(Action):通信模型、Hello World与拓展
  • Linux操作系统之信号:保存与处理信号
  • Quick API:赋能能源行业,化解数据痛点
  • MongoDB 学习笔记
  • 三年高端随身WIFI销量冠军如何用“技术普惠”击穿行业暴利,用户直呼真香;随身WIFI哪个牌子性价比最高?
  • netstat -tlnp | grep 5000
  • 基于Python的图像文字识别系统