C++ 单例模式:饿汉与懒汉模式

设计模式是软件工程中沉淀的 "代码兵法",而单例模式是最常用的创建型设计模式之一。

它的核心目标是:保证一个类在整个程序生命周期中只有一个实例,并提供全局访问点,适用于配置管理、资源池、日志管理等需要全局唯一实例的场景。

本文将详细讲解单例模式的两种核心实现(饿汉模式、懒汉模式),包括设计思路、代码实现、优缺点及线程安全优化。

一、单例模式的核心价值

在复杂系统中,单例模式能解决以下关键问题:

  • 资源统一管理:如服务器配置文件读取,单例对象统一加载配置,避免多实例重复读取 / 修改导致的数据不一致;
  • 性能优化:避免频繁创建 / 销毁重量级对象(如数据库连接、网络套接字),降低系统开销;
  • 全局访问:提供统一的实例访问入口,简化模块间的交互。

单例模式的实现需满足三个核心约束:

  1. 构造函数私有化,禁止外部直接创建对象;
  2. 禁用拷贝构造和赋值运算符,防止通过拷贝生成多实例;
  3. 提供静态成员函数,作为获取唯一实例的全局入口。

二、饿汉模式:程序启动即初始化

1. 核心思路

"饿汉" 顾名思义 ------程序启动时(main 函数执行前)就创建唯一实例,无论后续是否使用。利用全局静态变量的初始化特性,天然保证实例唯一性。

2. 完整代码实现

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 单例模式 - 饿汉模式
class Singleton 
{
public:
    // 全局访问点:返回唯一实例的指针/引用
    static Singleton* GetInstance() 
    {
        return &m_instance;
    }

    // 示例:测试成员函数
    void PrintInfo() 
    {
        cout << "Singleton Instance Address: " << this << endl;
    }

private:
    // 1. 私有化构造函数:禁止外部创建对象
    Singleton() 
    {
        cout << "饿汉模式:Singleton 构造函数调用" << endl;
    }

    // 2. 禁用拷贝构造和赋值运算符(C++98/C++11 两种写法)
    // C++98:私有 + 只声明不定义
    // Singleton(const Singleton&);
    // Singleton& operator=(const Singleton&);

    // C++11:显式删除(推荐)
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 3. 静态成员变量:程序启动时初始化,保证唯一
    static Singleton m_instance;
};

// 全局静态成员初始化(main函数前执行)
Singleton Singleton::m_instance;

// 测试代码
int main() 
{
    // 获取两个实例指针,验证地址一致
    Singleton* ptr1 = Singleton::GetInstance();
    Singleton* ptr2 = Singleton::GetInstance();

    ptr1->PrintInfo(); // 输出:Singleton Instance Address: 0xXXXXXXX
    ptr2->PrintInfo(); // 输出:与ptr1完全相同

    // 验证拷贝/赋值被禁用(编译报错)
    // Singleton obj = *ptr1; // 拷贝构造被删除
    // *ptr2 = *ptr1;         // 赋值运算符被删除

    return 0;
}

3. 优缺点分析

优点 缺点
实现简单,无线程安全问题(静态变量初始化由编译器保证原子性) 程序启动时即初始化,若实例未被使用则浪费内存
多线程高并发场景下响应快(无需加锁) 多个单例类的初始化顺序无法控制
无内存泄漏风险(静态变量由系统自动销毁) 若实例构造耗时(如加载大文件),会导致程序启动慢

4. 适用场景

  • 单例对象构造耗时短、占用资源少;
  • 程序运行期间必然会使用该实例;
  • 多线程高并发场景(避免懒汉模式的加锁开销)。

三、懒汉模式:延迟加载(按需初始化)

1. 核心思路

"懒汉" 即 "懒加载"------第一次调用 GetInstance 时才创建实例 ,程序启动时不初始化,避免资源浪费。核心挑战是解决多线程下的线程安全问题

2. 线程安全的完整实现(Double-Check 优化)

cpp 复制代码
#include <iostream>
#include <string>
#include <mutex>
#include <thread>
using namespace std;

// 单例模式 - 懒汉模式(线程安全版)
class Singleton 
{
public:
    // 全局访问点:Double-Check 加锁,保证效率+线程安全
    static Singleton* GetInstance() 
    {
        // 第一次检查:避免每次调用都加锁(提升效率)
        if (nullptr == m_pInstance) 
        {
            m_mtx.lock(); // 加锁:保证多线程下只有一个线程创建实例
            // 第二次检查:防止多个线程等待锁后重复创建
            if (nullptr == m_pInstance) 
            {
                m_pInstance = new Singleton();
                cout << "懒汉模式:Singleton 构造函数调用" << endl;
            }
            m_mtx.unlock();
        }
        return m_pInstance;
    }

    // 内嵌垃圾回收类:解决单例对象的内存泄漏问题
    class CGarbo 
    {
    public:
        ~CGarbo() 
        {
            // 程序结束时自动调用析构,释放单例对象
            if (Singleton::m_pInstance) 
            {
                delete Singleton::m_pInstance;
                Singleton::m_pInstance = nullptr;
                cout << "懒汉模式:Singleton 析构函数调用" << endl;
            }
        }
    };

    // 静态垃圾回收对象:程序结束时系统自动销毁
    static CGarbo Garbo;

    // 示例:测试成员函数
    void PrintInfo() 
    {
        cout << "Singleton Instance Address: " << this << endl;
    }

private:
    // 1. 私有化构造函数
    Singleton() {}

