C++ 单例模式

一、「单例模式」的核心定义 & 分类

1. 单例模式的核心目标

保证一个类在整个程序的生命周期内,永远只有唯一的一个实例对象 ,并且提供一个全局唯一的访问入口,全局任何地方调用这个入口,拿到的都是同一个对象。

2. 单例的两大基础分类
饿汉式(饿汉单例):程序启动即初始化,天生线程安全
  • 核心逻辑:在程序进程启动、main 函数执行之前,就完成单例对象的初始化;
  • 线程安全原因:C++ 的全局变量 / 静态成员变量的初始化,是在主线程执行 main 函数前的初始化阶段完成的,此时所有业务线程都还没创建,自然不存在多线程竞争问题,天生线程安全。
懒汉式(懒汉单例):第一次调用才初始化,按需加载,需要手动做线程安全
  • 核心逻辑:延迟初始化 ,单例对象的创建时机不是程序启动,而是「第一次调用getInstance()获取实例」的时候才创建;
  • 线程不安全根源:多线程环境下,如果多个线程同时第一次调用getInstance() ,会同时走到「对象未创建」的逻辑,导致多个线程同时创建对象,最终出现多个实例,违背单例的核心原则。
3. 内存池为什么「几乎不用饿汉式」,首选懒汉式?

内存池是高性能 的核心组件,饿汉式的设计理念和内存池的诉求完全相悖

  • 内存池是「按需使用」的组件:很多服务器程序启动后,可能很久才会分配内存,饿汉式在程序启动就创建内存池实例,会浪费内存资源
  • 服务器启动速度要求极致:饿汉式会增加程序启动的初始化耗时,服务器追求「秒启动」,能延迟的初始化绝对不提前做;
  • 内存池的实例化有一定开销:内存池内部会初始化内存块、空闲链表等结构,提前初始化会无意义占用 CPU 和内存。

二、C++ 中「线程安全单例模式」的所有主流实现方式

统一前提:所有单例模式,都必须 私有化构造函数、拷贝构造、赋值运算符重载 ------ 这是单例的「铁律」,目的是禁止外部通过new、拷贝、赋值的方式创建对象 ,唯一的创建入口只能是类的getInstance()静态方法。

必写的私有化代码(所有方案通用):

cpp 复制代码
class MemoryPool {
private:
    // 1. 私有化构造函数:禁止外部 new MemoryPool()
    MemoryPool() = default;
    // 2. 私有化拷贝构造:禁止外部拷贝实例
    MemoryPool(const MemoryPool&) = delete;
    // 3. 私有化赋值重载:禁止外部赋值实例
    MemoryPool& operator=(const MemoryPool&) = delete;
public:
    // 全局唯一访问入口
    static MemoryPool& getInstance();
};
方式一:饿汉式单例(天生线程安全)
实现代码
cpp 复制代码
class MemoryPool {
private:
    MemoryPool() = default;
    MemoryPool(const MemoryPool&) = delete;
    MemoryPool& operator=(const MemoryPool&) = delete;

    // 核心:静态成员变量,程序启动时初始化,全局唯一
    static MemoryPool instance_; 
public:
    static MemoryPool& getInstance() {
        return instance_; // 直接返回已初始化的实例,无任何逻辑
    }
};
// 全局初始化:必须在类外定义静态成员变量
MemoryPool MemoryPool::instance_;
优点
  1. 实现最简单,一行核心逻辑搞定,无任何线程安全问题;
  2. 调用getInstance()的效率极高,运行期零开销,只是简单的返回引用。
缺点
  1. 内存浪费:程序启动即初始化,不管后续是否使用内存池,实例都会一直占用内存;
  2. 启动耗时增加:内存池内部初始化(空闲链表、内存块)会占用启动时间;
  3. 无法处理依赖问题:如果内存池依赖其他组件(比如配置类),可能出现「内存池先初始化,依赖组件后初始化」的初始化顺序问题(C++ 全局变量初始化顺序未定义)。
