C++ 设计模式系列:单例模式

🔧 C++ 设计模式系列:单例模式完全指南

📅 更新时间:2025年12月14日

🏷️ 标签:C++ | 设计模式 | 单例模式 | 线程安全 | 音视频开发

文章目录


📖 什么是单例模式

1. 生活中的例子

想象一个公司的CEO:

复制代码
整个公司  →  [ 唯一的CEO ]  →  所有员工
                 (单例)         都向同一个CEO汇报

关键点

  • 一个公司只有一个CEO
  • 所有人访问的都是同一个CEO
  • CEO在公司成立时就确定,不会随意更换

2. 程序中的定义

单例模式(Singleton Pattern) 是一种创建型设计模式:

复制代码
全局访问点  →  [ 唯一实例 ]  →  所有调用者
getInstance()     单例对象      共享同一个对象

核心要素

  1. 私有构造函数:防止外部创建实例
  2. 私有静态实例:保存唯一的对象
  3. 公有静态方法:提供全局访问点
  4. 禁止拷贝和赋值:确保唯一性

3. 模式结构图

getInstance getInstance getInstance Singleton 调用者1 调用者2 调用者3


🎯 为什么需要这个模式

官方定义

根据 GoF(Gang of Four)《设计模式》原书的定义:

Intent : "Ensure a class has only one instance, and provide a global point of access to it."

(确保一个类只有一个实例,并提供一个全局访问点)

核心动机

"The most common reason for this is to control access to some shared resource --- for example, a database or a file."

(最常见的原因是控制对共享资源的访问------例如数据库或文件)


问题场景

生活中的例子:打印机

复制代码
办公室场景:
┌─────────────────────────────────────────────────────┐
│                    一台打印机                         │
│                        🖨️                           │
│                                                     │
│   员工A ──打印──┐                                    │
│   员工B ──打印──┼──→ 【打印队列】 ──→ 按顺序输出      │
│   员工C ──打印──┘                                    │
└─────────────────────────────────────────────────────┘

如果没有统一管理:
   员工A 直接打印 ──→ ╲
   员工B 直接打印 ──→  ╲ 打印内容混乱、卡纸、冲突!
   员工C 直接打印 ──→  ╱

❌ 没有单例模式

cpp 复制代码
// 多个打印管理器实例,导致打印任务混乱
class PrinterSpooler {
public:
    void addJob(const std::string& document) {
        jobQueue_.push(document);
    }

    void processJobs() {
        while (!jobQueue_.empty()) {
            std::cout << "打印: " << jobQueue_.front() << std::endl;
            jobQueue_.pop();
        }
    }

private:
    std::queue<std::string> jobQueue_;
};

int main() {
    PrinterSpooler spooler1;  // 队列1
    PrinterSpooler spooler2;  // 队列2 - 又一个队列!
    PrinterSpooler spooler3;  // 队列3 - 再来一个!

    spooler1.addJob("文档A");
    spooler2.addJob("文档B");  // 进入了不同的队列!
    spooler3.addJob("文档C");  // 打印顺序无法统一管理

    // 三个队列各自为政,打印机无法协调
    // 可能导致打印冲突、顺序混乱
}

问题

  1. ❌ 资源冲突:多个实例同时操作同一台打印机
  2. ❌ 顺序混乱:无法保证打印任务的先后顺序
  3. ❌ 状态不一致:每个实例维护自己的队列,无法统一调度

✅ 使用单例模式

cpp 复制代码
class PrinterSpooler {
public:
    static PrinterSpooler& getInstance() {
        static PrinterSpooler instance;
        return instance;
    }

    void addJob(const std::string& document) {
        std::lock_guard<std::mutex> lock(mutex_);
        jobQueue_.push(document);
        std::cout << "添加打印任务: " << document << std::endl;
    }

    void processJobs() {
        std::lock_guard<std::mutex> lock(mutex_);
        while (!jobQueue_.empty()) {
            std::cout << "正在打印: " << jobQueue_.front() << std::endl;
            jobQueue_.pop();
        }
    }

private:
    PrinterSpooler() = default;
    std::queue<std::string> jobQueue_;
    std::mutex mutex_;
};

