跳到主要内容

C++ 单例模式

介绍

单例模式是面向对象编程中最简单也是最常用的设计模式之一。它的核心思想是确保一个类只有一个实例,并提供一个全局访问点。在很多场景下,某些类只需要一个实例就足够了,比如配置管理器、日志记录器、数据库连接池等。

备注

单例模式属于创建型设计模式,专注于对象的创建方式。

为什么需要单例模式?

想象以下场景:

  1. 需要一个全局的配置管理器,在程序各处都能访问相同的配置
  2. 需要一个日志系统,所有模块共享同一个日志实例
  3. 需要管理有限的系统资源,如数据库连接

在这些情况下,如果允许创建多个实例可能会导致:

  • 资源浪费
  • 状态不一致
  • 行为异常

单例模式的基本实现

让我们逐步构建一个简单的单例类:

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是同一个实例: 是

单例模式的问题

上面的简单实现存在一些问题:

  1. 内存泄漏getInstance()中创建的实例没有被删除
  2. 线程安全问题:多线程环境下可能创建多个实例
  3. 不支持延迟初始化:需要在程序启动时就创建实例

改进:线程安全的单例模式

方法一:使用锁保护

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. 保证一个类只有一个实例
  2. 提供对该实例的全局访问点
  3. 实例在首次使用时才被创建(延迟初始化)
  4. 控制对共享资源的并发访问

缺点

  1. 违反单一职责原则,一个类既负责创建自己的实例,又负责业务逻辑
  2. 可能隐藏组件间的依赖关系
  3. 在多线程环境下实现复杂且容易出错
  4. 难以进行单元测试,因为无法轻易替换模拟对象

单例模式的实际应用场景

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;
}

单例与模块化设计

随着项目的增长,单例可能带来模块间的强耦合。为了缓解这个问题,可以考虑以下几点:

  1. 依赖注入:通过构造函数或setter方法注入依赖,而不是直接使用单例
  2. 服务定位器模式:将单例作为一种服务,通过服务定位器获取
  3. 接口抽象:单例类实现一个接口,使依赖于接口而非具体实现

总结

单例模式是一种保证类全局只有一个实例的设计模式,适用于需要协调全局行为的场景。在C++中,最简单安全的实现是使用C++11的局部静态变量特性。

单例模式的核心要点:

  1. 私有构造函数防止外部实例化
  2. 禁用拷贝构造和拷贝赋值
  3. 提供全局访问点
  4. 确保线程安全(在多线程环境下)

适当使用单例模式可以有效管理共享资源,但过度使用会导致程序难以测试和维护。在设计系统时,应当谨慎评估是否真的需要单例模式。

练习

  1. 实现一个线程安全的数据库连接池单例
  2. 修改现有单例实现,增加释放单例的方法(用于需要显式释放资源的场景)
  3. 实现一个带有最大实例数量限制的"多例模式"(允许创建有限个实例)

扩展阅读资源

  • 《设计模式:可复用面向对象软件的基础》- Gamma, Helm, Johnson, Vlissides (GoF)
  • 《Effective C++》- Scott Meyers
  • 《Modern C++ Design》- Andrei Alexandrescu

练习这些概念,将帮助你更好地理解和应用单例模式,掌握C++高级程序设计技巧。