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

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

换句话说:

整个类只想要一个对象

全局都可以使用这个对象

不允许创建第二个对象

再比如:

程序的配置管理器(只需要一个)

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

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

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

懒汉单例(c++11后)

cpp 复制代码
#include <iostream>
#include <mutex>

class Singleton {
public:
    // 获取唯一实例的全局访问点
    static Singleton& getInstance() {
        static Singleton instance; // C++11保证线程安全
        return instance;
    }

    void doSomething() {
        std::cout << "我是唯一的实例,正在工作..." << std::endl;
    }

private:
    // 构造函数私有,禁止外部 new
    Singleton() {
        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;
}

结果:

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

懒汉单例 vs 饿汉单例

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

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

懒汉单例的实现:

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

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

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

适合"可能用,也可能不用"的场景

饿汉单例的实现:

cpp 复制代码
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 单例:

cpp 复制代码
Database& db = Database::getInstance();
db.connect("mysql://...");

问题来了:

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

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

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

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

测试时:

cpp 复制代码
MockDatabase mockDb;
MyService service(&mockDb);  // 传入假的db

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

3.生命周期难管理

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

cpp 复制代码
static Logger& getInstance() {
    static Logger instance;
    return instance;
}

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

静态对象的销毁顺序

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

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

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

例如:

cpp 复制代码
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() 会被调用

问题出现的场景

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

cpp 复制代码
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://..." 写在代码里就是 硬编码,你要测试改数据库就必须改那段源码。

cpp 复制代码
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就可以使用这个logger
    service.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() 里创建一个对象,然后把它传递给需要的地方,完全不需要全局变量或单例。

相关推荐
EnigmaCoder4 小时前
【C++】引用的本质与高效应用
开发语言·c++
郭涤生4 小时前
arma::imat22
c++
zhangfeng11334 小时前
BiocManager下载失败 R语言 解决办法
开发语言·r语言
CoderYanger5 小时前
MySQL数据库——3.2.1 表的增删查改-查询部分(全列+指定列+去重)
java·开发语言·数据库·mysql·面试·职场和发展
炮院李教员5 小时前
使用Qt Core模块(无GUI依赖),确保程序作为后台服务/daemon运行,与任何GUI完全无交互。
开发语言·qt
歪歪1005 小时前
Qt Creator 打包应用程序时经常会遇到各种问题
开发语言·c++·qt·架构·编辑器
滴滴滴嘟嘟嘟.5 小时前
Qt自定义列表项与QListWidget学习
开发语言·qt·学习
PEI046 小时前
MVCC(多版本并发控制)
java·开发语言·数据库
熊猫钓鱼>_>6 小时前
2025反爬虫之战札记:从robots.txt到多层防御的攻防进化史
开发语言·c++·爬虫