int main() {
    // 所有打印任务都通过唯一的队列管理
    PrinterSpooler::getInstance().addJob("文档A");  // 进入统一队列
    PrinterSpooler::getInstance().addJob("文档B");  // 同一个队列
    PrinterSpooler::getInstance().addJob("文档C");  // 同一个队列

    PrinterSpooler::getInstance().processJobs();    // 按顺序打印
    // 输出:文档A → 文档B → 文档C(有序)
}

优点

  1. 资源统一:全局只有一个打印队列,避免冲突
  2. 顺序可控:所有任务按先进先出顺序处理
  3. 状态一致:所有调用者共享同一个队列状态
  4. 便于管理:可以统一监控、暂停、取消打印任务

适用场景

单例模式的核心价值是控制对共享资源的访问,以下是典型场景:

场景 单例对象 为什么需要单例
打印队列 PrinterSpooler 多实例会导致打印冲突和顺序混乱
数据库连接池 ConnectionPool 多实例会创建过多连接,耗尽数据库资源
线程池 ThreadPool 多实例会导致线程数失控,系统过载
配置管理 ConfigManager 多实例可能读取到不一致的配置
设备管理 DeviceManager 硬件设备天然唯一,必须统一管理
音视频播放器 MediaPlayer 播放状态和资源需要统一协调

🔧 实现方式详解

1. 饿汉式

特点

  • 程序启动时就创建实例
  • 简单直接,天然线程安全
  • 可能造成启动时资源浪费
cpp 复制代码
class Singleton {
public:
    static Singleton& getInstance() {
        return instance;
    }

    void doSomething() {
        std::cout << "Singleton working!" << std::endl;
    }

private:
    Singleton() {
        std::cout << "Singleton created!" << std::endl;
    }

    // 禁止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton instance;  // 静态成员,程序启动时初始化
};

// 必须在类外(.cpp)定义静态成员
Singleton Singleton::instance;

执行流程

程序启动 静态成员 instance 初始化 调用构造函数创建对象 单例已就绪 调用者1: getInstance 调用者2: getInstance 调用者3: getInstance 返回同一个 instance


2. 懒汉式

特点

  • 首次使用时才创建实例
  • 节省启动资源
  • 需要注意线程安全

❌ 非线程安全版本

cpp 复制代码
class Singleton {
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {        // 线程A执行到这里
            instance = new Singleton();   // 线程B也执行到这里
        }                                 // 可能创建两个实例!
        return instance;
    }

private:
    Singleton() = default;
    static Singleton* instance;
};

Singleton* Singleton::instance = nullptr;

问题:多线程环境下可能创建多个实例!


✅ 线程安全版本(双重检查锁)

cpp 复制代码
#include <mutex>

class Singleton {
public:
    static Singleton& getInstance() {
        if (instance == nullptr) {                    // 第一次检查(无锁)
            std::lock_guard<std::mutex> lock(mutex);  // 加锁
            if (instance == nullptr) {                // 第二次检查(有锁)
                instance = new Singleton();
            }
        }
        return *instance;
    }

private:
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* instance;
    static std::mutex mutex;
};

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;

双重检查锁执行流程

否 是 否 是 调用 getInstance instance == nullptr?
第一次检查 返回 instance 获取互斥锁 instance == nullptr?
第二次检查 释放锁 创建实例
instance = new Singleton 释放锁 返回单例


3. C++11 静态局部变量(推荐)

特点

  • C++11标准保证线程安全
  • 代码简洁优雅
  • 现代C++的最佳实践
cpp 复制代码
class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;  // C++11保证线程安全
        return instance;
    }

    void doSomething() {
        std::cout << "Singleton working!" << std::endl;
    }

private:
    Singleton() {
        std::cout << "Singleton created!" << std::endl;
    }

    ~Singleton() {
        std::cout << "Singleton destroyed!" << std::endl;
    }

    // 禁止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

为什么C++11静态局部变量是线程安全的?

