目录
[二、饿汉式(Eager Initialization)](#二、饿汉式(Eager Initialization))
[三、懒汉式(Lazy Initialization)](#三、懒汉式(Lazy Initialization))
[版本3:双检锁(Double-Checked Locking)------ 经典但有问题](#版本3:双检锁(Double-Checked Locking)—— 经典但有问题)
[四、Meyers Singleton(C++11 最佳实践)](#四、Meyers Singleton(C++11 最佳实践))
[1. 忘记删除拷贝构造和赋值](#1. 忘记删除拷贝构造和赋值)
[2. 多线程下使用裸指针懒汉式](#2. 多线程下使用裸指针懒汉式)
[3. Meyers Singleton 在 C++11 之前的编译器](#3. Meyers Singleton 在 C++11 之前的编译器)
[4. 单例中持有资源但不正确释放](#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)
在程序启动时就创建实例,无论是否使用。
cpp
class 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:基础版(线程不安全)
cpp
class 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_guard<mutex> lock(mtx); // 每次调用都加锁
if (instance == nullptr) {
instance = new LockedSingleton();
}
return instance;
}
};
LockedSingleton* LockedSingleton::instance = nullptr;
mutex LockedSingleton::mtx;
问题:每次调用都加锁,即使实例已存在,性能差。
版本3:双检锁(Double-Checked Locking)------ 经典但有问题
cpp
static LockedSingleton* getInstance() {
if (instance == nullptr) { // 第一次检查(无锁)
lock_guard<mutex> lock(mtx);
if (instance == nullptr) { // 第二次检查(有锁)
instance = new LockedSingleton();
}
}
return instance;
}
DCLP 在 C++11 之前有内存可见性问题 (指令重排可能导致返回未完全构造的对象)。C++11 之后可以用 std::atomic 和内存屏障修复,但实现复杂,不推荐。
四、Meyers Singleton(C++11 最佳实践)
Scott Meyers 在《Effective C++》中提出的方案,C++11 起是线程安全的。
cpp
class 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();
为什么它是线程安全的?
C++11 标准保证:函数内的静态变量初始化是线程安全的 。多个线程同时调用 getInstance() 时,只有一个线程执行初始化,其他线程等待。
| 特性 | Meyers Singleton |
|---|---|
| 线程安全 | ✅ C++11 起保证 |
| 懒加载 | ✅ 首次调用时创建 |
| 无锁 | ✅ 初始化后无锁访问 |
| 实现简单 | ✅ 仅 5 行代码 |
| 内存安全 | ✅ 程序结束时自动析构 |
这是现代 C++ 中实现单例的推荐方式。
五、完整例子:配置管理器
cpp
#include <iostream>
#include <string>
#include <unordered_map>
#include <mutex>
using namespace std;
class ConfigManager {
private:
unordered_map<string, 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(实在需要时) |
原则 :优先考虑依赖注入,只有在确实全局唯一 且没有更好的设计时才用单例。
七、四种实现对比
| 实现方式 | 线程安全 | 懒加载 | 实现复杂度 | 推荐度 |
|---|---|---|---|---|
| 饿汉式 | ✅(C++98) | ❌ | 简单 | 不推荐(启动慢) |
| 懒汉式(裸指针) | ❌ | ✅ | 简单 | ❌ |
| 懒汉式(加锁) | ✅ | ✅ | 中等 | 不推荐(性能差) |
| 双检锁 | ✅(C++11后需 careful) | ✅ | 复杂 | ❌ |
| Meyers Singleton | ✅(C++11) | ✅ | 极简 | ✅ 推荐 |
八、常见错误
1. 忘记删除拷贝构造和赋值
cpp
// ❌ 错误:没有删除拷贝构造
Singleton s1 = Singleton::getInstance();
Singleton s2 = s1; // 可以拷贝,破坏了单例
2. 多线程下使用裸指针懒汉式
cpp
// ❌ 多线程不安全
static Singleton* getInstance() {
if (!instance) instance = new Singleton();
return instance;
}
3. Meyers Singleton 在 C++11 之前的编译器
C++98/03 标准不保证函数静态变量初始化的线程安全。如果编译器不支持 C++11,需要加锁。
4. 单例中持有资源但不正确释放
Meyers Singleton 在程序结束时自动析构,但如果有复杂的资源依赖(如 A 单例析构时用到 B 单例),可能出问题。解决方案:让单例简单,复杂资源用 unique_ptr 管理。
九、这一篇的收获
你现在应该理解:
-
饿汉式:启动时创建,线程安全但可能浪费
-
懒汉式:首次使用时创建,基础版线程不安全
-
Meyers Singleton :函数内静态变量,C++11 起线程安全,最推荐
-
单例的缺点:全局状态、测试困难、隐藏依赖
-
原则:优先依赖注入,确实需要时用 Meyers Singleton
💡 小作业:实现一个
Logger单例,支持不同日志级别(INFO、WARNING、ERROR),线程安全,并提供setLogFile()方法。写一个多线程测试,验证getInstance()只构造一次。
下一篇预告:第41篇《函数模板与类模板:泛型编程的基石》------结束设计模式章节,进入模板与泛型编程。模板让代码与类型解耦,一个函数/类可以处理多种类型。下篇讲清楚函数模板和类模板的基本语法。