C++ 单例模式
介绍
单例模式是面向对象编程中最简单也是最常用的设计模式之一。它的核心思想是确保一个类只有一个实例,并提供一个全局访问点。在很多场景下,某些类只需要一个实例就足够了,比如配置管理器、日志记录器、数据库连接池等。
备注
单例模式属于创建型设计模式,专注于对象的创建方式。
为什么需要单例模式?
想象以下场景:
- 需要一个全局的配置管理器,在程序各处都能访问相同的配置
- 需要一个日志系统,所有模块共享同一个日志实例
- 需要管理有限的系统资源,如数据库连接
在这些情况下,如果允许创建多个实例可能会导致:
- 资源浪费
- 状态不一致
- 行为异常
单例模式的基本实现
让我们逐步构建一个简单的单例类:
cpp
class Singleton {
private:
// 私有构造函数,防止外部实例化
Singleton() {
std::cout << "构造函数被调用" << std::endl;
}
// 禁止拷贝构造和拷贝赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 静态实例指针
static Singleton* instance;
public:
// 全局访问点
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
// 示例方法
void someMethod() {
std::cout << "调用了单例的方法" << std::endl;
}
};
// 静态成员需要在类外初始化
Singleton* Singleton::instance = nullptr;
如何使用单例
cpp
#include <iostream>
int main() {
// 无法直接创建实例
// Singleton s; // 编译错误:构造函数是私有的
// 获取单例实例
Singleton* s1 = Singleton::getInstance();
s1->someMethod();
// 再次获取单例实例
Singleton* s2 = Singleton::getInstance();
// 验证是否是同一个实例
std::cout << "s1地址: " << s1 << std::endl;
std::cout << "s2地址: " << s2 << std::endl;
std::cout << "s1和s2是同一个实例: " << (s1 == s2 ? "是" : "否") << std::endl;
return 0;
}
输出:
构造函数被调用
调用了单例的方法
s1地址: 0x55f7e5642e70
s2地址: 0x55f7e5642e70
s1和s2是同一个实例: 是
单例模式的问题
上面的简单实现存在一些问题:
- 内存泄漏:
getInstance()
中创建的实例没有被删除 - 线程安全问题:多线程环境下可能创建多个实例
- 不支持延迟初始化:需要在程序启动时就创建实例
改进:线程安全的单例模式
方法一:使用锁保护
cpp
#include <mutex>
class ThreadSafeSingleton {
private:
ThreadSafeSingleton() {}
static ThreadSafeSingleton* instance;
static std::mutex mutex;
public:
static ThreadSafeSingleton* getInstance() {
std::lock_guard<std::mutex> lock(mutex);
if (instance == nullptr) {
instance = new ThreadSafeSingleton();
}
return instance;
}
};
ThreadSafeSingleton* ThreadSafeSingleton::instance = nullptr;
std::mutex ThreadSafeSingleton::mutex;
这种方法虽然解决了线程安全问题,但是每次调用getInstance()
都要加锁,影响性能。
方法二:双重检查锁定(DCLP)
cpp
class DCLPSingleton {
private:
DCLPSingleton() {}
static DCLPSingleton* instance;
static std::mutex mutex;
public:
static DCLPSingleton* getInstance() {
// 第一次检查
if (instance == nullptr) {
// 加锁
std::lock_guard<std::mutex> lock(mutex);
// 第二次检查
if (instance == nullptr) {
instance = new DCLPSingleton();
}
}
return instance;
}
};
DCLPSingleton* DCLPSingleton::instance = nullptr;
std::mutex DCLPSingleton::mutex;
警告
注意:在某些内存模型下,DCLP可能因为指令重排导致问题。在C++11之前,这个模式并不保证线程安全。
方法三:使用C++11的线程安全的局部静态变量
这是现代C++中推荐的单例实现方式,简洁且线程安全:
cpp
class ModernSingleton {
private:
ModernSingleton() {
std::cout << "现代单例被构造" << std::endl;
}
public:
// 禁止拷贝和赋值
ModernSingleton(const ModernSingleton&) = delete;
ModernSingleton& operator=(const ModernSingleton&) = delete;
static ModernSingleton& getInstance() {
// C++11保证这个静态局部变量的初始化是线程安全的
static ModernSingleton instance;
return instance;
}
void someMethod() {
std::cout << "调用现代单例的方法" << std::endl;
}
};
使用:
cpp
int main() {
// 获取单例引用
ModernSingleton& instance1 = ModernSingleton::getInstance();
instance1.someMethod();
// 再次获取单例引用
ModernSingleton& instance2 = ModernSingleton::getInstance();
// 验证是同一个实例
std::cout << "instance1地址: " << &instance1 << std::endl;
std::cout << "instance2地址: " << &instance2 << std::endl;
return 0;
}
输出:
现代单例被构造
调用现代单例的方法
instance1地址: 0x55f7e5642e90
instance2地址: 0x55f7e5642e90
单例模式的优缺点
优点
- 保证一个类只有一个实例
- 提供对该实例的全局访问点
- 实例在首次使用时才被创建(延迟初始化)
- 控制对共享资源的并发访问
缺点
- 违反单一职责原则,一个类既负责创建自己的实例,又负责业务逻辑
- 可能隐藏组件间的依赖关系
- 在多线程环境下实现复杂且容易出错
- 难以进行单元测试,因为无法轻易替换模拟对象
单例模式的实际应用场景
1. 配置管理器
cpp
class ConfigManager {
private:
ConfigManager() {
// 从配置文件加载配置
loadConfig();
}
std::map<std::string, std::string> configMap;
void loadConfig() {
// 从文件加载配置到configMap
configMap["db_host"] = "localhost";
configMap["db_port"] = "3306";
configMap["max_connections"] = "100";
}
public:
static ConfigManager& getInstance() {
static ConfigManager instance;
return instance;
}
std::string getConfig(const std::string& key) {
if (configMap.find(key) != configMap.end()) {
return configMap[key];
}
return "";
}
void setConfig(const std::string& key, const std::string& value) {
configMap[key] = value;
}
};
使用:
cpp
int main() {
// 获取配置
std::cout << "数据库主机: "
<< ConfigManager::getInstance().getConfig("db_host") << std::endl;
std::cout << "数据库端口: "
<< ConfigManager::getInstance().getConfig("db_port") << std::endl;
// 修改配置
ConfigManager::getInstance().setConfig("db_host", "127.0.0.1");
// 验证修改生效
std::cout << "修改后的数据库主机: "
<< ConfigManager::getInstance().getConfig("db_host") << std::endl;
return 0;
}
2. 日志系统
cpp
class Logger {
private:
Logger() {
logFile.open("application.log", std::ios::app);
}
~Logger() {
if (logFile.is_open()) {
logFile.close();
}
}
std::ofstream logFile;
public:
static Logger& getInstance() {
static Logger instance;
return instance;
}
void log(const std::string& message) {
std::lock_guard<std::mutex> lock(mutex);
std::string timestamp = getCurrentTimestamp();
logFile << "[" << timestamp << "] " << message << std::endl;
std::cout << "[" << timestamp << "] " << message << std::endl;
}
std::string getCurrentTimestamp() {
auto now = std::chrono::system_clock::now();
auto now_c = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&now_c), "%Y-%m-%d %H:%M:%S");
return ss.str();
}
private:
std::mutex mutex;
};
使用:
cpp
void someFunction() {
Logger::getInstance().log("someFunction被调用");
// 函数逻辑...
}
void anotherFunction() {
Logger::getInstance().log("anotherFunction开始执行");
// 函数逻辑...
Logger::getInstance().log("anotherFunction执行完成");
}
int main() {
Logger::getInstance().log("程序启动");
someFunction();
anotherFunction();
Logger::getInstance().log("程序结束");
return 0;
}
单例与模块化设计
随着项目的增长,单例可能带来模块间的强耦合。为了缓解这个问题,可以考虑以下几点:
- 依赖注入:通过构造函数或setter方法注入依赖,而不是直接使用单例
- 服务定位器模式:将单例作为一种服务,通过服务定位器获取
- 接口抽象:单例类实现一个接口,使依赖于接口而非具体实现
总结
单例模式是一种保证类全局只有一个实例的设计模式,适用于需要协调全局行为的场景。在C++中,最简单安全的实现是使用C++11的局部静态变量特性。
单例模式的核心要点:
- 私有构造函数防止外部实例化
- 禁用拷贝构造和拷贝赋值
- 提供全局访问点
- 确保线程安全(在多线程环境下)
适当使用单例模式可以有效管理共享资源,但过度使用会导致程序难以测试和维护。在设计系统时,应当谨慎评估是否真的需要单例模式。
练习
- 实现一个线程安全的数据库连接池单例
- 修改现有单例实现,增加释放单例的方法(用于需要显式释放资源的场景)
- 实现一个带有最大实例数量限制的"多例模式"(允许创建有限个实例)
扩展阅读资源
- 《设计模式:可复用面向对象软件的基础》- Gamma, Helm, Johnson, Vlissides (GoF)
- 《Effective C++》- Scott Meyers
- 《Modern C++ Design》- Andrei Alexandrescu
练习这些概念,将帮助你更好地理解和应用单例模式,掌握C++高级程序设计技巧。