C++ 多线程安全的单例模式

🧩 实现一个线程安全的单例模式

在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;
}
关键点解析
  1. 线程安全与懒加载static Singleton instance;getInstance() 函数内部声明。这个变量只在第一次调用该函数时才会被创建,并且C++标准保证了在多线程环境下这个创建过程是线程安全的,无需手动加锁。
  2. 防止拷贝和移动 :使用 = delete 语法明确禁止了编译器自动生成拷贝/移动构造函数和赋值运算符,确保了实例的唯一性。
  3. 控制构造与析构 :将构造函数和析构函数设为 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;

会发生:

  1. 调用拷贝构造
  2. 参数 other 是值传递 → 必须拷贝 b
  3. 拷贝 b 又要调用拷贝构造
  4. 无限循环 → 编译直接报错

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 个终极原因

  1. 防止无限递归(值传递会触发拷贝,拷贝又调用拷贝构造)
  2. 不产生新对象(单例绝对不允许复制/移动)
  3. 符合 C++ 语法规范(赋值运算符必须返回自身引用)

五、最简单的记忆口诀

构造传值会递归,单例禁止造副本;
赋值必须返自身,引用就是不复制。


总结

  • 参数用引用:避免无限递归 + 不拷贝对象
  • 返回用引用:符合 C++ 赋值运算符标准语法
  • 全部 delete :彻底保证单例唯一、不可复制、不可移动

如果你愿意,我还能给你对比错误写法正确写法的编译报错,让你一眼看懂为什么必须用引用。

相关推荐
咩咦4 小时前
C++学习笔记28:静态成员应用:不用循环求1到n的和
c++·学习笔记·类和对象·static·构造函数·oj·静态成员
EllinY4 小时前
CF2217E Definitely Larger 题解
c++·笔记·算法·构造
筠筠喵呜喵5 小时前
Linux软件开发性能优化
linux·c++·性能优化
Bruce_kaizy5 小时前
c++ linux环境编程——文件io介绍以及open 、write 、read 三剑客深度详解
linux·服务器·c++·ubuntu·操作系统·文件io
PAK向日葵8 小时前
我用 C++ 写了一个轻量级 Python 虚拟机,刚刚开源
c++·python·开源
玖釉-8 小时前
下一个排列:从字典序到原地算法的完整推导
数据结构·c++·windows·算法
枕星而眠8 小时前
数据结构八大排序详解(一):四大简单排序
c语言·数据结构·c++·后端
努力努力再努力wz9 小时前
【Qt入门系列】:按钮组件全解析:从 QAbstractButton 到快捷键事件、单选与复选机制
c语言·开发语言·数据结构·c++·git·qt·github
yunn_9 小时前
单例模式两种实现方法
开发语言·c++·单例模式
代钦塔拉12 小时前
C++ auto
开发语言·c++