复制代码
C++11标准规定:
如果多个线程同时首次进入声明静态局部变量的语句,
只有一个线程会执行初始化,其他线程会等待初始化完成。

这是编译器层面的保证,无需手动加锁!


4. 各种实现方式对比

实现方式 线程安全 延迟加载 代码复杂度 推荐指数
饿汉式 ✅ 是 ❌ 否 ⭐ 简单 ⭐⭐⭐
懒汉式(无锁) ❌ 否 ✅ 是 ⭐ 简单
双重检查锁 ✅ 是 ✅ 是 ⭐⭐⭐ 复杂 ⭐⭐⭐
C++11静态局部变量 ✅ 是 ✅ 是 ⭐ 简单 ⭐⭐⭐⭐⭐

💻 C++ 完整实现

cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

// ========== 推荐的单例实现(C++11) ==========
class Singleton {
public:
    // 获取唯一实例
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }

    // 示例方法
    void showMessage(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout <<msg << std::endl;
    }

    int getCount() const {
        return count_;
    }

    void increment() {
        std::lock_guard<std::mutex> lock(mutex_);
        ++count_;
    }

private:
    // 私有构造函数
    Singleton() : count_(0) {
        std::cout << "[单例] 构造函数被调用,实例已创建!" << std::endl;
    }

    // 私有析构函数
    ~Singleton() {
        std::cout << "[单例] 析构函数被调用,实例已销毁!" << std::endl;
    }

    // 禁止拷贝
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 禁止移动
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

    int count_;
    mutable std::mutex mutex_;
};

// ========== 测试代码 ==========
void threadFunc(int threadId) {
    // 每个线程都访问同一个单例
    Singleton& singleton = Singleton::getInstance();
    singleton.increment();
    singleton.showMessage("线程 " + std::to_string(threadId) + " 访问单例");
}

int main() {
    std::cout << "========== 单例模式测试 ==========" << std::endl;
    std::cout << std::endl;

    // 创建多个线程同时访问单例
    std::vector<std::thread> threads;

    std::cout << "[主线程] 创建5个线程..." << std::endl;
    for (int i = 1; i <= 5; ++i) {
        threads.emplace_back(threadFunc, i);
    }

    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }

    std::cout << std::endl;
    std::cout << "[主线程] 所有线程完成" << std::endl;
    std::cout << "[主线程] 单例被访问次数: "
              << Singleton::getInstance().getCount() << std::endl;
    std::cout << std::endl;
    std::cout << "========== 程序结束 ==========" << std::endl;

    return 0;
}

运行结果示例

复制代码
========== 单例模式测试 ==========

[主线程] 创建5个线程...
[单例] 构造函数被调用,实例已创建!
线程 5 访问单例
线程 2 访问单例
线程 3 访问单例
线程 1 访问单例
线程 4 访问单例

[主线程] 所有线程完成
[主线程] 单例被访问次数: 5

========== 程序结束 ==========
[单例] 析构函数被调用,实例已销毁!

⚠️ 常见问题

问题1:内存泄漏

使用指针的懒汉式

cpp 复制代码
// ❌ 存在内存泄漏风险
class Singleton {
public:
    static Singleton* getInstance() {
        if (!instance) {
            instance = new Singleton();  // new 了但没有 delete
        }
        return instance;
    }
private:
    static Singleton* instance;
};

解决方案

cpp 复制代码
// ✅ 方案1:使用静态局部变量(推荐)
static Singleton& getInstance() {
    static Singleton instance;
    return instance;
}

// ✅ 方案2:使用智能指针
static std::shared_ptr<Singleton> getInstance() {
    static std::shared_ptr<Singleton> instance(new Singleton());
    return instance;
}

问题2:多线程竞争

数据竞争场景

cpp 复制代码
class Counter {
public:
    static Counter& getInstance() {
        static Counter instance;
        return instance;
    }

    // ❌ 非线程安全的操作
    void increment() {
        count_++;  // 多线程同时调用会出问题
    }

private:
    int count_ = 0;
};

解决方案

cpp 复制代码
class Counter {
public:
    static Counter& getInstance() {
        static Counter instance;
        return instance;
    }

