在C++开发中,单例模式可能是最广为人知的设计模式之一。我第一次接触单例是在一个需要全局配置管理的项目中,当时为了确保所有模块读取的配置一致,单例模式成了最自然的解决方案。简单来说,单例模式确保一个类只有一个实例,并提供一个全局访问点。
想象你正在开发一个日志系统。如果每个模块都自己创建日志对象,不仅会造成资源浪费,更可能导致日志文件被多个实例争抢写入。单例模式通过以下特性解决这类问题:
最基本的单例实现包含三个关键要素:
cpp复制class Singleton {
private:
static Singleton* instance; // 静态成员保存唯一实例
Singleton() {} // 私有构造函数防止外部创建
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
// 静态成员初始化
Singleton* Singleton::instance = nullptr;
这个实现虽然简单,但在多线程环境下会出问题。当多个线程同时调用getInstance()时,可能会创建多个实例。我在早期项目中就遇到过这种bug,日志文件中出现了交叉写入的混乱内容。
为了解决线程安全问题,最经典的方案是双检锁模式。我第一次在生产环境使用这个模式时,曾因为内存屏障问题导致难以复现的bug。正确的实现应该这样:
cpp复制class Singleton {
private:
static std::atomic<Singleton*> instance;
static std::mutex mtx;
Singleton() {}
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
std::atomic_thread_fence(std::memory_order_release);
instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
};
注意:这里使用了C++11的内存序(memory_order)保证,这是很多初学者容易忽略的关键点。我在代码审查中经常看到漏掉内存屏障的实现,这种bug通常只在高压测试下才会暴露。
Scott Meyers提出的这种实现堪称单例模式的"优雅典范":
cpp复制class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
这种方案的优势在于:
我在现代C++项目中更倾向于使用这种方案,除非有特殊的初始化顺序要求。
有时我们需要在单例初始化时传递参数。我曾在一个图像处理项目中遇到这种情况,解决方案是:
cpp复制class ConfigManager {
private:
static std::unique_ptr<ConfigManager> instance;
static std::once_flag initFlag;
std::string configPath;
ConfigManager(const std::string& path) : configPath(path) {}
public:
static void initialize(const std::string& path) {
std::call_once(initFlag, [&]() {
instance.reset(new ConfigManager(path));
});
}
static ConfigManager& getInstance() {
if (!instance) {
throw std::runtime_error("ConfigManager not initialized");
}
return *instance;
}
};
使用时需要先调用initialize(),这虽然稍微麻烦,但提供了更好的控制能力。
让单例类支持多态需要特殊设计。我在一个跨平台UI框架中是这样实现的:
cpp复制class AudioService {
protected:
static AudioService* instance;
AudioService() = default;
public:
static AudioService& getInstance() {
assert(instance != nullptr);
return *instance;
}
virtual void playSound() = 0;
virtual ~AudioService() = default;
};
// 平台特定实现
class WindowsAudioService : public AudioService {
private:
WindowsAudioService() = default;
friend class AudioServiceInitializer;
public:
void playSound() override {
// Windows特定的音频实现
}
};
// 初始化器负责创建具体实例
class AudioServiceInitializer {
public:
AudioServiceInitializer() {
AudioService::instance = new WindowsAudioService();
}
~AudioServiceInitializer() {
delete AudioService::instance;
AudioService::instance = nullptr;
}
};
这种模式虽然复杂,但在需要平台特定实现时非常有用。
忘记禁用拷贝操作:这可能导致单例被意外复制
cpp复制Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
析构问题:特别是当单例持有资源时,需要确保正确释放
cpp复制~Singleton() {
// 释放资源
}
初始化顺序问题:当多个单例相互依赖时,可能产生静态初始化顺序问题
单例常被诟病的一点是难以测试。通过引入依赖注入的思想可以改善:
cpp复制class Database {
protected:
static Database* instance;
public:
static void setInstance(Database* db) {
instance = db;
}
static Database& getInstance() {
if (!instance) {
instance = new Database();
}
return *instance;
}
virtual void query() = 0;
};
// 生产环境实现
class ProductionDatabase : public Database {
public:
void query() override { /* 真实数据库操作 */ }
};
// 测试环境mock
class MockDatabase : public Database {
public:
void query() override { /* 模拟数据返回 */ }
};
// 测试时可以这样
TEST(DatabaseTest, QueryTest) {
MockDatabase mock;
Database::setInstance(&mock);
// 执行测试...
}
根据我的经验,单例模式最适合以下场景:
但在以下情况应避免使用:
C++17引入的inline变量让单例实现更加简洁:
cpp复制class Singleton {
public:
static Singleton& getInstance() {
static inline Singleton instance;
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
这种写法与Meyers' Singleton类似,但inline关键字可以避免ODR(One Definition Rule)问题,特别适合头文件中的单例定义。
在实际项目中,我还会为单例添加线程安全的销毁控制:
cpp复制class SafeSingleton {
private:
static std::atomic<SafeSingleton*> instance;
static std::mutex mtx;
SafeSingleton() {}
~SafeSingleton() {}
public:
static SafeSingleton& getInstance() {
SafeSingleton* tmp = instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new SafeSingleton();
std::atomic_thread_fence(std::memory_order_release);
instance.store(tmp, std::memory_order_relaxed);
std::atexit([]() {
delete instance.load();
instance.store(nullptr);
});
}
}
return *tmp;
}
};
这个版本确保了:
在最近的一个高频交易系统中,这种实现经受住了每天数亿次调用的考验。