【c++面向对象编程】第40篇:单例模式(Singleton)的多种C++实现 目录一、单例模式是什么二、饿汉式Eager Initialization三、懒汉式Lazy Initialization版本1基础版线程不安全版本2加锁版线程安全但性能差版本3双检锁Double-Checked Locking—— 经典但有问题四、Meyers SingletonC11 最佳实践为什么它是线程安全的五、完整例子配置管理器六、单例模式的问题与批评替代方案七、四种实现对比八、常见错误1. 忘记删除拷贝构造和赋值2. 多线程下使用裸指针懒汉式3. Meyers Singleton 在 C11 之前的编译器4. 单例中持有资源但不正确释放九、这一篇的收获一、单例模式是什么cpp// 单例模式的核心特征 // 1. 私有构造函数 —— 防止外部创建 // 2. 静态成员函数 getInstance() —— 全局访问点 // 3. 静态成员变量 —— 持有唯一实例 class Singleton { private: Singleton() {} // 私有构造 public: static Singleton getInstance() { static Singleton instance; // 唯一实例 return instance; } Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; };使用场景全局配置管理器日志记录器数据库连接池硬件接口如打印机二、饿汉式Eager Initialization在程序启动时就创建实例无论是否使用。cppclass EagerSingleton { private: static EagerSingleton instance; // 静态实例 int data; EagerSingleton() : data(0) { cout 饿汉式单例程序启动时创建 endl; } public: static EagerSingleton getInstance() { return instance; } void setData(int d) { data d; } int getData() const { return data; } EagerSingleton(const EagerSingleton) delete; EagerSingleton operator(const EagerSingleton) delete; }; // 静态成员必须在类外定义 EagerSingleton EagerSingleton::instance; int main() { // 即使不使用 getInstance实例已存在 auto s EagerSingleton::getInstance(); s.setData(42); }优点缺点线程安全静态初始化由编译器保证程序启动慢所有单例都提前创建实现简单可能创建永远不用的实例访问快无锁多个单例的初始化顺序不确定三、懒汉式Lazy Initialization首次调用getInstance()时才创建实例。版本1基础版线程不安全cppclass LazySingleton { private: static LazySingleton* instance; LazySingleton() { cout 懒汉式首次使用时创建 endl; } public: static LazySingleton* getInstance() { if (instance nullptr) { instance new LazySingleton(); // ❌ 多线程下可能创建多个 } return instance; } LazySingleton(const LazySingleton) delete; LazySingleton operator(const LazySingleton) delete; }; LazySingleton* LazySingleton::instance nullptr;线程安全问题多个线程同时进入if (instance nullptr)会创建多个实例。版本2加锁版线程安全但性能差cpp#include mutex class LockedSingleton { private: static LockedSingleton* instance; static mutex mtx; LockedSingleton() { cout 加锁懒汉式创建 endl; } public: static LockedSingleton* getInstance() { lock_guardmutex lock(mtx); // 每次调用都加锁 if (instance nullptr) { instance new LockedSingleton(); } return instance; } }; LockedSingleton* LockedSingleton::instance nullptr; mutex LockedSingleton::mtx;问题每次调用都加锁即使实例已存在性能差。版本3双检锁Double-Checked Locking—— 经典但有问题cppstatic LockedSingleton* getInstance() { if (instance nullptr) { // 第一次检查无锁 lock_guardmutex lock(mtx); if (instance nullptr) { // 第二次检查有锁 instance new LockedSingleton(); } } return instance; }DCLP 在 C11 之前有内存可见性问题指令重排可能导致返回未完全构造的对象。C11 之后可以用std::atomic和内存屏障修复但实现复杂不推荐。四、Meyers SingletonC11 最佳实践Scott Meyers 在《Effective C》中提出的方案C11 起是线程安全的。cppclass MeyersSingleton { private: int data; MeyersSingleton() : data(0) { cout Meyers Singleton 创建 endl; } public: static MeyersSingleton getInstance() { static MeyersSingleton instance; // 函数静态变量 return instance; } void setData(int d) { data d; } int getData() const { return data; } MeyersSingleton(const MeyersSingleton) delete; MeyersSingleton operator(const MeyersSingleton) delete; }; // 使用 auto s MeyersSingleton::getInstance();为什么它是线程安全的C11 标准保证函数内的静态变量初始化是线程安全的。多个线程同时调用getInstance()时只有一个线程执行初始化其他线程等待。特性Meyers Singleton线程安全✅ C11 起保证懒加载✅ 首次调用时创建无锁✅ 初始化后无锁访问实现简单✅ 仅 5 行代码内存安全✅ 程序结束时自动析构这是现代 C 中实现单例的推荐方式。五、完整例子配置管理器cpp#include iostream #include string #include unordered_map #include mutex using namespace std; class ConfigManager { private: unordered_mapstring, string config; // 私有构造 ConfigManager() { cout 加载配置文件... endl; // 模拟从文件加载 config[db_host] localhost; config[db_port] 3306; config[log_level] info; } // 清理日志可选 void logAccess(const string key) const { // 可以记录配置访问日志 } public: // 删除拷贝构造和赋值 ConfigManager(const ConfigManager) delete; ConfigManager operator(const ConfigManager) delete; // 唯一访问点 static ConfigManager getInstance() { static ConfigManager instance; return instance; } // 获取配置 string get(const string key, const string defaultVal ) const { logAccess(key); auto it config.find(key); if (it ! config.end()) { return it-second; } return defaultVal; } // 设置配置运行时修改 void set(const string key, const string value) { config[key] value; } // 打印所有配置 void printAll() const { cout \n 当前配置 endl; for (const auto [k, v] : config) { cout k v endl; } } }; // 其他需要使用配置的类 class Database { public: void connect() { auto config ConfigManager::getInstance(); string host config.get(db_host); string port config.get(db_port); cout 连接数据库: host : port endl; } }; class Logger { public: void log(const string msg) { auto config ConfigManager::getInstance(); string level config.get(log_level, debug); cout [ level ] msg endl; } }; int main() { // 第一次调用 getInstance() 时创建实例 auto config ConfigManager::getInstance(); config.printAll(); Database db; db.connect(); Logger logger; logger.log(应用程序启动); // 运行时修改配置 config.set(log_level, warning); logger.log(这是一条警告); // 证明是同一个实例 auto config2 ConfigManager::getInstance(); cout \n地址相同: (config config2) endl; return 0; }输出text加载配置文件... 当前配置 db_host localhost db_port 3306 log_level info 连接数据库: localhost:3306 [info] 应用程序启动 [warning] 这是一条警告 地址相同: 1六、单例模式的问题与批评单例模式虽然常用但也经常被批评为“反模式”问题说明全局状态单例本质上是全局变量增加了耦合测试困难无法 mock 单例单元测试时状态会残留隐藏依赖函数内部调用getInstance()依赖关系不明显多例难以扩展如果需要变成多例如不同环境的配置改造成本高替代方案场景推荐方式配置管理依赖注入传引用日志系统传递 Logger 引用全局唯一Meyer Singleton实在需要时原则优先考虑依赖注入只有在确实全局唯一且没有更好的设计时才用单例。七、四种实现对比实现方式线程安全懒加载实现复杂度推荐度饿汉式✅C98❌简单不推荐启动慢懒汉式裸指针❌✅简单❌懒汉式加锁✅✅中等不推荐性能差双检锁✅C11后需 careful✅复杂❌Meyers Singleton✅C11✅极简✅ 推荐八、常见错误1. 忘记删除拷贝构造和赋值cpp// ❌ 错误没有删除拷贝构造 Singleton s1 Singleton::getInstance(); Singleton s2 s1; // 可以拷贝破坏了单例2. 多线程下使用裸指针懒汉式cpp// ❌ 多线程不安全 static Singleton* getInstance() { if (!instance) instance new Singleton(); return instance; }3. Meyers Singleton 在 C11 之前的编译器C98/03 标准不保证函数静态变量初始化的线程安全。如果编译器不支持 C11需要加锁。4. 单例中持有资源但不正确释放Meyers Singleton 在程序结束时自动析构但如果有复杂的资源依赖如 A 单例析构时用到 B 单例可能出问题。解决方案让单例简单复杂资源用unique_ptr管理。九、这一篇的收获你现在应该理解饿汉式启动时创建线程安全但可能浪费懒汉式首次使用时创建基础版线程不安全Meyers Singleton函数内静态变量C11 起线程安全最推荐单例的缺点全局状态、测试困难、隐藏依赖原则优先依赖注入确实需要时用 Meyers Singleton 小作业实现一个Logger单例支持不同日志级别INFO、WARNING、ERROR线程安全并提供setLogFile()方法。写一个多线程测试验证getInstance()只构造一次。下一篇预告第41篇《函数模板与类模板泛型编程的基石》——结束设计模式章节进入模板与泛型编程。模板让代码与类型解耦一个函数/类可以处理多种类型。下篇讲清楚函数模板和类模板的基本语法。