    // ✅ 使用互斥锁保护
    void increment() {
        std::lock_guard<std::mutex> lock(mutex_);
        count_++;
    }

    // ✅ 或者使用原子变量
    void incrementAtomic() {
        atomicCount_++;
    }

private:
    int count_ = 0;
    std::atomic<int> atomicCount_{0};
    std::mutex mutex_;
};

问题3:单例的继承问题

cpp 复制代码
// ❌ 单例类难以被继承
class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
protected:
    Singleton() = default;
};

class DerivedSingleton : public Singleton {
    // 问题:无法创建 DerivedSingleton 的单例
};

解决方案:使用模板

cpp 复制代码
// ✅ 单例模板(CRTP模式)
template<typename T>
class Singleton {
public:
    static T& getInstance() {
        static T instance;
        return instance;
    }

protected:
    Singleton() = default;
    ~Singleton() = default;

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

// 使用方式
class Logger : public Singleton<Logger> {
    friend class Singleton<Logger>;  // 允许基类访问私有构造
private:
    Logger() = default;
public:
    void log(const std::string& msg) {
        std::cout << msg << std::endl;
    }
};

// 调用
Logger::getInstance().log("Hello Singleton!");

📋 总结

核心要点

复制代码
1️⃣ 单例模式 = 私有构造 + 静态实例 + 公有访问点
2️⃣ C++11静态局部变量是最佳实践,天然线程安全
3️⃣ 禁止拷贝和赋值,使用 = delete
4️⃣ 单例内部操作仍需考虑线程安全
5️⃣ 避免滥用单例,它本质上是一个全局变量

使用场景总结

核心原则 :当需要控制对共享资源的访问时,考虑使用单例模式。

场景类型 是否适用 说明
打印队列/任务队列 ✅ 非常适合 物理资源天然唯一,必须统一调度
数据库连接池 ✅ 非常适合 防止连接过多导致资源耗尽
线程池 ✅ 非常适合 防止线程数失控导致系统崩溃
配置管理器 ✅ 适合 保证配置一致性
播放器核心 ✅ 适合 状态和资源统一管理
日志管理器 ⚠️ 可用但非必需 更多是便利性而非必要性
频繁创建的对象 ❌ 不适用 应使用对象池
需要多实例的场景 ❌ 不适用 与单例矛盾
需要依赖注入 ⚠️ 慎用 影响可测试性

最佳实践建议

cpp 复制代码
// ✅ 推荐的单例写法
class MySingleton {
public:
    static MySingleton& getInstance() {
        static MySingleton instance;
        return instance;
    }

    // 业务方法...

private:
    MySingleton() = default;
    ~MySingleton() = default;

    MySingleton(const MySingleton&) = delete; //禁止拷贝
    MySingleton& operator=(const MySingleton&) = delete;  //禁止赋值
    MySingleton(MySingleton&&) = delete; //禁止移动构造
    MySingleton& operator=(MySingleton&&) = delete; //禁止移动赋值
};

如果您觉得这篇文章对您有帮助,不妨点赞 + 收藏 + 关注,更多设计模式系列教程将持续更新 🔥!

相关推荐
fie88896 小时前
C++实现D星 Lite算法
c++·算法·哈希算法
liu****6 小时前
二.protobuf的使用
c++·c·protobuf·企业级组件
Arciab6 小时前
C++ 学习_基础知识
c++·学习
__万波__6 小时前
二十三种设计模式(十二)--代理模式
java·设计模式·代理模式
oioihoii6 小时前
C++ 多线程开发:从零开始的完整指南
开发语言·c++
曼巴UE56 小时前
UE 运行时编辑效果。Gizom使用增强输入改写
c++·ue5
是有头发的程序猿6 小时前
1688数据采集:官方API与网页爬虫实战指南
开发语言·c++·爬虫
霍田煜熙6 小时前
C++ 部署小型图书管理系统
开发语言·c++·算法
@小码农6 小时前
2025年厦门市小学生信息学竞赛C++(初赛)真题-附答案
开发语言·c++·python·算法·蓝桥杯