C++ 单例模式

C++ 单例模式(Singleton Pattern)完整总结

目录

  • [1. 什么是单例模式](#1. 什么是单例模式)
  • [2. 饿汉模式 vs 懒汉模式](#2. 饿汉模式 vs 懒汉模式)
  • [3. 懒汉模式的线程安全问题与解决方案](#3. 懒汉模式的线程安全问题与解决方案)
  • [4. 编程练习:Logger 单例](#4. 编程练习:Logger 单例)

1. 什么是单例模式

单例模式(Singleton Pattern) 确保一个类在整个程序生命周期中 只有一个实例 ,并提供一个 全局访问点

核心要素:

  1. 私有/保护构造函数 --- 阻止外部 new
  2. 静态成员变量 --- 保存唯一实例
  3. 静态方法 instance() --- 全局获取入口

2. 饿汉模式 vs 懒汉模式

特征 饿汉模式(Eager) 懒汉模式(Lazy)
初始化时机 程序启动 / 类加载时 首次调用 instance()
典型写法 static T* m_inst = new T(); if (!m_inst) m_inst = new T();
线程安全 ✅ 天然安全 ❌ 需加锁或用 call_once
资源占用 不管用不用都创建 用时才创建,节省资源
适用场景 实例轻量、一定会用 实例较重、可能不使用

3. 懒汉模式的线程安全问题与解决方案

3.1 问题描述

经典指针式懒汉在多线程下存在竞态条件:

cpp 复制代码
CHwrelDataBase* CHwrelDataBase::instance() {
    if (m_instance == nullptr) {              // 线程A 判断为空
        m_instance = new CHwrelDataBase();    // 线程B 也判断为空,重复创建!
    }
    return m_instance;
}

线程A 刚判断完 nullptr、还没赋值,线程B 也进来判断为空 → 创建两个实例,还可能内存泄漏。

3.2 四种解决方案

方案一:加互斥锁(最直接,但性能差)
cpp 复制代码
std::mutex mtx;

T* instance() {
    std::lock_guard<std::mutex> lock(mtx);  // 每次调用都加锁
    if (!m_instance)
        m_instance = new T();
    return m_instance;
}

⚠️ 缺点:实例已创建后,每次获取仍加锁,性能浪费

方案二:双重检查锁(DCLP)
cpp 复制代码
std::mutex mtx;
std::atomic<T*> m_instance{nullptr};

T* instance() {
    T* tmp = m_instance.load(std::memory_order_acquire);   // 第一次无锁检查
    if (!tmp) {
        std::lock_guard<std::mutex> lock(mtx);
        tmp = m_instance.load(std::memory_order_relaxed);  // 第二次加锁检查
        if (!tmp) {
            tmp = new T();
            m_instance.store(tmp, std::memory_order_release);
        }
    }
    return tmp;
}

✅ 只在首次创建时加锁,之后走快速路径。

⚠️ 必须用 atomic + 正确的内存序,否则编译器/CPU 指令重排会导致返回未构造完成的对象。

方案三:std::call_once
cpp 复制代码
std::once_flag flag;
T* m_instance = nullptr;

T* instance() {
    std::call_once(flag, []() {
        m_instance = new T();
    });
    return m_instance;
}

✅ 标准库保证只执行一次,简洁安全。

方案四:Meyer's Singleton --- C++11 最佳实践 ⭐
cpp 复制代码
T& instance() {
    static T obj;   // C++11 标准保证:局部静态变量的初始化是线程安全的
    return obj;
}

最推荐。C++11 标准 §6.7 规定:如果多个线程同时进入,只有一个线程执行初始化,其他线程阻塞等待。

3.3 方案对比

方案 线程安全 性能 复杂度 推荐度
无保护 ⭐⭐⭐
全局锁 ⚠️
双重检查锁 (DCLP) ⭐⭐⭐ ⚠️ 易写错
std::call_once ⭐⭐⭐
Meyer's Singleton ⭐⭐⭐ 最低 ⭐⭐⭐

4. 编程练习:Logger 单例

题目: 设计一个 Logger 类,要求:

  1. 全局唯一实例,记录日志消息到内部缓冲区
  2. 支持 log(message) 写入和 dump() 打印所有日志
  3. 分别用 饿汉模式懒汉模式(Meyer's Singleton) 实现
  4. main() 中验证两种实现都返回同一个实例

参考答案

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>

// ============================================================
//  方式一:饿汉模式(Eager Singleton)
//  实例在程序启动时(main 之前)就已创建
// ============================================================
class EagerLogger
{
public:
    static EagerLogger& instance()
    {
        return s_instance;   // 直接返回,已在 main() 前构造完毕
    }

    void log(const std::string& msg)
    {
        m_logs.push_back("[Eager] " + msg);
    }

    void dump() const
    {
        for (const auto& entry : m_logs)
            std::cout << entry << "\n";
    }

    EagerLogger(const EagerLogger&) = delete;
    EagerLogger& operator=(const EagerLogger&) = delete;

private:
    EagerLogger() { std::cout << ">>> EagerLogger constructed\n"; }

    std::vector<std::string> m_logs;
    static EagerLogger s_instance;   // 声明静态成员
};

// 关键:类外定义 --- 程序启动时即构造(饿汉的核心)
EagerLogger EagerLogger::s_instance;

// ============================================================
//  方式二:懒汉模式 - Meyer's Singleton(Lazy Singleton)
//  实例在首次调用 instance() 时才创建,C++11 线程安全
// ============================================================
class LazyLogger
{
public:
    static LazyLogger& instance()
    {
        static LazyLogger s_instance;  // 首次调用时构造,C++11 保证线程安全
        return s_instance;
    }

    void log(const std::string& msg)
    {
        m_logs.push_back("[Lazy] " + msg);
    }

    void dump() const
    {
        for (const auto& entry : m_logs)
            std::cout << entry << "\n";
    }

    LazyLogger(const LazyLogger&) = delete;
    LazyLogger& operator=(const LazyLogger&) = delete;

private:
    LazyLogger() { std::cout << ">>> LazyLogger constructed\n"; }

    std::vector<std::string> m_logs;
};

// ============================================================
//  main:验证
// ============================================================
int main()
{
    std::cout << "===== main() started =====\n\n";

    // --- 饿汉 ---
    EagerLogger& e1 = EagerLogger::instance();
    EagerLogger& e2 = EagerLogger::instance();
    e1.log("Hello from e1");
    e2.log("Hello from e2");
    std::cout << "EagerLogger same instance? "
              << (&e1 == &e2 ? "YES" : "NO") << "\n";
    e1.dump();

    std::cout << "\n";

    // --- 懒汉 ---
    LazyLogger& l1 = LazyLogger::instance();
    LazyLogger& l2 = LazyLogger::instance();
    l1.log("Hello from l1");
    l2.log("Hello from l2");
    std::cout << "LazyLogger same instance? "
              << (&l1 == &l2 ? "YES" : "NO") << "\n";
    l1.dump();

    return 0;
}

预期输出

复制代码
>>> EagerLogger constructed        ← 饿汉:main() 之前就已构造
===== main() started =====

EagerLogger same instance? YES
[Eager] Hello from e1
[Eager] Hello from e2

>>> LazyLogger constructed         ← 懒汉:首次调用 instance() 时才构造
LazyLogger same instance? YES
[Lazy] Hello from l1
[Lazy] Hello from l2

要点总结

对比项 饿汉 EagerLogger 懒汉 LazyLogger
实例位置 类的静态成员,类外定义 函数局部 static 变量
构造时机 main() 之前 首次调用 instance()
输出顺序 构造信息在 main started 之前 构造信息在 main started 之后
线程安全 ✅ 天然安全(无竞态) ✅ C++11 标准保证
析构顺序 可能引发跨翻译单元的 静态初始化顺序问题 无此问题
相关推荐
Brilliantwxx1 小时前
【C++】认识标准库STL(2)
开发语言·c++
故事还在继续吗2 小时前
STL 容器算法手册
开发语言·c++·算法
啊我不会诶2 小时前
2023西安邀请赛vp补题
c++·算法
唠玖馆2 小时前
c++ list详解
c++
khalil10202 小时前
代码随想录算法训练营Day-38动态规划06 | 322. 零钱兑换、279.完全平方数、139.单词拆分、多重背包、总结
数据结构·c++·算法·leetcode·动态规划
6Hzlia2 小时前
Hot 100 刷题计划】 LeetCode 146. LRU 缓存 | C++ 哈希表+双向链表
c++·leetcode·缓存
风筝在晴天搁浅2 小时前
手撕单例模式
java·开发语言·单例模式
我不是懒洋洋2 小时前
【数据结构】二叉树OJ(单值二叉树、检查两棵树是否相同、对称二叉树、二叉树的前序遍历、另一颗树的子树)
c语言·数据结构·c++·经验分享·算法·leetcode·visual studio
wljy12 小时前
每日一题(2026.4.29) 猫猫与数学
c语言·c++·算法·蓝桥杯·stl·牛客