当前位置: 首页 > news >正文

单例模式(巨通俗易懂)普通单例,懒汉单例的实现和区别,依赖注入......

单例模式:保证在整个程序中,某个类只有一个实例,并且可以全局可访问。

换句话说:

整个类只想要一个对象

全局都可以使用这个对象

不允许创建第二个对象

再比如:
程序的配置管理器(只需要一个)

日志系统(写日志的对象只有一个)

数据库连接池(共享同一个)

想用全局变量(虽然用单例模式也不太好)

懒汉单例(c++11后)

#include <iostream>
#include <mutex>class Singleton {
public:// 获取唯一实例的全局访问点static Singleton& getInstance() {static Singleton instance; // C++11保证线程安全return instance;}void doSomething() {std::cout << "我是唯一的实例,正在工作..." << std::endl;}private:// 构造函数私有,禁止外部 newSingleton() {std::cout << "单例对象创建成功!" << std::endl;}// 禁止拷贝和赋值Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;
};int main() {// 获取唯一实例Singleton& s1 = Singleton::getInstance();Singleton& s2 = Singleton::getInstance();s1.doSomething();// 检查是否是同一个对象if (&s1 == &s2) {std::cout << "s1 和 s2 是同一个实例" << std::endl;}return 0;
}

结果:

单例对象创建成功!
我是唯一的实例,正在工作...
s1 和 s2 是同一个实例

懒汉单例 vs 饿汉单例

在 C++ 中,常见的单例实现分为两种:

特性懒汉单例(Lazy Singleton)饿汉单例(Eager Singleton / 普通单例)
创建时机第一次使用 getInstance() 时才创建程序启动时就创建
资源使用节省资源,只有需要时才创建程序一启动就分配资源,可能会浪费
线程安全C++11 以后用 static 自动线程安全程序启动前初始化,天然线程安全
实现难度稍微复杂,需要考虑懒加载非常简单,直接静态初始化
性能首次访问时稍慢,但之后一样快程序启动时有初始化开销,访问快
适用场景创建代价大、偶尔使用的对象创建代价小、经常使用的对象

懒汉单例的实现:

class LazySingleton {
public:static LazySingleton& getInstance() {static LazySingleton instance; // 第一次调用时创建return instance;}
private:LazySingleton() {}LazySingleton(const LazySingleton&) = delete;LazySingleton& operator=(const LazySingleton&) = delete;
};

只有第一次调用getInstance()才会创造对象

如果程序没有用到这个对象,就不会浪费资源

适合“可能用,也可能不用”的场景

饿汉单例的实现:

class EagerSingleton {
public:static EagerSingleton& getInstance() {return instance;}
private:EagerSingleton() {}static EagerSingleton instance;  // 程序启动时就创建
};EagerSingleton EagerSingleton::instance;  // 定义并初始化

程序一启动就会创造对象

初始化顺序再程序启动阶段完成

适合“肯定会用”的场景,比如日志系统,配置管理器

static EagerSingleton instance;  写在类外,会在程序启动时就创建

tips:

能用单例不用全局变量,能不用单例就不用单例

单例模式不仅仅是“全局变量”,它还带来更好的封装性、控制力和安全性

特性全局变量单例模式
内存控制程序一启动就分配可以懒加载,按需创建
封装性所有人可以直接改值对象私有,接口可控
可维护性容易被误用提供统一访问入口
多实例风险程序员可以随便 new 多个禁止拷贝、赋值,确保唯一
线程安全需要自己管理C++11 的懒汉式默认安全
可扩展性改逻辑要修改全局代码可以封装到类里,扩展方便

那为什么能不用单例就不用单例

单例缺点:

1.单例的本质就是全局变量,说单例不好就是说全局变量不好。

全局状态会带来问题:

  • 难以追踪谁修改了它

  • 如果多人同时修改,可能出现不可预期的行为

  • 线程安全变复杂

2.难以进行单元测试

举个例子,有一个 Database 单例:

Database& db = Database::getInstance();
db.connect("mysql://...");

问题来了:

  • 如果我要写一个测试用例,让 Database 不连接真实的 MySQL,而是用一个假数据库(Mock Database),怎么办?

  • 单例让“依赖注入”变得困难,因为你无法轻易替换内部对象。

如果是普通类,我们可以这样:

class MyService {
public:MyService(Database* db) : db_(db) {}   // 构造函数,传入数据库对象指针void work() { db_->query("SELECT 1"); } // 调用数据库执行查询
private:Database* db_;                         // 保存数据库对象指针
};

测试时:

MockDatabase mockDb;
MyService service(&mockDb);  // 传入假的db

如果用单例,就很难做到这一点 → 这就是很多书批评单例的原因。

3.生命周期难管理

单例通常使用静态变量,比如:

static Logger& getInstance() {static Logger instance;return instance;
}

这个对象会在程序退出时自动销毁,但如果在其他全局对象的析构函数中访问 Logger,可能会出现“对象已被销毁”的错误。

静态对象的销毁顺序

在 C++ 中,静态对象(static 对象)的生命周期是:

  1. 程序运行到第一次使用它时创建(懒汉)或程序启动就创建(饿汉)

  2. 程序退出时会自动销毁(调用析构函数)

例如:

class Logger {
public:Logger() { std::cout << "Logger创建\n"; }~Logger() { std::cout << "Logger销毁\n"; }void log(const std::string& msg) { std::cout << msg << std::endl; }
};int main() {static Logger logger;logger.log("Hello");return 0;
}
  • logger 对象会在 main() 结束后自动销毁

