C++ 单例模式硬核通关:从“只创建一个对象”到真正写对企业级 Singleton
C++ 单例模式硬核通关:从“只创建一个对象”到真正写对企业级 Singleton
很多初学者第一次接触单例模式,脑子里通常只有一句话:
单例模式就是保证一个类只有一个对象。
这句话没错,但太浅了。
真正写代码时,你很快会遇到一堆更具体的问题:
- 为什么构造函数要写成
private? - 只把构造函数私有化就够了吗?
- 为什么还要禁用拷贝构造和赋值运算符?
static Singleton instance;这个对象到底放在栈上、堆上,还是全局区?~Singleton() = default;是不是“不写析构”,会不会不释放资源?- 多线程同时调用
GetInstance()会不会创建多个对象? - 配置文件、日志系统这种全局资源,到底适不适合用单例?
这篇文章就从这些真实问题出发,把 C++ 单例模式从基础写法一路讲到推荐实现、配置文件实战、常见误区和复习速查表。
目标很明确:你看完之后,不只是会背 Singleton,而是能在真实项目里判断该不该用、怎么写、哪里会炸。
一、引言:为什么你以为会写单例,其实很容易写错
假设项目里有一个配置文件 camera.toml,里面保存相机分辨率、帧率、曝光参数等信息。
很多模块都要读取它:
auto width = config["camera"]["width"];
auto height = config["camera"]["height"];
auto fps = config["camera"]["fps"];
这时候你肯定不希望每个模块都自己解析一遍配置文件。
因为这样会带来几个问题:
| 问题 | 后果 |
|---|---|
| 重复加载配置文件 | 浪费 IO 和解析成本 |
| 多处维护配置对象 | 状态容易不一致 |
| 每个模块都能随便创建配置对象 | 代码边界混乱 |
| 多线程同时初始化配置 | 可能出现竞态条件 |
所以我们自然会想到:
配置对象全程序只需要一个,并且所有地方都通过同一个入口访问它。
这就是单例模式最典型的使用场景。
但问题来了,很多人第一版会这么写:
class Config
{
public:
static Config* GetInstance()
{
if (instance_ == nullptr)
{
instance_ = new Config();
}
return instance_;
}
private:
Config() = default;
private:
static Config* instance_;
};
看起来没毛病,实际上问题一堆:
new出来的对象谁释放?- 多线程同时进
if (instance_ == nullptr)怎么办? - 如果忘了定义
Config::instance_,还会链接报错。 - 外部虽然不能
new Config(),但如果没有禁用拷贝,仍然可能通过复制制造新对象。
避坑提示
单例模式不是“写一个静态指针”这么简单。它真正要解决的是:创建入口唯一、实例数量唯一、生命周期可控、多线程初始化安全。
二、核心概念:单例模式到底在控制什么
单例模式的核心目的只有一句话:
保证一个类在整个程序运行期间有且仅有一个实例,并提供一个全局访问点。
这句话可以拆成三个动作。
| 核心支柱 | 具体手段 | 解决的问题 |
|---|---|---|
| 禁止外部创建 | 构造函数设为 private |
外部不能 new Singleton(),也不能直接声明对象 |
| 禁止复制对象 | 拷贝构造、赋值运算符 = delete |
防止通过复制绕过唯一性 |
| 提供唯一入口 | static GetInstance() |
外部只能从固定入口拿到实例 |
1. 为什么构造函数必须私有化
如果构造函数是 public,外部可以这样写:
Singleton s1;
Singleton s2;
那就已经不是单例了。
也可以这样写:
auto* p1 = new Singleton();
auto* p2 = new Singleton();
这更失控。
所以第一步一定是:
private:
Singleton() = default;
意思是:这个类的对象只能由类自己创建,外部没有资格创建。
2. 为什么还要禁用拷贝和赋值
很多人只记得私有化构造函数,却忘了复制也能产生新对象。
比如如果你返回的是对象本身,或者内部某个接口暴露不当,可能出现:
Singleton copy = Singleton::GetInstance();
为了从语义上彻底堵住复制入口,标准写法会加上:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
含义很直白:
- 不允许拷贝构造
- 不允许赋值复制
如果有人误用,编译期直接报错。
底层思考
单例模式真正怕的不是“你现在没写错”,而是“未来某个调用者能不能绕过你的设计”。
= delete的价值就在这里:把错误用法提前卡死在编译期。
3. 为什么 GetInstance() 必须是静态函数
普通成员函数必须先有对象才能调用:
Singleton obj;
obj.GetInstance();
但单例的矛盾点就在这里:对象不能由外部创建。
所以访问入口必须属于类本身,而不是某个对象:
static Singleton& GetInstance();
调用方式就是:
Singleton::GetInstance();
这就是所谓的“全局访问点”。
三、渐进式演进:从能跑到真正推荐
这一节是重点。我们不直接丢最终答案,而是按真实学习路径一步步升级。
Version 1.0:最朴素的单例实现
第一版代码可以这样写:
#include <iostream>
class Singleton
{
public:
// 对外提供全局访问点
static Singleton* GetInstance()
{
// 第一次访问时创建对象
if (instance_ == nullptr)
{
instance_ = new Singleton();
}
return instance_;
}
void SayHello()
{
std::cout << "Hello Singleton\n";
}
private:
// 构造函数私有化,禁止外部直接创建对象
Singleton() = default;
private:
// 保存唯一实例的静态指针
static Singleton* instance_;
};
// 静态成员变量必须在类外定义
Singleton* Singleton::instance_ = nullptr;
int main()
{
Singleton::GetInstance()->SayHello();
return 0;
}
这版代码能跑,也能体现单例的基本思想。
但它不是好实现。
核心逻辑拆解
instance_是一个静态指针,所有调用者共享。- 第一次调用
GetInstance()时,发现instance_ == nullptr,于是new Singleton()。 - 后续调用直接返回之前创建的对象。
- 构造函数是
private,外部不能直接创建对象。
看似完整,其实有明显缺陷。
Version 1.0 的问题
问题 1:内存释放不明确
对象是这样创建的:
instance_ = new Singleton();
那它应该在哪里 delete?
如果一直不释放,严格来说就是泄漏。虽然很多进程退出时操作系统会回收内存,但这不是一个干净的 C++ 资源管理方式。
如果手动提供 Destroy():
static void Destroy()
{
delete instance_;
instance_ = nullptr;
}
又会引入新问题:
- 谁负责调用?
- 调用早了,后面模块再访问怎么办?
- 多线程下一个线程销毁,另一个线程正在使用怎么办?
问题 2:多线程不安全
假设两个线程同时第一次调用:
if (instance_ == nullptr)
{
instance_ = new Singleton();
}
可能发生这样的时序:
| 时间点 | 线程 A | 线程 B |
|---|---|---|
| 1 | 判断 instance_ == nullptr 成立 |
|
| 2 | 判断 instance_ == nullptr 成立 |
|
| 3 | 创建一个 Singleton |
|
| 4 | 又创建一个 Singleton |
结果就是:单例变双例。
问题 3:没有禁用拷贝和赋值
如果接口稍微改错,或者未来维护者返回对象而不是指针/引用,就可能复制出新实例。
所以单例类必须显式写:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Version 2.0:用锁修复线程安全
既然多线程下 if (instance_ == nullptr) 有问题,那我们可以加锁。
#include <iostream>
#include <mutex>
class Singleton
{
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* GetInstance()
{
// 加锁保护初始化过程,避免多个线程同时创建对象
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr)
{
instance_ = new Singleton();
}
return instance_;
}
static void Destroy()
{
std::lock_guard<std::mutex> lock(mutex_);
delete instance_;
instance_ = nullptr;
}
void SayHello()
{
std::cout << "Hello Singleton\n";
}
private:
Singleton() = default;
~Singleton() = default;
private:
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
这版比 1.0 强不少:
- 构造函数私有化了。
- 禁用了拷贝和赋值。
- 初始化过程加锁了。
- 提供了销毁函数。
但它仍然不是最推荐的现代 C++ 写法。
核心逻辑拆解
std::mutex mutex_用来保护共享指针instance_。std::lock_guard<std::mutex>利用 RAII 自动加锁、自动解锁。- 多线程同时调用
GetInstance()时,只有一个线程能进入临界区。 Destroy()负责释放new出来的对象。
这版还有什么不舒服
第一,每次调用 GetInstance() 都要加锁。
即使对象已经创建好了,仍然要进入锁逻辑:
std::lock_guard<std::mutex> lock(mutex_);
如果 GetInstance() 调用频率很高,这就是不必要的开销。
第二,生命周期还是需要人工管理。
你必须记得调用:
Singleton::Destroy();
忘了不干净,早了会出事,多线程下还要考虑并发访问。
第三,代码变复杂了。
手动维护:
- 静态指针
- 静态锁
- 初始化
- 销毁
这套东西本质上是在和 C++ 运行时抢活干。
避坑提示
能交给语言标准保证的事情,不要自己手写一套复杂机制。单例初始化就是典型例子。
Version 3.0:Meyers’ Singleton,现代 C++ 首选写法
C++ 里最推荐的单例写法是 Meyers’ Singleton。
代码非常短:
#include <iostream>
class Singleton
{
public:
// 禁止拷贝构造,防止通过复制创建第二个实例
Singleton(const Singleton&) = delete;
// 禁止赋值运算,防止把单例状态复制给另一个对象
Singleton& operator=(const Singleton&) = delete;
// 全局唯一访问点
static Singleton& GetInstance()
{
// 局部静态变量:第一次执行到这里时初始化
// C++11 起,局部静态变量初始化由标准保证线程安全
static Singleton instance;
// 返回引用,避免空指针,也避免调用者 delete
return instance;
}
void SayHello()
{
std::cout << "Hello Meyers Singleton\n";
}
private:
// 构造函数私有化,外部不能创建对象
Singleton() = default;
// 析构函数私有化,外部不能手动 delete
// = default 表示使用编译器生成的默认析构函数,不是不析构
~Singleton() = default;
};
int main()
{
Singleton::GetInstance().SayHello();
return 0;
}
这就是很多 C++ 项目里最常见、最推荐的版本。
核心逻辑拆解
关键只有这一行:
static Singleton instance;
它不是普通局部变量。
虽然写在函数里面,但因为有 static,所以它具有静态存储期。
| 属性 | 说明 |
|---|---|
| 写法位置 | 函数内部 |
| 生命周期 | 从第一次初始化成功开始,到程序退出 |
| 内存区域 | 静态/全局存储区 |
| 初始化时机 | 第一次执行到这行代码 |
| 销毁时机 | main() 结束或调用 exit() 后,由 C++ 运行时析构 |
| 线程安全 | C++11 起标准保证初始化线程安全 |
这几个点非常重要。
它写在函数里,所以外部访问不到。
它是 static,所以不会随着函数返回销毁。
它第一次调用才创建,所以是懒加载。
它由运行时负责销毁,所以不需要手动 delete。
为什么 Meyers’ Singleton 是首选
1. 自动线程安全
C++11 起,标准规定:
局部静态变量的初始化过程是线程安全的。
也就是说,多个线程同时第一次调用 GetInstance() 时,编译器和运行时会保证只有一个线程完成初始化。
你不需要自己写:
std::mutex
std::lock_guard
2. 延迟加载
实例不会在程序启动时立刻创建,而是在第一次调用时创建:
Singleton::GetInstance();
不调用,就不初始化。
这对配置、日志、线程池这类资源很有用。
3. 自动释放
instance 是静态存储期对象。程序退出时,C++ 运行时会自动调用它的析构函数。
所以不需要:
delete instance;
也不需要:
Destroy();
4. 返回引用更安全
推荐返回:
static Singleton& GetInstance()
而不是:
static Singleton* GetInstance()
原因是:
| 返回方式 | 特点 | 风险 |
|---|---|---|
| 返回指针 | 调用者要用 -> |
可能误以为需要判空或 delete |
| 返回引用 | 调用者用 . |
语义更明确:一定存在,不归你释放 |
单例对象一旦返回,就应该表达“它一定存在,且生命周期由类自己管理”。
引用更符合这个语义。
四、企业级实战:配置文件单例
前面的 SayHello() 只是演示。真实项目里,单例经常用于:
- 配置管理器
- 日志系统
- 线程池
- 全局资源注册表
- 硬件设备管理器
下面用配置文件单例举例。
假设项目里用 tomlplusplus 解析 TOML 配置文件,用 LOG_E / LOG_I 打日志。
完整代码如下:
#pragma once
#include <sstream>
#include <string>
#include "spdlog/log_common.hpp"
#include "tomlplusplus/toml.hpp"
#include "utils/Log.h"
namespace config {
class ConfigSingleton
{
public:
// 禁止拷贝构造,保证不会通过复制得到第二份配置管理器
ConfigSingleton(const ConfigSingleton&) = delete;
// 禁止赋值运算,避免单例对象之间状态复制
ConfigSingleton& operator=(const ConfigSingleton&) = delete;
// 获取全局唯一配置对象
static ConfigSingleton& GetInstance()
{
// C++11 起线程安全;第一次调用时创建;程序退出时自动析构
static ConfigSingleton instance;
return instance;
}
// 从 TOML 文件加载配置
bool LoadFromFile(const std::string& conf_file = "./config/camera.toml")
{
try
{
// toml::parse_file 可能因为文件不存在、格式错误等原因抛异常
config_ = toml::parse_file(conf_file);
}
catch (const std::exception& e)
{
// 捕获异常,记录错误,返回 false 让上层决定是否退出程序
LOG_E("TomlConfig Load Error: {}", std::string(e.what()));
return false;
}
// 把配置内容转成字符串,便于启动时确认实际加载内容
std::ostringstream oss;
oss << config_;
LOG_I("camera.toml context:{}", oss.str());
return true;
}
// 对外只暴露 const 引用,避免外部随意修改配置表
[[nodiscard]] const toml::table& Config() const
{
return config_;
}
private:
// 构造函数私有化,外部不能创建 ConfigSingleton
ConfigSingleton() = default;
// 析构函数私有化,外部不能 delete
// = default 会正常析构成员变量 config_
~ConfigSingleton() = default;
private:
// 保存解析后的 TOML 表
toml::table config_;
};
} // namespace config
使用方式
程序启动阶段加载一次:
bool ok = config::ConfigSingleton::GetInstance().LoadFromFile("./config/camera.toml");
if (!ok)
{
// 根据项目要求决定:退出、使用默认配置、或者进入降级逻辑
return -1;
}
业务模块里读取配置:
const auto& cfg = config::ConfigSingleton::GetInstance().Config();
auto width = cfg["camera"]["width"].value_or(1920);
auto height = cfg["camera"]["height"].value_or(1080);
auto fps = cfg["camera"]["fps"].value_or(30);
为什么这里返回 const toml::table&
配置对象对外暴露时,推荐这样写:
[[nodiscard]] const toml::table& Config() const
{
return config_;
}
原因有三个:
| 写法 | 结果 |
|---|---|
返回 toml::table |
会复制整张配置表,没必要 |
返回 toml::table& |
外部可以随便改配置,风险大 |
返回 const toml::table& |
不复制,并且外部不能直接修改 |
[[nodiscard]] 的作用是提醒调用者:这个返回值你大概率不应该忽略。
如果有人写:
config::ConfigSingleton::GetInstance().Config();
但没有使用返回值,编译器可以给出提醒。
LoadFromFile 为什么返回 bool
配置加载失败是很现实的情况:
- 文件路径写错
- 文件不存在
- TOML 语法错误
- 权限不足
- 部署时漏拷贝配置文件
所以 LoadFromFile() 不应该假装一定成功。
推荐这样设计:
bool LoadFromFile(const std::string& conf_file)
失败时:
return false;
让上层决定怎么处理。
避坑提示
配置加载失败不要静默吞掉。启动阶段直接打印明确错误,比运行半小时之后某个模块拿到默认值再炸要好排查得多。
五、避坑指南:这些坑初学者特别容易踩
坑 1:以为 = default 等于“不释放”
看到这行代码:
~Singleton() = default;
很多人会误解:
默认析构是不是代表什么都不做?那成员变量会不会不释放?
答案很明确:
= default 不是“不析构”,而是让编译器生成默认析构函数。
默认析构函数会做该做的事情:
- 调用成员变量的析构函数。
- 调用基类的析构函数。
- 按 C++ 对象模型正常销毁对象。
比如:
class Demo
{
private:
std::string name_;
std::vector<int> data_;
};
即使你写:
~Demo() = default;
name_ 和 data_ 也会被正常析构。
真正危险的是你手写了错误的析构逻辑,或者用裸指针管理资源却忘了释放。
坑 2:局部静态变量不是栈变量
这行代码特别容易误导人:
static Singleton instance;
因为它写在函数里面,有人会以为它在栈上。
但它不是。
普通局部变量才在栈上:
Singleton instance;
加了 static 后,它具有静态存储期:
static Singleton instance;
对比一下:
| 变量形式 | 内存/生命周期特点 |
|---|---|
| 普通局部变量 | 通常在栈上,函数返回后销毁 |
| 局部静态变量 | 静态存储期,第一次执行到定义处初始化,程序退出时销毁 |
new 创建对象 |
堆上,需要手动 delete 或智能指针管理 |
| 全局变量 | 程序启动阶段初始化,程序退出时销毁 |
所以 Meyers’ Singleton 既不是栈对象,也不是堆对象,而是局部作用域里的静态存储期对象。
坑 3:只私有化构造函数,不禁用拷贝
错误写法:
class Singleton
{
public:
static Singleton& GetInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton() = default;
};
这版代码少了:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
正确写法:
class Singleton
{
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& GetInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
};
单例模式要堵住的是所有创建第二个实例的入口。
构造函数只是其中一个入口,复制也是入口。
坑 4:返回指针导致调用者误解所有权
不推荐:
static Singleton* GetInstance()
{
static Singleton instance;
return &instance;
}
虽然这也能工作,但指针容易给调用者错误暗示:
- 这个指针会不会是空?
- 我要不要判空?
- 我要不要释放?
推荐:
static Singleton& GetInstance()
{
static Singleton instance;
return instance;
}
引用表达的语义更干净:
这个对象一定存在,你只管用,不要管释放。
坑 5:多个单例之间互相依赖
比如:
Logger::GetInstance().Info("config loaded");
Config::GetInstance().LoadFromFile();
如果 ConfigSingleton 的析构函数里又写日志:
~ConfigSingleton()
{
Logger::GetInstance().Info("destroy config");
}
程序退出时就可能遇到析构顺序问题。
原因是:不同静态对象之间的析构顺序并不总是符合你的直觉。
如果 Logger 已经析构了,ConfigSingleton 析构时再用日志,就可能出问题。
避坑提示
单例之间尽量不要在析构阶段互相调用。尤其是日志、配置、线程池这种全局设施,退出阶段的依赖关系要非常克制。
坑 6:把单例当万能全局变量
单例本质上是“受控的全局状态”。
它适合管理真正全局唯一的资源:
| 适合使用单例 | 不适合使用单例 |
|---|---|
| 配置管理器 | 普通业务对象 |
| 日志系统 | 用户订单对象 |
| 线程池 | 临时计算上下文 |
| 硬件设备管理器 | 每个请求独立的数据 |
如果一个对象并不要求全局唯一,就不要硬做成单例。
否则后果就是:
- 模块耦合变强。
- 单元测试变难。
- 状态污染难排查。
- 代码越来越像到处乱飞的全局变量。
坑 7:单元测试不友好
单例持有全局状态,测试时很容易出现这种情况:
TEST(ConfigTest, LoadA)
{
ConfigSingleton::GetInstance().LoadFromFile("a.toml");
}
TEST(ConfigTest, LoadB)
{
ConfigSingleton::GetInstance().LoadFromFile("b.toml");
}
如果两个测试共享同一个单例对象,就可能互相影响。
解决思路有几种:
| 方案 | 适用场景 | 代价 |
|---|---|---|
提供测试专用 Reset() |
小项目、内部工具 | 破坏单例纯粹性 |
| 测试进程隔离 | 状态复杂、要求稳定 | 执行成本更高 |
| 依赖注入替代单例 | 对可测试性要求高 | 架构稍复杂 |
| 只把无状态服务做单例 | 工具类、日志类 | 适用范围有限 |
实际项目里要根据场景取舍。
如果是配置中心这种启动时加载一次、运行期只读的对象,单例可以接受。
如果是业务状态频繁变化的对象,就要谨慎。
六、几种单例实现方式对比
把常见写法放在一张表里,对比会更清楚。
| 实现方式 | 线程安全 | 是否懒加载 | 是否需要手动释放 | 推荐度 | 说明 |
|---|---|---|---|---|---|
裸指针 + new |
否 | 是 | 是 | 不推荐 | 早期写法,容易泄漏和竞态 |
裸指针 + mutex |
是 | 是 | 是 | 一般 | 能用但代码复杂,生命周期麻烦 |
| 双重检查锁 DCLP | 可以做到 | 是 | 通常需要 | 谨慎 | 细节多,现代 C++ 没必要优先选 |
| 饿汉式静态对象 | 是 | 否 | 否 | 可用 | 程序启动即创建,适合轻量对象 |
| Meyers’ Singleton | C++11 起安全 | 是 | 否 | 首选 | 简洁、懒加载、自动析构 |
饿汉式是什么
饿汉式就是程序启动时提前创建对象。
class Singleton
{
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& GetInstance()
{
return instance_;
}
private:
Singleton() = default;
~Singleton() = default;
private:
static Singleton instance_;
};
Singleton Singleton::instance_;
它的优点是简单,初始化也天然只发生一次。
缺点是:不管你用不用,程序启动时都会创建它。
如果对象很轻量,比如一个无状态工具类,问题不大。
如果对象初始化很重,比如加载配置、连接数据库、启动线程池,就不适合无脑饿汉式。
双重检查锁为什么不优先推荐
很多文章会写这种:
if (instance_ == nullptr)
{
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr)
{
instance_ = new Singleton();
}
}
这叫双重检查锁。
它的目的很明确:
- 外层
if:避免每次都加锁。 - 内层
if:避免多个线程排队进入锁后重复创建。
但这类写法涉及内存模型、指令重排、对象构造可见性等细节。
现代 C++ 里,如果没有非常特殊的理由,直接用 Meyers’ Singleton 就够了。
核心结论
面试可以理解双重检查锁,项目里优先写 Meyers’ Singleton。少写一堆并发细节,就少一堆隐藏 bug。
七、最终推荐模板
如果你只是想在项目里快速写一个标准、干净、现代 C++ 风格的单例,可以直接套这个模板:
class Singleton
{
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& GetInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
};
如果类里有业务方法,就加在 public 区域:
class Logger
{
public:
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
static Logger& GetInstance()
{
static Logger instance;
return instance;
}
void Info(const std::string& message)
{
// 写日志逻辑
}
private:
Logger() = default;
~Logger() = default;
};
调用方式:
Logger::GetInstance().Info("program started");
记住这个形状就够用了。
八、复习心法:一张表带走
| 问题 | 正确理解 |
|---|---|
| 单例模式解决什么问题 | 保证某个类全程序只有一个实例,并提供统一访问入口 |
| 构造函数为什么私有 | 防止外部直接创建对象 |
为什么要 = delete 拷贝和赋值 |
防止通过复制创建第二个实例 |
GetInstance() 为什么是 static |
没有对象时也能通过类名访问 |
| 推荐返回指针还是引用 | 推荐引用,语义更明确,不需要判空和释放 |
static Singleton instance 在哪里 |
静态/全局存储区,不是栈,不是堆 |
| 什么时候创建 | 第一次执行到定义处时创建 |
| 什么时候销毁 | 程序退出时由 C++ 运行时自动析构 |
= default 是不是不析构 |
不是,会生成正常的默认析构函数 |
| C++11 后局部静态变量线程安全吗 | 初始化过程线程安全 |
| 最推荐写法 | Meyers’ Singleton |
| 最大风险 | 滥用导致全局状态泛滥、测试困难、模块耦合 |
再压缩成一句话:
现代 C++ 写单例,优先用函数内局部静态变量;构造私有,拷贝删除,返回引用,让语言标准替你处理线程安全和生命周期。
九、最后给初学者的判断标准
写单例之前,先问自己三个问题:
- 这个对象是否真的全程序只应该有一个?
- 它是否代表某种全局资源,比如配置、日志、线程池、设备管理器?
- 它持有的状态会不会让测试和模块边界变得混乱?
如果三个问题都想清楚了,再用单例。
如果只是为了“哪里都能访问方便”,那大概率是在给未来埋坑。
单例不是不能用,而是要用在刀刃上。
真正成熟的写法不是把所有东西都做成 Singleton,而是只让那些天然全局唯一、生命周期贯穿程序、访问入口需要统一管理的对象成为 Singleton。
这才是单例模式的正确打开方式。
更多推荐



所有评论(0)