C++ 实现单例模式的多种方式
2025-03-31

在软件设计中,有时我们希望一个类在整个应用程序的生命周期中只存在一个实例。这可能是为了管理共享资源(如数据库连接池、线程池)、提供全局配置信息,或者实现一个全局的日志记录器等。单例模式(Singleton Pattern)正是为了解决这类问题而生的经典设计模式。

它的核心目标非常明确:

  1. 确保一个类只有一个实例。
  2. 提供一个全局访问点来获取这个唯一的实例。

实现单例的核心要素

要实现单例,我们需要阻止外部代码随意创建类的实例,并提供一个受控的获取实例的方法。这通常通过以下几个步骤实现:

  1. 私有化构造函数 (Private Constructor): 将类的构造函数声明为 privateprotected。这样一来,外部代码就不能通过 new ClassName() 的方式直接创建实例了。
  2. 静态私有成员变量 (Static Private Member): 在类内部定义一个静态的、指向本类实例的私有指针或对象。这个静态成员将持有那个唯一的实例。
  3. 静态公有访问方法 (Static Public Access Method): 提供一个 public 的静态方法(通常命名为 getInstance() 或类似名称),用于获取类的唯一实例。这个方法负责创建实例(如果尚未创建)并返回它。

基础懒汉式实现(非线程安全)

最直观的实现方式是“懒汉式”(Lazy Initialization),即只有在第一次请求实例时才创建它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>

class Singleton {
private:
// 1. 私有化构造函数
Singleton() {
std::cout << "Singleton instance created." << std::endl;
}

// 2. 静态私有成员变量 (指针形式)
static Singleton* instance;

// (可选) 私有化拷贝构造和赋值操作符,防止拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

public:
// 3. 静态公有访问方法
static Singleton* getInstance() {
// 只有当 instance 为空时才创建
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}

// 示例方法
void showMessage() {
std::cout << "Hello from Singleton instance!" << std::endl;
}

// (需要考虑) 如何释放 instance? - 这是懒汉式指针版本的一个痛点
// 可以使用静态内部类 + 析构函数,或者 atexit() 等方式处理
};

// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;

int main() {
Singleton* s1 = Singleton::getInstance();
s1->showMessage();

Singleton* s2 = Singleton::getInstance();
s2->showMessage();

if (s1 == s2) {
std::cout << "s1 and s2 point to the same instance." << std::endl;
}

// 注意:需要手动管理内存释放,或者采用更健壮的方式
// delete Singleton::getInstance(); // 这样做有问题,getInstance 可能再次创建
// 需要设计专门的销毁机制

return 0;
}

基础懒汉式的线程安全问题

上面这个基础懒汉式实现在多线程环境下是不安全的。想象一下场景:

  1. 线程 A 执行 getInstance(),判断 instance == nullptr 为真。
  2. 在线程 A 即将执行 instance = new Singleton(); 之前,操作系统切换到线程 B。
  3. 线程 B 执行 getInstance(),也判断 instance == nullptr 为真(因为线程 A 还没来得及创建)。
  4. 线程 B 执行 instance = new Singleton(); 创建了一个实例。
  5. 操作系统切换回线程 A,线程 A 继续执行 instance = new Singleton();,又创建了一个实例!

这就破坏了单例的唯一性原则。

线程安全的懒汉式实现 (C++11 及以后)

在 C++11 及之后的版本中,实现线程安全的懒汉式单例变得异常简单和优雅,这得益于静态局部变量的线程安全初始化保证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <mutex> // 虽然这里不需要显式 mutex,但提一下线程安全

class Singleton {
private:
Singleton() {
std::cout << "Singleton instance created (thread-safe)." << std::endl;
}
// 私有化拷贝构造和赋值操作符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

public:
// C++11 推荐的线程安全懒汉式实现
static Singleton& getInstance() {
// 静态局部变量在 C++11 后保证只被初始化一次,且过程是线程安全的
static Singleton instance;
return instance;
}

void showMessage() {
std::cout << "Hello from Singleton instance! (thread-safe)" << std::endl;
}
};

// 无需在类外初始化静态成员

int main() {
// 获取实例(返回引用)
Singleton& s1 = Singleton::getInstance();
s1.showMessage();

Singleton& s2 = Singleton::getInstance();
s2.showMessage();

if (&s1 == &s2) { // 比较地址
std::cout << "s1 and s2 refer to the same instance." << std::endl;
}

// 不需要手动管理内存,静态局部变量在程序结束时自动销毁
return 0;
}

这种方式被称为 "Meyers' Singleton",它简洁、高效且是线程安全的(由编译器和运行时库保证)。这是现代 C++ 中实现懒汉式单例的首选方法。

饿汉式实现(线程安全)

另一种实现方式是“饿汉式”(Eager Initialization)。它在程序启动时(或类加载时)就直接创建实例,而不是等到第一次请求时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

class Singleton {
private:
Singleton() {
std::cout << "Singleton instance created (eager)." << std::endl;
}
// 私有化拷贝构造和赋值操作符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

// 2. 静态私有成员变量 - 直接初始化 (C++17 inline or define outside)
// static inline Singleton instance; // C++17 way
static Singleton instance; // Older way, requires definition outside

public:
// 3. 静态公有访问方法 - 直接返回已创建的实例
static Singleton& getInstance() {
return instance;
}

void showMessage() {
std::cout << "Hello from Singleton instance! (eager)" << std::endl;
}
};

// 在类外定义并初始化静态成员变量 (非 C++17 inline 时需要)
Singleton Singleton::instance;

int main() {
// 注意:实例在 main 函数执行前可能就已经创建了
std::cout << "Entering main..." << std::endl;

Singleton& s1 = Singleton::getInstance();
s1.showMessage();

Singleton& s2 = Singleton::getInstance();
s2.showMessage();

if (&s1 == &s2) { // 比较地址
std::cout << "s1 and s2 refer to the same instance." << std::endl;
}
return 0;
}

饿汉式的优点

  • 天生线程安全: 实例在进入多线程环境之前就已经创建好了,不存在懒汉式的竞态条件问题。
  • 实现简单: 获取实例的方法非常直接。

饿汉式的缺点

  • 可能造成资源浪费: 无论程序是否真的用到这个实例,它都会在程序启动时被创建,可能导致启动时间变长或占用不必要的内存。
  • 初始化顺序问题: 如果多个饿汉式单例之间存在依赖关系,它们的初始化顺序可能难以控制(所谓的“静态初始化顺序灾难”)。

总结与思考

单例模式是一种简单但强大的模式,用于保证全局只有一个实例。

  • 核心思想: 私有构造 + 静态成员 + 静态访问方法。
  • 懒汉式: 延迟创建,节省资源,但在多线程下需要特别处理。C++11 的静态局部变量是最佳实践。
  • 饿汉式: 程序启动时创建,简单且线程安全,但可能浪费资源且有初始化顺序问题。

在选择实现方式时,需要根据具体场景权衡:

  • 如果实例创建成本高,或者不一定会被用到,懒汉式(推荐 C++11 版本) 更合适。
  • 如果实例必须存在,且创建成本不高,或者需要避免懒汉式的首次访问延迟,饿汉式是一个简单可靠的选择。