  • 析构函数 ~Logger() 会被调用

问题出现的场景

假设你还有另一个全局对象:

class GlobalObject {
public:~GlobalObject() {Logger::getInstance().log("GlobalObject析构");}
};GlobalObject g_obj;  // 全局对象

程序运行顺序:

  1. main() 结束,静态对象开始销毁

  2. 全局对象 g_obj 的析构函数先执行

  3. 析构函数里调用 Logger::getInstance().log()

问题来了:

  • 如果 Logger 是静态对象(比如懒汉单例的 static Logger instance;

  • 它的析构函数可能已经被调用(对象已销毁)

  • 这时再调用 log()访问已销毁的对象 → 未定义行为 → 程序可能崩溃

在大型C++项目中,这个问题被称为 Static Initialization Order Fiasco(静态初始化顺序灾难)。
如果用普通类+依赖注入,可以更好地控制对象的生命周期。

4.隐式耦合,降低可维护性

单例是一种全局可见的状态,导致“想改一处,动全局”:

  • 你在某个地方修改了单例的状态

  • 另一个模块突然行为异常

  • 你完全不知道问题出在哪

如果改用依赖注入(把需要的对象通过构造函数传递),模块之间的依赖会更明确。

原因解释
全局状态单例本质是“全局变量”,会导致数据混乱
难测试单元测试很难替换单例对象
生命周期不可控静态对象销毁顺序不受控制
隐式耦合模块间依赖隐藏在单例中,难以维护
多线程风险

如果用C++98或旧代码,线程安全很难保证

不适合用单例

  1. 数据缓存

    如果缓存只是某个功能模块使用,用类成员更好
  2. 临时状态管理

    比如某个页面的UI状态,不需要全局唯一
  3. 可扩展性要求高

    如果未来有一天可能需要多个实例,提前用单例会很难改

替代方案

如果能不用单例,常见的替代方案有:

(1) 依赖注入(Dependency Injection)

不要在类内部自己创建或固定依赖,把依赖从外部传进来。

拿刚刚的数据库举例子来说,"mysql://..." 写在代码里就是 硬编码,你要测试改数据库就必须改那段源码。

class Logger {
public:void log(const std::string& msg); //用来打印或记录日志
};class Service {
public:Service(Logger* logger) : logger_(logger) {} // 把传进来的 logger 参数 赋值给类成员变量 logger_void run() { logger_->log("running"); }  // 成员函数
private:Logger* logger_;   //成员变量
};int main() {Logger logger; //创建一个Logger对象Service service(&logger);  // 明确注入依赖,把logger的地址传进去,service就可以使用这个loggerservice.run(); // Service调用Logger的log()方法
}

Service 类解析

  1. 成员变量 Logger* logger_

    • 指向一个 Logger 对象的指针

    • 用来在 Service 内部调用 Logger 的功能

  2. 构造函数 Service(Logger* logger)

    • 接收一个 Logger 对象指针作为参数

    • 初始化成员变量 logger_

    • 核心思想:Service 不负责创建 Logger,而是使用外部提供的 Logger → 这就是“依赖注入”

  3. : logger_(logger)初始化列表

    把传进来的 logger 参数 赋值给类成员变量 logger_
  • Service依赖 一个 Logger 对象来记录日志

  • Service 自己不创建 Logger,而是通过构造函数 外部传入 一个 Logger 对象

  • main() 函数创建 Logger 对象,再传给 Service 使用

好处:

  • 测试更容易(可以传入Mock对象(用于测试的假对象))

  • 生命周期由你控制

  • 模块之间的依赖明确

(2) 把对象放到 main() 里

有些时候,其实你只需要在 main() 里创建一个对象,然后把它传递给需要的地方,完全不需要全局变量或单例。

http://www.xdnf.cn/news/1481203.html

相关文章:

  • 【C++题解】DFS和BFS
  • leetcode 75 颜色分类
  • OS项目构建效能改进策划方案
  • 神马 M60S++ 238T矿机参数解析:高效SHA-256算法比拼
  • Docker加速下载镜像的配置指南
  • 计算机网络:物理层---数据通信基础知识
  • 【C++ 11 模板类】tuple 元组
  • 嵌入式笔记系列——UART:TTL-UART、RS-232、RS-422、RS-485
  • 旧电脑改造linux服务器2:安装系统
  • 软考中级习题与解答——第二章_程序语言与语言处理程序(3)
  • AD渗透中服务账号相关攻击手法总结(Kerberoasting、委派)
  • 数据仓库概要
  • 【selenium】网页元素找不到?从$(‘[placeholder=“手机号“]‘)说起
  • PyQt5 入门(上):开启 GUI 编程之旅
  • python advance -----object-oriented
  • URI与URL区别:资源ID和地址差异
  • Vue3中Vite的介绍与应用
  • 第1课:开篇:RAG技术与DeepSeek模型全景导读
  • Cloudflare for SaaS 实现 CNAME 接入 CDN 支持国内外智能分流建站
  • AI Agent侵入办公室
  • Android Audio Patch
  • 长尾关键词优化驱动SEO流量增长
  • 链动2+1模式:全渠道整合与用户角色化的商业逻辑与行为动机探析
  • ElasticSearch原理
  • CAN总线学习
  • HarmonyOS:通过组件导航设置信息提醒
  • 贪心算法应用:机器人路径平滑问题详解
  • 9月6日笔记
  • 让机器具有主动性-主动性算法[01]
  • HuggingFace Trainer(回调可视化)