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 :彻底保证单例唯一、不可复制、不可移动

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

相关推荐
星恒随风24 分钟前
C++ string 类详解:常用接口、OJ 场景与模拟实现中的深浅拷贝
开发语言·c++·笔记·学习·状态模式
程序喵大人37 分钟前
【C++并发系列】第二章:锁解决了什么问题?
开发语言·c++·并发编程·
天天代码码天天37 分钟前
用 TensorRT 加速 PP-OCR:一套 C++ DLL + C# 调用的高性能 OCR 推理方案
c++·c#·ocr
我不是懒洋洋1 小时前
从零实现一个分布式链路追踪:TraceId与Span
c++
森G1 小时前
78、框架分析------服务器源码解析----云视频服务项目
服务器·c++·qt
我不是懒洋洋1 小时前
【C++】string(string的成员变量、auto和范围for、string常用接口的说明、OJ题目、string的模拟实现)
c语言·开发语言·c++·visual studio
Brilliantwxx1 小时前
【C++】 C++11 知识点梳理(中)
开发语言·c++
j7~1 小时前
【C++】STL--Vector容器--拆析解剖Vector的实现以及Vector的底层详解(2)
开发语言·c++·动态二维数组·vector深度剖析·vector的实现·杨辉三角形
旖-旎2 小时前
《LeetCode 130 被围绕的区域 FloodFill DFS 解法》
c++·算法·深度优先·力扣·floodfill
一只旭宝10 小时前
【C++入门精讲22】常见设计模式
c++·设计模式