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 标准保证
析构顺序 可能引发跨翻译单元的 静态初始化顺序问题 无此问题
相关推荐
想唱rap34 分钟前
IO多路转接之poll
服务器·开发语言·数据库·c++
落羽的落羽2 小时前
【算法札记】练习 | Week4
linux·服务器·数据结构·c++·人工智能·算法·动态规划
goodesocket2 小时前
芯片HAST测试:通电工作下如何精准模拟极端环境挑战?
c++
特种加菲猫3 小时前
从零开始手撕AVL树:详解插入、平衡因子更新与四种旋转
开发语言·c++
萑澈3 小时前
算法竞赛入门:C++ STL核心用法与时空复杂度速查手册
数据结构·c++·算法·stl
江屿风3 小时前
C++OJ题经验总结(竞赛)1
开发语言·c++·笔记·算法
运筹vivo@4 小时前
LeetCode 2405. 子字符串的最优划分
c++·算法·leetcode·职场和发展·哈希表
有点。4 小时前
C++(枚举法一练习题)
开发语言·c++·算法
basketball6164 小时前
C++ 单例模式完全指南:从饿汉式到现代 C++ 的最佳实践
java·c++·单例模式
玖釉-5 小时前
栈——栈的定义及基本操作
c++·windows·算法·图形渲染