    // 2. 禁用拷贝构造和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 3. 静态成员变量
    static Singleton* m_pInstance; // 单例对象指针(初始化为nullptr)
    static mutex m_mtx;            // 互斥锁:保证线程安全
};

// 静态成员初始化
Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Singleton::Garbo;
mutex Singleton::m_mtx;

// 多线程测试函数
void ThreadFunc() 
{
    Singleton* ptr = Singleton::GetInstance();
    ptr->PrintInfo();
}

// 测试代码
int main() {
    // 创建多个线程,验证实例唯一性
    thread t1(ThreadFunc);
    thread t2(ThreadFunc);
    thread t3(ThreadFunc);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

3. 核心设计解析

(1)Double-Check 加锁机制
  • 第一次检查(无锁):若实例已创建,直接返回,避免每次调用都加锁(提升高并发场景下的效率);
  • 加锁:保证多线程下只有一个线程进入实例创建逻辑;
  • 第二次检查(加锁后):防止多个线程等待锁后重复创建实例(比如线程 A 创建实例前,线程 B 已等待锁,A 创建完成后 B 若不检查会再次创建)。
(2)内嵌垃圾回收类(CGarbo)

懒汉模式使用 new 创建堆对象,若手动调用 delete 易遗漏,导致内存泄漏。内嵌垃圾回收类的核心逻辑:

  • CGarbo 是静态成员对象,程序结束时系统会自动调用其析构函数;
  • 析构函数中删除单例对象,保证资源正常释放。

4. 优缺点分析

优点 缺点
延迟加载:仅在第一次使用时初始化,节省内存 实现复杂,需处理线程安全和内存泄漏问题
程序启动快,无初始化负载 Double-Check 加锁存在微小的性能开销(远低于每次加锁)
多个单例类的初始化顺序可自由控制 C++11 前可能存在指令重排导致的线程安全隐患(需 volatile 优化)

5. 适用场景

  • 单例对象构造耗时久、占用资源多(如加载插件、初始化网络连接);
  • 程序运行期间可能不使用该实例;
  • 对程序启动速度要求高。

四、饿汉 vs 懒汉:核心对比

特性 饿汉模式 懒汉模式
初始化时机 程序启动时(main 前) 第一次调用 GetInstance 时
线程安全 天然安全(编译器保证) 需加锁(Double-Check)
内存占用 启动即占用,可能浪费 按需占用,更节省
程序启动速度 可能变慢(构造耗时) 快(无初始化负载)
实现复杂度 简单(几行代码) 复杂(加锁 + 垃圾回收)
初始化顺序 无法控制 可自由控制

五、进阶优化:C++11 简化版懒汉模式

C++11 规定:局部静态变量的初始化是线程安全的,可利用这一特性实现极简的懒汉模式:

cpp 复制代码
class Singleton 
{
public:
    static Singleton& GetInstance() 
    {
        // 局部静态变量:第一次调用时初始化,线程安全
        static Singleton instance;
        return instance;
    }

    // 禁用拷贝构造和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() {} // 私有化构造函数
};

// 测试调用
Singleton& instance = Singleton::GetInstance();

该方案无需加锁、无需垃圾回收类,兼顾简洁性和线程安全,是 C++11 及以上版本的首选懒汉模式实现。

六、注意事项

  1. 禁用拷贝 / 赋值 :无论哪种模式,必须禁用拷贝构造和赋值运算符,否则可能通过 **Singleton obj = *GetInstance();**生成多实例;
  2. 线程安全:懒汉模式务必处理线程安全问题,避免多线程下创建多个实例;
  3. 内存泄漏:懒汉模式需保证堆对象正常释放(推荐内嵌垃圾回收类或 C++11 局部静态变量);
  4. 单例滥用:单例模式会增加代码耦合性,非必要场景(如可通过参数传递的对象)避免使用。

总结

  1. 单例模式的核心是构造函数私有化 + 禁用拷贝 + 静态全局访问点,保证实例唯一性;
  2. 饿汉模式简单、线程安全,但可能浪费资源,适用于轻量级实例;
  3. 懒汉模式延迟加载、节省资源,需处理线程安全和内存泄漏,C++11 可通过局部静态变量简化实现;
  4. 选择哪种模式取决于实例的构造开销、使用频率及程序启动性能要求。
相关推荐
echome8882 小时前
Go 语言并发编程实战:用 Goroutine 和 Channel 构建高性能任务调度器
开发语言·后端·golang
l1t2 小时前
与系统库同名python脚本文件引起的奇怪错误及其解决
开发语言·数据库·python
Jackey_Song_Odd2 小时前
Part 1:Python语言核心 - 内建数据类型
开发语言·python
切糕师学AI2 小时前
编程语言 Erlang 简介
开发语言·erlang
sycmancia2 小时前
C++——C++中的类型识别
开发语言·c++
还是大剑师兰特2 小时前
Vue3 按钮切换示例(启动 / 关闭互斥显示)
开发语言·javascript·vue.js
星空露珠2 小时前
迷你世界UGC3.0脚本Wiki角色模块管理接口 Actor
开发语言·数据库·算法·游戏·lua
我星期八休息2 小时前
深入理解哈希表
开发语言·数据结构·c++·算法·哈希算法·散列表
寻寻觅觅☆2 小时前
东华OJ-进阶题-19-排队打水问题(C++)
开发语言·c++·算法