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

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

相关推荐
草莓熊Lotso3 小时前
Linux Socket 编程筑基:从底层本质到核心 API,一文吃透 Socket 预备知识
linux·运维·服务器·数据库·c++
浅念-3 小时前
LeetCode最短路必看:BFS算法原理+经典题解
数据结构·c++·算法·leetcode·职场和发展·bfs·宽度优先
say_fall3 小时前
装软件慢到崩溃?用户创建总出错?Linux 工具避坑指南
linux·运维·服务器·c++·学习
叼烟扛炮3 小时前
C++ 知识点02 输入输出
开发语言·c++·算法·输入输出
小此方3 小时前
Re:思考·重建·记录 现代C++ C++11篇(六) 从 shared_ptr 到 weak_ptr:起底智能指针的引用计数与循环引用之痛
开发语言·c++·c++11·现代c++
字节高级特工3 小时前
MySQL数据库基础与实战指南
数据库·c++·人工智能·后端·mysql·adb
郝学胜-神的一滴3 小时前
中级OpenGL教程 004:为几何体注入法线灵魂
c++·unity·游戏引擎·godot·图形渲染·opengl·unreal
晨非辰3 小时前
吃透C++两大默认成员函数:const成员函数、 & 取地址运算符重载
java·大数据·开发语言·c++·人工智能·后端·面试
雪度娃娃3 小时前
创建型设计模式——建造者模式
c++·microsoft·设计模式·建造者模式