适用场景 & 内存池适配性
  • 适用:简单的工具类、无依赖、初始化开销极小的场景;
  • 内存池适配性:不推荐,几乎不用 ------ 违背内存池「按需加载、极致性能」的核心诉求。
方式二:C++11静态局部变量的懒汉式单例

C++11 标准明确规定 :在函数内定义的静态局部变量 ,其初始化过程是线程安全的!编译器会自动为静态局部变量的初始化逻辑,插入「隐式的互斥锁」和「内存屏障」,保证:

  1. 只有第一个调用函数的线程能执行初始化逻辑;
  2. 其他线程会阻塞等待,直到初始化完成;
  3. 初始化完成后,所有线程直接返回已创建的实例,无任何锁开销。
实现代码
cpp 复制代码
class MemoryPool {
private:
    // 铁律:私有化三大构造,禁止外部创建
    MemoryPool() = default;
    MemoryPool(const MemoryPool&) = delete;
    MemoryPool& operator=(const MemoryPool&) = delete;

public:
    // 全局唯一访问入口:一行代码实现线程安全懒汉单例,极致简洁!
    static MemoryPool& getInstance() {
        static MemoryPool instance; // C++11静态局部变量,线程安全初始化
        return instance;
    }
};
优点 1:【天生线程安全】,无锁、无漏洞、无开销

编译器原生保证线程安全,不需要手动加std::mutex彻底消除锁的性能开销 ------ 这是内存池的核心诉求,内存池的allocate/deallocate是服务器的性能热点,任何额外开销都不能有。

优点 2:【完美懒加载】,按需初始化,零内存浪费

只有第一次调用MemoryPool::getInstance()时,才会创建内存池实例;如果程序全程不用内存池,实例永远不会创建,完全契合内存池「按需使用」的设计理念。

优点 3:【极致简洁】

核心逻辑只有一行代码,没有指针、没有锁、没有静态成员变量的类外初始化,几乎不可能写出 bug,后续维护成本为零。

优点 4:【运行期零开销】,调用效率媲美饿汉式

初始化完成后,每次调用getInstance(),只是简单的返回静态变量的引用,没有任何判断、没有任何锁、没有任何计算,CPU 直接执行,效率拉满。

优点 5:【自动内存释放】,无内存泄漏风险

静态局部变量的生命周期是「从初始化到程序退出」,程序结束时,编译器会自动调用析构函数释放内存池的资源(比如归还内存块、清理空闲链表),不需要手动管理内存,彻底解决了「懒汉式单例内存泄漏」的问题。

优点 6:【无初始化顺序问题】

静态局部变量的初始化时机是「第一次调用函数时」,而非程序启动时,完美解决了饿汉式的「全局变量初始化顺序未定义」的问题。

  • 适用:所有 C++11 及以后的项目,所有场景,无例外
  • 内存池适配性:唯一首选,没有之一 ------ 完美匹配内存池「高性能、线程安全、懒加载、零开销、无内存泄漏」的所有核心诉求。
方式三:模板封装的单例基类
实现核心

这是方式三的升级版 ,本质还是「C++11 静态局部变量的懒汉单例」,只是把单例的逻辑封装成一个通用的模板基类Singleton ,所有需要单例的类(比如内存池、日志器、配置类)都可以继承这个基类,不用重复写单例的代码,实现「单例逻辑复用」。

实现代码
cpp 复制代码
// 通用单例模板基类:所有需要单例的类都可以继承它
template <typename T>
class Singleton {
protected:
    // 保护构造:允许子类继承,禁止外部创建
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    // 全局唯一访问入口,复用C++11静态局部变量的线程安全逻辑
    static T& getInstance() {
        static T instance;
        return instance;
    }
};

