🧩 实现一个线程安全的单例模式
在C++17中,实现线程安全的单例模式最简洁、最推荐的方式是利用"Meyers' Singleton"模式。这种方式利用了C++11标准就已保证的"函数内静态变量初始化是线程安全的"这一特性,代码非常优雅。
C++17 实现代码
cpp
#include <iostream>
// 单例类
class Singleton {
public:
// 1. 提供一个全局访问点,用于获取唯一的实例
static Singleton& getInstance() {
// C++11及以后标准保证,静态局部变量的初始化是线程安全的。
// 这是一种高效的懒加载(Lazy Initialization)实现。
static Singleton instance;
return instance;
}
// 2. 删除拷贝构造函数和拷贝赋值运算符,防止实例被复制
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 3. 删除移动构造函数和移动赋值运算符,防止实例被移动
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
// 示例业务方法
void doSomething() {
std::cout << "Singleton is working at address: " << this << std::endl;
}
private:
// 4. 将构造函数和析构函数设为私有,防止外部创建或销毁实例
Singleton() {
std::cout << "Singleton constructed." << std::endl;
}
~Singleton() {
std::cout << "Singleton destroyed." << std::endl;
}
};
// 使用示例
int main() {
// 通过 getInstance() 获取实例
Singleton::getInstance().doSomething();
Singleton::getInstance().doSomething();
// 验证两次获取的是同一个实例
Singleton& s1 = Singleton::getInstance();
Singleton& s2 = Singleton::getInstance();
if (&s1 == &s2) {
std::cout << "Success: s1 and s2 are the same instance." << std::endl;
}
return 0;
}
关键点解析
- 线程安全与懒加载 :
static Singleton instance;在getInstance()函数内部声明。这个变量只在第一次调用该函数时才会被创建,并且C++标准保证了在多线程环境下这个创建过程是线程安全的,无需手动加锁。 - 防止拷贝和移动 :使用
= delete语法明确禁止了编译器自动生成拷贝/移动构造函数和赋值运算符,确保了实例的唯一性。 - 控制构造与析构 :将构造函数和析构函数设为
private,强制所有访问都必须通过getInstance(),从而完全掌控对象的生命周期。
C++ 单例中:拷贝/移动构造、赋值运算符必须用引用的核心原因
我直接给你讲最本质、最关键、最容易理解的原因,不绕弯子。
一、先看代码里的这 4 个函数
cpp
Singleton(const Singleton&) = delete; // 拷贝构造
Singleton& operator=(const Singleton&) = delete; // 拷贝赋值
Singleton(Singleton&&) = delete; // 移动构造
Singleton& operator=(Singleton&&) = delete; // 移动赋值
它们的共同点:参数和返回值都用了引用(& / &&)。
二、为什么参数必须用引用?
1. 不用引用 = 会触发无限递归(编译死循环)
C++ 规定:
如果函数参数是值传递(不是引用),调用时会自动拷贝一份实参。
而拷贝构造函数本身就是用来做拷贝的。
如果你写成:
cpp
// ❌ 错误写法
Singleton(const Singleton other); // 没有 &
当你试图调用:
cpp
Singleton a = b;
会发生:
- 调用拷贝构造
- 参数
other是值传递 → 必须拷贝 b - 拷贝 b 又要调用拷贝构造
- 无限循环 → 编译直接报错
2. 单例本身就禁止拷贝
单例的核心规则:
全世界只能有一个对象,绝不允许复制、移动。
所以:
- 用
const Singleton&:只看、不拷贝、不修改 - 用
Singleton&&:只识别右值,不允许移动
引用的作用就是:不产生新对象,只传递身份。
三、为什么赋值运算符必须返回引用?
cpp
Singleton& operator=(...);
1. 符合 C++ 内置类型的默认行为
比如:
cpp
int a, b, c;
a = b = c; // 连续赋值
b = c 返回的是 b 的引用,才能链式赋值。
用户自定义类型也遵循这个规则:
赋值运算符应当返回自身的引用,以支持连续赋值。
2. 单例虽然已经 delete 了赋值,但语法规范不能错
即使你不让它被调用,语法格式也必须正确:
- 赋值运算符 必须返回
类名& - 这是 C++ 标准规定的写法
四、总结:4 个函数用引用的 3 个终极原因
- 防止无限递归(值传递会触发拷贝,拷贝又调用拷贝构造)
- 不产生新对象(单例绝对不允许复制/移动)
- 符合 C++ 语法规范(赋值运算符必须返回自身引用)
五、最简单的记忆口诀
构造传值会递归,单例禁止造副本;
赋值必须返自身,引用就是不复制。
总结
- 参数用引用:避免无限递归 + 不拷贝对象
- 返回用引用:符合 C++ 赋值运算符标准语法
- 全部 delete :彻底保证单例唯一、不可复制、不可移动
如果你愿意,我还能给你对比错误写法 和正确写法的编译报错,让你一眼看懂为什么必须用引用。