// 内存池类:继承单例模板,一行代码实现单例,无需自己写getInstance()
class MemoryPool : public Singleton<MemoryPool> {
    // 声明友元:让基类能访问子类的私有构造函数
    friend class Singleton<MemoryPool>;
private:
    // 私有化构造:保证单例的核心约束
    MemoryPool() = default;
public:
    // 内存池的业务接口
    void* allocate(size_t size);
    void deallocate(void* ptr);
};
调用方式
cpp 复制代码
// 全局调用内存池实例,和方式三完全一致
void* buf = MemoryPool::getInstance().allocate(1024);
MemoryPool::getInstance().deallocate(buf);
优点
  1. 完全继承方式三的所有优点(线程安全、懒加载、零开销、自动释放);
  2. 极致复用代码:服务器中有很多需要单例的组件(内存池、日志器、配置类、EventLoop),只需要继承模板基类,不用重复写单例逻辑;
  3. 代码解耦:单例的逻辑被封装在模板中,业务类(内存池)只需要关注自己的业务逻辑,职责单一。

三、内存池项目里「为什么必选 C++11 静态局部变量的懒汉单例」?

理由 1:【内存池是高性能核心组件,零锁开销是硬性要求】

内存池是服务器的性能基石allocate/deallocate的调用频率是百万次 / 秒甚至千万次 / 秒 ,加锁的懒汉式会引入「锁竞争、上下文切换」的性能开销,这个开销会被无限放大,直接导致服务器的并发能力下降;而 C++11 静态局部变量的单例,无任何锁开销,初始化后调用效率极致,完美匹配内存池的性能诉求。

理由 2:【内存池需要懒加载,避免启动时的资源浪费】

内存池内部会初始化「空闲内存块、内存链表、对齐页」等结构,这些结构会占用内存和 CPU 资源;如果用饿汉式,程序启动就初始化内存池,而服务器可能很久才会分配内存,这是无意义的资源浪费。C++11 的懒汉单例是「按需初始化」,第一次分配内存时才创建实例,完美契合内存池的使用场景。

理由 3:【内存池需要自动释放资源,无内存泄漏风险】

内存池管理着大量的堆内存块,如果单例对象的析构函数不被调用,会导致内存泄漏;C++11 的静态局部变量是「全局生命周期」,程序退出时编译器会自动调用内存池的析构函数,清理所有内存资源,无需手动处理,安全可靠。

理由 4:【代码极简,无 bug,维护成本低】

内存池是服务器的核心组件,稳定性优先于一切,C++11 的单例实现只有一行核心代码,几乎不可能写出 bug;而加锁的懒汉式需要处理锁、指针、双重检查,容易出现逻辑漏洞,维护成本高。

理由 5:【天然解决线程安全问题,无历史漏洞】

C++11 的标准保证了静态局部变量的线程安全初始化,不存在「指令重排、半初始化对象」的问题,比加锁的懒汉式更可靠,这对于多线程高并发的服务器来说,是「零风险」的保障。

理由 6:【适配服务器的 C++ 版本环境】

现在的高性能服务器项目,全部采用 C++11 及以上标准(C++11 是服务器开发的「标配」),完全不用担心兼容性问题,这也是该方案能成为主流的前提。

相关推荐
Yu_Lijing2 小时前
基于C++的《Head First设计模式》笔记——适配器模式
c++·笔记·设计模式
点云SLAM2 小时前
C++ 设计模式之工厂模式(Factory)和面试问题
开发语言·c++·设计模式·面试·c++11·工厂模式
玖釉-2 小时前
[Vulkan 学习之路] 05 - 缔结契约:创建逻辑设备 (Logical Device)
c++·windows·图形渲染
彩妙不是菜喵2 小时前
c++:初阶/初始模版
开发语言·c++
想唱rap2 小时前
MySQL表得内外连接
服务器·数据库·c++·mysql·ubuntu
A7bert7772 小时前
【DeepSeek R1部署至RK3588】RKLLM转换→板端部署→局域网web浏览
c++·人工智能·深度学习·ubuntu·自然语言处理·nlp
星河耀银海2 小时前
C++基础数据类型与变量管理:内存安全与高效代码的基石
java·开发语言·c++
小欣加油2 小时前
leetcode 面试题17.16 按摩师
数据结构·c++·算法·leetcode·动态规划
CSDN_RTKLIB2 小时前
【字符编码】文本文件与二进制文件
c++·qt