单例模式 6 种实现 + 避坑指南:别再手写单例了!从崩溃到稳如老狗

目录

引言:为什么你的全局对象总出问题?

一、先搞懂:单例模式到底是什么?

1.1 核心思想:“一个类,只有一个对象,全局能用”

1.2 单例模式的 3 个核心要素(缺一不可)

1.3 什么时候该用单例?3 个典型场景

二、C++ 单例模式 6 种实现方式:从简单到工业级

2.1 饿汉式单例:“程序一启动就创建,管你用不用”

核心思路:

代码实现:

测试代码:

运行结果:

优点:

缺点:

适用场景:

2.2 懒汉式单例(基础版):“用到的时候再创建,省内存”

核心思路:

代码实现:

测试代码(单线程):

单线程运行结果:

问题暴露(多线程测试):

多线程可能的运行结果(崩溃或多实例):

优点:

缺点:

适用场景:

2.3 懒汉式单例(线程安全版):“加锁防并发,安全第一”

核心思路:

代码实现:

测试代码(多线程):

运行结果:

关键优化点:

优点:

支持传参的改进:

缺点:

适用场景:

2.4 局部静态变量单例:“C++11 的魔法,一行代码搞定线程安全”

核心思路:

代码实现:

测试代码(多线程 + 带参数):

运行结果:

核心原理(C++11 标准):

优点:

缺点:

适用场景:

2.5 模板单例:“一次实现,所有类都能用”

核心思路:

代码实现:

测试代码:

运行结果:

核心技巧(CRTP 模式):

优点:

缺点:

带参模板单例扩展:

适用场景:

2.6 枚举单例:“极简中的极简,一行代码实现”

核心思路:

代码实现:

测试代码:

运行结果:

优点:

缺点:

适用场景:

三、避坑指南:单例模式的 6 个致命错误

错误 1:忘记禁用拷贝构造和赋值运算符

问题代码:

解决方案:

错误 2:线程安全实现错误(只加一次锁)

问题代码:

问题分析:

解决方案:

错误 3:内存泄漏(未处理析构)

问题代码:

问题分析:

解决方案:

错误 4:依赖单例的初始化顺序

问题代码:

问题分析:

解决方案:

错误 5:在析构函数中调用其他单例

问题代码:

问题分析:

解决方案:

错误 6:滥用单例,导致代码耦合

问题场景:

解决方案:

四、实战场景:单例模式在项目中的 3 个经典应用

4.1 场景 1:全局配置管理器

需求:

单例实现:

使用示例:

为什么用单例:

4.2 场景 2:日志器(Logger)

需求:

单例实现:

使用示例:

为什么用单例:

4.3 场景 3:数据库连接池

需求:

单例实现:

使用示例:

为什么用单例:

五、总结:单例模式的选择指南与核心价值

5.1 6 种实现方式对比与选择建议

5.2 单例模式的核心价值

5.3 最后一句忠告


class 卑微码农:
    def __init__(self):
        self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
        self.发量 = 100  # 初始发量
        self.咖啡因耐受度 = '极限'
        
    def 修Bug(self, bug):
        try:
            # 试图用玄学解决问题
            if bug.严重程度 == '离谱':
                print("这一定是环境问题!")
            else:
                print("让我看看是谁又没写注释...哦,是我自己。")
        except Exception as e:
            # 如果try块都救不了,那就...
            print("重启一下试试?")
            self.发量 -= 1  # 每解决一个bug,头发-1
 
 
# 实例化一个我
我 = 卑微码农()

引言:为什么你的全局对象总出问题?

作为 C++ 开发者,你一定写过这样的代码:

// 全局配置对象,到处用
Config g_config;

// 日志工具,全局调用
Logger g_logger;

int main() {
    g_config.load("config.ini"); // 初始化配置
    g_logger.init(g_config.getLogPath()); // 用配置初始化日志
    
    // 其他模块直接用g_config和g_logger
    UserModule user;
    user.setConfig(&g_config);
    user.setLogger(&g_logger);
    // ...
}

这段代码看似简单,却藏着 3 个致命问题:

  • 初始化顺序混乱:如果g_logger.init用到g_config,但g_config还没加载(全局对象初始化顺序不确定),直接崩溃;
  • 多线程灾难:如果多个线程同时操作g_config(比如一个读配置,一个更新配置),没有锁保护,数据会错乱;
  • 重复创建:如果其他文件也定义了Config实例,或者不小心new Config,就会出现多个配置对象,导致数据不一致。

这些问题的根源,在于全局对象缺乏 “唯一性” 和 “可控性”—— 你没法保证它只被创建一次,也没法控制它的初始化时机和访问顺序。

单例模式(Singleton Pattern),就是专门解决 “全局唯一对象” 问题的设计模式:它能确保一个类只有一个实例,提供全局访问点,并且由类自身控制实例的创建和生命周期。

本文会用 C++ 实战场景 + 可运行代码 + 真实踩坑经验,带你彻底吃透单例模式。从最简单的 “饿汉式” 到线程安全的 “双重检查锁定”,从常见错误到工业级实现,读完这篇,你再也不会为 “全局唯一对象” 头疼。

一、先搞懂:单例模式到底是什么?

1.1 核心思想:“一个类,只有一个对象,全局能用”

单例模式的核心是:保证一个类在整个程序生命周期中只有一个实例,并提供一个全局访问点,避免实例被重复创建

用一句大白话解释:单例就是 “全局唯一的对象”,但比普通全局变量更靠谱。比如:

  • 公司的 CEO:整个公司只有一个,所有人都能找到他(全局访问),且只能由公司成立时任命(自行控制创建);
  • 游戏中的玩家角色:单人游戏里只有一个主角,所有系统(任务、背包、战斗)都要和他交互,不能同时存在两个主角。

单例模式解决的不是 “全局访问”(普通全局变量也能做到),而是 **“唯一性” 和 “可控性”**—— 确保无论怎么调用,都只能得到同一个实例,并且初始化、销毁都有明确的时机。

1.2 单例模式的 3 个核心要素(缺一不可)

一个合格的单例必须满足 3 个条件:

  1. 唯一实例:类只能有一个实例,不能通过new、拷贝等方式创建多个;
  2. 自行创建:实例必须由类自身创建,而不是外部强制生成;
  3. 全局访问:提供一个全局访问点(通常是getInstance()方法),让其他模块能获取这个实例。

举个反例:如果一个类虽然提供了getInstance(),但没禁用拷贝构造,那么通过Singleton s = *Singleton::getInstance()就能复制出一个新对象,这就不是单例。

1.3 什么时候该用单例?3 个典型场景

单例不是万能的,滥用会导致代码耦合严重(所有模块都依赖单例)。但以下 3 种场景,单例是最优解:

  1. 全局资源管理:如配置管理器(整个程序只能有一套配置)、日志器(所有日志输出到同一个地方)、连接池(数据库连接池全局唯一,避免重复创建连接);
  2. 状态共享:如全局计数器(记录程序运行中的事件总数,必须唯一)、用户会话(整个程序只有一个当前登录用户);
  3. 重量级对象:创建成本高的对象(如大型缓存、硬件设备控制器),重复创建会浪费资源,必须保证唯一。

简单说:当你需要 “一个对象被全局共享,且不能有多个实例” 时,就该考虑单例。

二、C++ 单例模式 6 种实现方式:从简单到工业级

单例模式的实现方式很多,不同场景适合不同实现。下面从最简单的 “饿汉式” 开始,逐步讲解 6 种实现,每种都附完整代码和优缺点分析。

2.1 饿汉式单例:“程序一启动就创建,管你用不用”

核心思路:

程序启动时(main 函数前)就创建单例实例,不管后续是否使用。因为初始化早,像 “饿汉” 一样提前准备好,所以叫饿汉式。

代码实现:

// SingletonHungry.h
#ifndef SINGLETON_HUNGRY_H
#define SINGLETON_HUNGRY_H

#include <iostream>

// 饿汉式单例:程序启动时创建实例
class SingletonHungry {
public:
    // 禁用拷贝构造和赋值运算符(关键!避免复制出多个实例)
    SingletonHungry(const SingletonHungry&) = delete;
    SingletonHungry& operator=(const SingletonHungry&) = delete;

    // 全局访问点
    static SingletonHungry& getInstance() {
        return instance; // 直接返回已创建的实例
    }

    // 单例的业务方法(示例)
    void doSomething() {
        std::cout << "饿汉式单例:执行操作,实例地址=" << this << std::endl;
    }

private:
    // 私有构造函数(禁止外部new)
    SingletonHungry() {
        std::cout << "饿汉式单例:构造函数被调用" << std::endl;
    }

    // 私有析构函数(可选,控制销毁逻辑)
    ~SingletonHungry() {
        std::cout << "饿汉式单例:析构函数被调用" << std::endl;
    }

    // 静态成员变量:存储唯一实例
    static SingletonHungry instance;
};

// 初始化静态成员(程序启动时执行,早于main函数)
SingletonHungry SingletonHungry::instance;

#endif // SINGLETON_HUNGRY_H

测试代码:

// main.cpp
#include "SingletonHungry.h"
#include <thread>
#include <chrono>

void testHungry() {
    // 多个线程获取实例,验证是否唯一
    for (int i = 0; i < 3; ++i) {
        std::thread t([](){
            SingletonHungry::getInstance().doSomething();
        });
        t.join();
    }
}

int main() {
    std::cout << "main函数开始执行" << std::endl;
    testHungry();
    return 0;
}

运行结果:

饿汉式单例:构造函数被调用  // 注意:这行在main函数前输出!
main函数开始执行
饿汉式单例:执行操作,实例地址=0x55f8a5c6c120
饿汉式单例:执行操作,实例地址=0x55f8a5c6c120
饿汉式单例:执行操作,实例地址=0x55f8a5c6c120
饿汉式单例:析构函数被调用  // 程序退出时执行

优点:

  • 实现简单:几行代码就能搞定,无需复杂的线程同步;
  • 线程安全:全局静态变量在程序启动时初始化,此时没有多线程问题,天然线程安全;
  • 访问高效getInstance()直接返回实例,无需判断和锁,性能开销小。

缺点:

  • 初始化过早:程序启动时就创建实例,即使整个程序都没用到,也会占用内存(比如一个很少用到的日志器,启动就占内存);
  • 无法传参:构造函数不能带参数(因为静态成员初始化时无法传递参数),如果单例需要配置信息(如日志路径),就无法满足;
  • 初始化顺序问题:多个饿汉式单例之间的初始化顺序不确定(全局变量初始化顺序是未定义的),如果 A 单例的构造依赖 B 单例,可能导致崩溃。

适用场景:

  • 单例实例体积小,创建成本低;
  • 不需要参数初始化;
  • 程序启动后肯定会用到(如核心配置管理器)。

2.2 懒汉式单例(基础版):“用到的时候再创建,省内存”

核心思路:

实例在第一次调用getInstance()时才创建,而不是程序启动时。像 “懒汉” 一样,不到万不得已不干活,所以叫懒汉式。

代码实现:

// SingletonLazyBasic.h
#ifndef SINGLETON_LAZY_BASIC_H
#define SINGLETON_LAZY_BASIC_H

#include <iostream>

// 懒汉式单例(基础版,非线程安全)
class SingletonLazyBasic {
public:
    // 禁用拷贝和赋值
    SingletonLazyBasic(const SingletonLazyBasic&) = delete;
    SingletonLazyBasic& operator=(const SingletonLazyBasic&) = delete;

    // 全局访问点:第一次调用时创建实例
    static SingletonLazyBasic* getInstance() {
        if (instance == nullptr) { // 没创建过,就new一个
            instance = new SingletonLazyBasic();
        }
        return instance;
    }

    // 业务方法
    void doSomething() {
        std::cout << "基础懒汉式单例:执行操作,实例地址=" << this << std::endl;
    }

private:
    // 私有构造
    SingletonLazyBasic() {
        std::cout << "基础懒汉式单例:构造函数被调用" << std::endl;
    }

    // 注意:基础版通常不处理析构,可能导致内存泄漏(后面会解决)
    ~SingletonLazyBasic() {
        std::cout << "基础懒汉式单例:析构函数被调用" << std::endl;
    }

    // 静态指针:存储实例(初始为nullptr)
    static SingletonLazyBasic* instance;
};

// 初始化静态指针为nullptr
SingletonLazyBasic* SingletonLazyBasic::instance = nullptr;

#endif // SINGLETON_LAZY_BASIC_H

测试代码(单线程):

#include "SingletonLazyBasic.h"

void testLazyBasicSingleThread() {
    std::cout << "第一次获取实例:" << std::endl;
    auto s1 = SingletonLazyBasic::getInstance();
    s1->doSomething();

    std::cout << "第二次获取实例:" << std::endl;
    auto s2 = SingletonLazyBasic::getInstance();
    s2->doSomething();

    // 验证是否为同一实例
    std::cout << "s1和s2是否相同?" << (s1 == s2 ? "是" : "否") << std::endl;
}

int main() {
    std::cout << "main函数开始执行" << std::endl;
    testLazyBasicSingleThread();
    return 0;
}

单线程运行结果:

main函数开始执行
第一次获取实例:
基础懒汉式单例:构造函数被调用
基础懒汉式单例:执行操作,实例地址=0x55d9b6f83e70
第二次获取实例:
基础懒汉式单例:执行操作,实例地址=0x55d9b6f83e70
s1和s2是否相同?是

问题暴露(多线程测试):

void testLazyBasicMultiThread() {
    // 多个线程同时调用getInstance()
    std::thread t1([](){
        auto s = SingletonLazyBasic::getInstance();
        s->doSomething();
    });
    std::thread t2([](){
        auto s = SingletonLazyBasic::getInstance();
        s->doSomething();
    });
    t1.join();
    t2.join();
}

int main() {
    testLazyBasicMultiThread();
    return 0;
}

多线程可能的运行结果(崩溃或多实例):

基础懒汉式单例:构造函数被调用  // 线程1进入,创建实例
基础懒汉式单例:构造函数被调用  // 线程2同时进入,也创建实例(问题!)
基础懒汉式单例:执行操作,实例地址=0x55d9b6f83e70
基础懒汉式单例:执行操作,实例地址=0x55d9b6f842a0  // 地址不同,说明多个实例

优点:

  • 延迟初始化:用到时才创建,节省内存(适合大型对象或不常用的单例);
  • 支持传参:可以在getInstance()中传递参数给构造函数(后面优化版会实现)。

缺点:

  • 线程不安全:多线程同时调用getInstance()时,可能创建多个实例(如上面的测试);
  • 内存泄漏风险new出来的实例如果不手动delete,程序退出时不会调用析构函数(基础版没解决)。

适用场景:

  • 单线程环境;
  • 实例创建成本高,且不一定会用到。

2.3 懒汉式单例(线程安全版):“加锁防并发,安全第一”

核心思路:

在基础版懒汉式的基础上,给getInstance()加互斥锁(std::mutex),确保同一时间只有一个线程能执行实例创建逻辑,解决多线程安全问题。

代码实现:

// SingletonLazySafe.h
#ifndef SINGLETON_LAZY_SAFE_H
#define SINGLETON_LAZY_SAFE_H

#include <iostream>
#include <mutex> // 互斥锁
#include <memory> // 智能指针(解决内存泄漏)

// 懒汉式单例(线程安全版)
class SingletonLazySafe {
public:
    // 禁用拷贝和赋值
    SingletonLazySafe(const SingletonLazySafe&) = delete;
    SingletonLazySafe& operator=(const SingletonLazySafe&) = delete;

    // 全局访问点:加锁确保线程安全
    static std::shared_ptr<SingletonLazySafe> getInstance() {
        // 双重检查锁定(后面会讲为什么双重检查)
        if (instance == nullptr) { 
            std::lock_guard<std::mutex> lock(mtx); // 加锁
            if (instance == nullptr) {
                // 用智能指针管理,避免内存泄漏
                instance = std::shared_ptr<SingletonLazySafe>(new SingletonLazySafe());
            }
        }
        return instance;
    }

    // 业务方法
    void doSomething() {
        std::cout << "线程安全懒汉式单例:执行操作,实例地址=" << this << std::endl;
    }

private:
    // 私有构造(支持传参,这里简化)
    SingletonLazySafe() {
        std::cout << "线程安全懒汉式单例:构造函数被调用" << std::endl;
    }

    ~SingletonLazySafe() {
        std::cout << "线程安全懒汉式单例:析构函数被调用" << std::endl;
    }

    // 静态智能指针:存储实例
    static std::shared_ptr<SingletonLazySafe> instance;
    // 静态互斥锁:确保线程安全
    static std::mutex mtx;
};

// 初始化静态成员
std::shared_ptr<SingletonLazySafe> SingletonLazySafe::instance = nullptr;
std::mutex SingletonLazySafe::mtx;

#endif // SINGLETON_LAZY_SAFE_H

测试代码(多线程):

#include "SingletonLazySafe.h"
#include <thread>

void testLazySafeMultiThread() {
    // 5个线程同时获取实例
    std::thread ts[5];
    for (int i = 0; i < 5; ++i) {
        ts[i] = std::thread([](){
            auto s = SingletonLazySafe::getInstance();
            s->doSomething();
        });
    }
    for (auto& t : ts) {
        t.join();
    }
}

int main() {
    testLazySafeMultiThread();
    return 0;
}

运行结果:

线程安全懒汉式单例:构造函数被调用  // 只调用一次
线程安全懒汉式单例:执行操作,实例地址=0x55f1b6a5e2a0
线程安全懒汉式单例:执行操作,实例地址=0x55f1b6a5e2a0
线程安全懒汉式单例:执行操作,实例地址=0x55f1b6a5e2a0
线程安全懒汉式单例:执行操作,实例地址=0x55f1b6a5e2a0
线程安全懒汉式单例:执行操作,实例地址=0x55f1b6a5e2a0
线程安全懒汉式单例:析构函数被调用  // 智能指针自动释放

关键优化点:

  1. 互斥锁(std::mutex:确保同一时间只有一个线程进入实例创建逻辑,解决多线程并发问题;
  2. 智能指针(std::shared_ptr:自动管理实例生命周期,程序退出时调用析构函数,避免内存泄漏;
  3. 双重检查锁定:先判断instance == nullptr,再加锁,加锁后再判断一次 —— 避免每次调用getInstance()都加锁(加锁是有性能开销的),提高效率。

优点:

  • 线程安全:多线程环境下不会创建多个实例;
  • 延迟初始化:用到时才创建,节省内存;
  • 无内存泄漏:智能指针自动释放资源;
  • 支持传参:可以在getInstance()中传递参数(如下)。

支持传参的改进:

// 带参数的getInstance()
static std::shared_ptr<SingletonLazySafe> getInstance(const std::string& config) {
    if (instance == nullptr) {
        std::lock_guard<std::mutex> lock(mtx);
        if (instance == nullptr) {
            instance = std::shared_ptr<SingletonLazySafe>(new SingletonLazySafe(config));
        }
    }
    return instance;
}

// 带参数的构造函数
private:
    explicit SingletonLazySafe(const std::string& config) {
        std::cout << "初始化单例,配置:" << config << std::endl;
    }

缺点:

  • 实现稍复杂:需要处理锁和智能指针;
  • 性能开销:虽然双重检查减少了加锁次数,但首次调用仍有锁的开销(不过对大多数场景影响不大)。

适用场景:

  • 多线程环境;
  • 需要延迟初始化或带参数的单例(如日志器需要日志路径参数)。

2.4 局部静态变量单例:“C++11 的魔法,一行代码搞定线程安全”

核心思路:

利用 C++11 的特性:局部静态变量的初始化是线程安全的。在getInstance()中定义一个局部静态变量,第一次调用时初始化,后续调用直接返回,既保证唯一,又天然线程安全。

代码实现:

// SingletonLocalStatic.h
#ifndef SINGLETON_LOCAL_STATIC_H
#define SINGLETON_LOCAL_STATIC_H

#include <iostream>

// 局部静态变量单例(C++11及以上)
class SingletonLocalStatic {
public:
    // 禁用拷贝和赋值
    SingletonLocalStatic(const SingletonLocalStatic&) = delete;
    SingletonLocalStatic& operator=(const SingletonLocalStatic&) = delete;

    // 全局访问点:局部静态变量自动保证唯一和线程安全
    static SingletonLocalStatic& getInstance() {
        static SingletonLocalStatic instance; // 局部静态变量,第一次调用时初始化
        return instance;
    }

    // 带参数的版本(C++11支持)
    static SingletonLocalStatic& getInstance(const std::string& config) {
        static SingletonLocalStatic instance(config); // 支持传参
        return instance;
    }

    // 业务方法
    void doSomething() {
        std::cout << "局部静态单例:执行操作,实例地址=" << this << std::endl;
    }

private:
    // 无参构造
    SingletonLocalStatic() {
        std::cout << "局部静态单例:无参构造被调用" << std::endl;
    }

    // 带参构造
    explicit SingletonLocalStatic(const std::string& config) {
        std::cout << "局部静态单例:带参构造被调用,配置=" << config << std::endl;
    }

    ~SingletonLocalStatic() {
        std::cout << "局部静态单例:析构函数被调用" << std::endl;
    }
};

#endif // SINGLETON_LOCAL_STATIC_H

测试代码(多线程 + 带参数):

#include "SingletonLocalStatic.h"
#include <thread>

void testLocalStaticMultiThread() {
    // 多线程调用带参数的getInstance()
    std::thread t1([](){
        auto& s = SingletonLocalStatic::getInstance("log_path=/var/log");
        s.doSomething();
    });
    std::thread t2([](){
        auto& s = SingletonLocalStatic::getInstance("log_path=/var/log"); // 传相同参数
        s.doSomething();
    });
    t1.join();
    t2.join();
}

int main() {
    testLocalStaticMultiThread();
    return 0;
}

运行结果:

局部静态单例:带参构造被调用,配置=log_path=/var/log  // 只初始化一次
局部静态单例:执行操作,实例地址=0x55e8d6f5c140
局部静态单例:执行操作,实例地址=0x55e8d6f5c140
局部静态单例:析构函数被调用  // 程序退出时自动调用

核心原理(C++11 标准):

C++11 规定,如果多个线程同时初始化同一个局部静态变量,初始化会被保证只执行一次(编译器会自动加入线程同步逻辑)。这意味着:

  • 不需要手动加锁,编译器帮你搞定线程安全;
  • 局部静态变量在第一次调用时初始化(延迟初始化);
  • 程序退出时,局部静态变量会自动析构(无内存泄漏)。

优点:

  • 实现极简:几行代码搞定,比加锁的懒汉式简单;
  • 线程安全:C++11 及以上标准保证初始化安全;
  • 延迟初始化:用到时才创建;
  • 无内存泄漏:自动析构;
  • 支持传参:可以定义带参数的getInstance()(注意参数必须一致,否则多次调用可能出问题)。

缺点:

  • 依赖 C++11 及以上标准:老编译器(如 VS2010 以前)可能不支持;
  • 参数一致性问题:如果多次调用getInstance()传不同参数,只有第一次的参数有效(后续调用会忽略参数),可能导致误解。

适用场景:

  • C++11 及以上环境;
  • 希望代码简洁,同时需要线程安全和延迟初始化的场景(推荐首选)。

2.5 模板单例:“一次实现,所有类都能用”

核心思路:

将单例逻辑封装成模板类,其他类只需继承该模板,就能自动成为单例。避免重复编写单例代码,提高复用性。

代码实现:

// SingletonTemplate.h
#ifndef SINGLETON_TEMPLATE_H
#define SINGLETON_TEMPLATE_H

#include <iostream>

// 模板单例基类(CRTP:奇异递归模板模式)
template <typename T>
class SingletonTemplate {
public:
    // 禁用拷贝和赋值
    SingletonTemplate(const SingletonTemplate&) = delete;
    SingletonTemplate& operator=(const SingletonTemplate&) = delete;

    // 全局访问点:返回子类的实例
    static T& getInstance() {
        static T instance; // 局部静态变量,线程安全(C++11)
        return instance;
    }

protected:
    // 保护构造和析构,允许子类继承
    SingletonTemplate() = default;
    ~SingletonTemplate() = default;
};

// ---------------------- 示例:如何使用模板单例 ----------------------
// 1. 定义需要成为单例的类,继承SingletonTemplate,并传入自身类型
class Logger : public SingletonTemplate<Logger> {
    // 必须声明模板类为友元,否则模板无法访问Logger的私有构造
    friend class SingletonTemplate<Logger>;
public:
    void log(const std::string& msg) {
        std::cout << "[日志] " << msg << std::endl;
    }

private:
    // 私有构造,确保只能通过模板创建
    Logger() {
        std::cout << "Logger单例初始化" << std::endl;
    }
};

// 2. 另一个单例类:配置管理器
class ConfigManager : public SingletonTemplate<ConfigManager> {
    friend class SingletonTemplate<ConfigManager>;
public:
    std::string getConfig(const std::string& key) {
        // 模拟获取配置
        return "配置值:" + key;
    }

private:
    ConfigManager() {
        std::cout << "ConfigManager单例初始化" << std::endl;
    }
};

#endif // SINGLETON_TEMPLATE_H

测试代码:

#include "SingletonTemplate.h"

void testTemplateSingleton() {
    // 使用Logger单例
    Logger::getInstance().log("程序启动");

    // 使用ConfigManager单例
    std::cout << ConfigManager::getInstance().getConfig("log_level") << std::endl;

    // 验证唯一性
    Logger& log1 = Logger::getInstance();
    Logger& log2 = Logger::getInstance();
    std::cout << "Logger实例是否唯一?" << (&log1 == &log2 ? "是" : "否") << std::endl;
}

int main() {
    testTemplateSingleton();
    return 0;
}

运行结果:

Logger单例初始化
[日志] 程序启动
ConfigManager单例初始化
配置值:log_level
Logger实例是否唯一?是

核心技巧(CRTP 模式):

模板单例使用了奇异递归模板模式(CRTP)class T : public SingletonTemplate<T>,即子类继承模板类时,将自身作为模板参数传入。这样模板类就能通过T访问子类的构造函数(需声明友元)。

优点:

  • 代码复用:一次实现模板,所有类都能快速变成单例,避免重复劳动;
  • 自动继承特性:子类自动获得getInstance()、线程安全、延迟初始化等特性;
  • 灵活性高:每个子类都是独立的单例,互不影响(Logger 和 ConfigManager 是不同的单例)。

缺点:

  • 理解门槛:CRTP 模式对新手来说稍难理解;
  • 构造函数限制:子类必须将模板类声明为友元,且构造函数必须私有;
  • 不支持带参构造:模板的getInstance()是固定的,如需传参,需要额外扩展(如下)。

带参模板单例扩展:

// 带参数的模板单例
template <typename T>
class SingletonTemplateWithArgs {
public:
    template <typename... Args>
    static T& getInstance(Args&&... args) {
        static T instance(std::forward<Args>(args)...); // 完美转发参数
        return instance;
    }

    // 禁用拷贝和赋值(同上)
    // ...
};

// 使用示例
class LoggerWithArgs : public SingletonTemplateWithArgs<LoggerWithArgs> {
    friend class SingletonTemplateWithArgs<LoggerWithArgs>;
public:
    void log(const std::string& msg) { /* ... */ }
private:
    explicit LoggerWithArgs(const std::string& path) {
        std::cout << "Logger初始化,路径:" << path << std::endl;
    }
};

// 调用
LoggerWithArgs::getInstance("/var/log/app.log").log("test");

适用场景:

  • 项目中需要多个单例类(如日志、配置、连接池);
  • 希望减少重复代码,提高开发效率。

2.6 枚举单例:“极简中的极简,一行代码实现”

核心思路:

利用 C++ 枚举的特性:枚举常量是全局唯一的。通过定义包含一个元素的枚举,实现最简单的单例(适合无状态单例)。

代码实现:

// SingletonEnum.h
#ifndef SINGLETON_ENUM_H
#define SINGLETON_ENUM_H

#include <iostream>

// 枚举单例:适合无状态工具类
enum class UtilitySingleton {
    INSTANCE // 唯一实例
};

// 为枚举添加方法(C++中枚举本身不能有方法,需用命名空间包装)
namespace Utility {
    // 工具类的业务方法
    void doTask() {
        std::cout << "枚举单例:执行工具任务" << std::endl;
    }

    // 获取单例(其实就是返回枚举常量)
    UtilitySingleton getInstance() {
        return UtilitySingleton::INSTANCE;
    }
}

#endif // SINGLETON_ENUM_H

测试代码:

#include "SingletonEnum.h"

void testEnumSingleton() {
    auto s1 = Utility::getInstance();
    auto s2 = Utility::getInstance();
    // 枚举常量比较,验证唯一
    std::cout << "枚举实例是否唯一?" << (s1 == s2 ? "是" : "否") << std::endl;
    Utility::doTask();
}

int main() {
    testEnumSingleton();
    return 0;
}

运行结果:

枚举实例是否唯一?是
枚举单例:执行工具任务

优点:

  • 极致简单:几行代码搞定,几乎不可能出错;
  • 天然唯一:枚举常量全局唯一,编译期保证;
  • 无内存问题:枚举不涉及动态内存,没有泄漏风险。

缺点:

  • 无状态限制:枚举本身不能有成员变量和方法,只能通过命名空间包装函数,适合纯工具类(无状态);
  • 不支持初始化逻辑:无法在构造时执行初始化(如加载配置)。

适用场景:

  • 无状态的全局工具类(如字符串工具、数学工具);
  • 不需要初始化逻辑,只需保证全局唯一访问点。

三、避坑指南:单例模式的 6 个致命错误

单例模式看似简单,但实际开发中 90% 的手写单例都有隐藏问题。下面列出 6 个最常见的错误及解决方案。

错误 1:忘记禁用拷贝构造和赋值运算符

问题代码:

class BadSingleton {
public:
    static BadSingleton& getInstance() {
        static BadSingleton instance;
        return instance;
    }

private:
    BadSingleton() = default;
    // 忘记禁用拷贝和赋值!
    // ~BadSingleton() = default;
};

// 测试:可以复制出多个实例
void testBadSingleton() {
    BadSingleton& s1 = BadSingleton::getInstance();
    BadSingleton s2 = s1; // 调用默认拷贝构造,创建新实例(问题!)
    std::cout << "s1地址:" << &s1 << ",s2地址:" << &s2 << std::endl; // 地址不同
}

解决方案:

显式禁用拷贝构造和赋值运算符(C++11 及以上):

class GoodSingleton {
public:
    GoodSingleton(const GoodSingleton&) = delete; // 禁用拷贝构造
    GoodSingleton& operator=(const GoodSingleton&) = delete; // 禁用赋值
    // ... 其他代码
};

错误 2:线程安全实现错误(只加一次锁)

问题代码:

class UnsafeSingleton {
public:
    static UnsafeSingleton* getInstance() {
        std::lock_guard<std::mutex> lock(mtx); // 只加一次锁(错误!)
        if (instance == nullptr) {
            instance = new UnsafeSingleton();
        }
        return instance;
    }
    // ...
};

问题分析:

每次调用getInstance()都加锁,即使实例已经创建,会导致严重的性能开销(锁的获取和释放是有成本的)。

解决方案:

双重检查锁定:先判断实例是否存在,不存在再加锁,加锁后再判断一次:

static UnsafeSingleton* getInstance() {
    if (instance == nullptr) { // 第一次检查:避免每次加锁
        std::lock_guard<std::mutex> lock(mtx);
        if (instance == nullptr) { // 第二次检查:防止多线程并发创建
            instance = new UnsafeSingleton();
        }
    }
    return instance;
}

错误 3:内存泄漏(未处理析构)

问题代码:

class LeakSingleton {
public:
    static LeakSingleton* getInstance() {
        if (instance == nullptr) {
            instance = new LeakSingleton(); // new出来的实例没delete
        }
        return instance;
    }
    // ...
private:
    static LeakSingleton* instance;
};

问题分析:

new创建的实例如果不手动delete,程序退出时不会调用析构函数,导致内存泄漏(虽然程序退出后系统会回收内存,但析构中的清理逻辑(如关闭文件、释放连接)不会执行)。

解决方案:

  • 用智能指针(std::shared_ptr)管理实例;
  • 或提供手动销毁方法(不推荐,容易忘记调用);
  • 或用局部静态变量(自动析构)。

错误 4:依赖单例的初始化顺序

问题代码:

// A单例的构造依赖B单例
class SingletonA {
public:
    static SingletonA& getInstance() {
        static SingletonA instance;
        return instance;
    }
private:
    SingletonA() {
        // B单例还没初始化,调用其方法会崩溃!
        SingletonB::getInstance().doSomething(); 
    }
};

class SingletonB {
    // ... 类似实现
};

// 全局变量初始化顺序不确定,A可能先于B初始化
SingletonA a;
SingletonB b;

问题分析:

多个单例之间如果有依赖关系,且初始化顺序不确定(如饿汉式、全局变量),可能导致 “先初始化的单例调用未初始化的单例”,直接崩溃。

解决方案:

  • 用懒汉式(局部静态变量),确保依赖的单例在被调用时才初始化;
  • 明确初始化顺序:在main函数中手动控制单例的初始化顺序。

错误 5:在析构函数中调用其他单例

问题代码:

class SingletonC {
public:
    ~SingletonC() {
        // 析构时调用SingletonD,可能D已经被销毁!
        SingletonD::getInstance().cleanup(); 
    }
    // ...
};

问题分析:

程序退出时,单例的析构顺序是不确定的。如果SingletonC的析构调用了SingletonD,而SingletonD已经析构,会导致未定义行为(崩溃)。

解决方案:

  • 避免在单例析构函数中依赖其他单例;
  • 用 “单例管理器” 统一控制析构顺序(复杂,不推荐)。

错误 6:滥用单例,导致代码耦合

问题场景:

项目中到处都是Singleton::getInstance().xxx(),所有模块都直接依赖单例,导致:

  • 单元测试困难(无法 Mock 单例);
  • 代码耦合严重,修改单例会影响所有依赖模块;
  • 无法扩展(如需要多个实例时,只能重写大量代码)。

解决方案:

  • 优先考虑 “依赖注入”:将单例作为参数传递给需要的模块,而非直接在模块中调用getInstance()
  • 限制单例数量:只对真正需要全局唯一的对象使用单例(如配置、日志),其他场景用普通类。

四、实战场景:单例模式在项目中的 3 个经典应用

单例模式不是 “炫技工具”,而是解决实际问题的方案。下面通过 3 个真实项目场景,展示单例的价值。

4.1 场景 1:全局配置管理器

需求:

程序启动时加载配置文件(config.ini),所有模块都需要读取配置(如数据库地址、日志级别),且配置只能加载一次。

单例实现:

// ConfigManager.h
#ifndef CONFIG_MANAGER_H
#define CONFIG_MANAGER_H

#include <string>
#include <unordered_map>
#include <fstream>
#include <sstream>

// 配置管理器:单例模式
class ConfigManager : public SingletonTemplate<ConfigManager> {
    friend class SingletonTemplate<ConfigManager>;
public:
    // 加载配置文件(只允许调用一次)
    bool load(const std::string& filePath) {
        if (isLoaded_) {
            std::cout << "配置已加载,无需重复加载" << std::endl;
            return true;
        }

        std::ifstream file(filePath);
        if (!file.is_open()) {
            std::cout << "配置文件打开失败:" << filePath << std::endl;
            return false;
        }

        std::string line;
        while (std::getline(file, line)) {
            // 解析键值对(简化:key=value)
            size_t pos = line.find('=');
            if (pos == std::string::npos) continue;
            std::string key = line.substr(0, pos);
            std::string value = line.substr(pos + 1);
            configs_[key] = value;
        }

        isLoaded_ = true;
        std::cout << "配置加载成功,共" << configs_.size() << "项" << std::endl;
        return true;
    }

    // 获取配置值
    std::string get(const std::string& key, const std::string& defaultValue = "") const {
        auto it = configs_.find(key);
        return it != configs_.end() ? it->second : defaultValue;
    }

private:
    ConfigManager() = default; // 私有构造

    std::unordered_map<std::string, std::string> configs_; // 存储配置
    bool isLoaded_ = false; // 是否已加载
};

#endif // CONFIG_MANAGER_H

使用示例:

#include "ConfigManager.h"
#include "SingletonTemplate.h"

int main() {
    // 加载配置(全局只加载一次)
    ConfigManager::getInstance().load("config.ini");

    // 其他模块读取配置
    std::string dbHost = ConfigManager::getInstance().get("db_host", "127.0.0.1");
    std::string logLevel = ConfigManager::getInstance().get("log_level", "info");

    std::cout << "数据库地址:" << dbHost << std::endl;
    std::cout << "日志级别:" << logLevel << std::endl;

    return 0;
}

为什么用单例:

  • 配置全局唯一,不能加载多次(否则可能读取到不同配置);
  • 所有模块都需要访问,单例提供统一访问点;
  • 加载成本高(读文件、解析),单例确保只加载一次。

4.2 场景 2:日志器(Logger)

需求:

所有模块的日志都输出到同一个文件,且日志器需要先初始化(指定日志路径),之后才能使用。

单例实现:

// Logger.h
#ifndef LOGGER_H
#define LOGGER_H

#include <fstream>
#include <string>
#include <mutex>
#include <chrono>
#include <iomanip>

// 日志器:单例模式(线程安全)
class Logger : public SingletonTemplate<Logger> {
    friend class SingletonTemplate<Logger>;
public:
    // 初始化日志器(指定日志文件路径)
    bool init(const std::string& logPath) {
        if (isInited_) {
            return true;
        }
        logFile_.open(logPath, std::ios::app);
        if (!logFile_.is_open()) {
            return false;
        }
        isInited_ = true;
        return true;
    }

    // 输出日志(线程安全)
    void log(const std::string& msg) {
        if (!isInited_) {
            return; // 未初始化,不输出
        }
        std::lock_guard<std::mutex> lock(mtx_); // 加锁确保线程安全

        // 获取当前时间
        auto now = std::chrono::system_clock::now();
        auto time = std::chrono::system_clock::to_time_t(now);
        logFile_ << "[" << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S") << "] " << msg << std::endl;
    }

private:
    Logger() = default;
    ~Logger() {
        if (logFile_.is_open()) {
            logFile_.close();
        }
    }

    std::ofstream logFile_; // 日志文件
    bool isInited_ = false; // 是否初始化
    std::mutex mtx_; // 线程安全锁
};

#endif // LOGGER_H

使用示例:

#include "Logger.h"
#include "ConfigManager.h"
#include <thread>

void testLogger() {
    // 从配置中获取日志路径,初始化日志器
    std::string logPath = ConfigManager::getInstance().get("log_path", "app.log");
    Logger::getInstance().init(logPath);

    // 多线程输出日志
    std::thread t1([](){
        Logger::getInstance().log("线程1:用户登录");
    });
    std::thread t2([](){
        Logger::getInstance().log("线程2:订单创建");
    });
    t1.join();
    t2.join();
}

int main() {
    ConfigManager::getInstance().load("config.ini");
    testLogger();
    return 0;
}

为什么用单例:

  • 日志文件只能被一个日志器打开(多实例会导致日志错乱);
  • 所有模块共享同一个日志输出点,确保日志格式统一;
  • 初始化逻辑(打开文件)只执行一次,避免重复操作。

4.3 场景 3:数据库连接池

需求:

管理数据库连接,连接池全局唯一,所有模块从池中获取连接,避免频繁创建和关闭连接(成本高)。

单例实现:

// DbConnectionPool.h
#ifndef DB_CONNECTION_POOL_H
#define DB_CONNECTION_POOL_H

#include <queue>
#include <mutex>
#include <memory>
#include <string>
#include <iostream>

// 数据库连接(简化)
class DbConnection {
public:
    bool connect(const std::string& host, int port, const std::string& user, const std::string& pwd) {
        // 实际连接逻辑(简化)
        std::cout << "连接数据库:" << host << ":" << port << std::endl;
        return true;
    }

    void executeSql(const std::string& sql) {
        std::cout << "执行SQL:" << sql << std::endl;
    }
};

// 数据库连接池:单例模式
class DbConnectionPool : public SingletonTemplate<DbConnectionPool> {
    friend class SingletonTemplate<DbConnectionPool>;
public:
    // 初始化连接池
    bool init(const std::string& host, int port, const std::string& user, const std::string& pwd, int maxSize) {
        if (isInited_) return true;

        // 创建maxSize个连接
        for (int i = 0; i < maxSize; ++i) {
            std::unique_ptr<DbConnection> conn(new DbConnection());
            if (conn->connect(host, port, user, pwd)) {
                pool_.push(std::move(conn));
            }
        }

        if (pool_.empty()) {
            return false;
        }

        maxSize_ = maxSize;
        isInited_ = true;
        std::cout << "连接池初始化完成,连接数:" << pool_.size() << std::endl;
        return true;
    }

    // 获取连接(从池中取)
    std::unique_ptr<DbConnection, void(*)(DbConnection*)> getConnection() {
        std::lock_guard<std::mutex> lock(mtx_);
        if (pool_.empty()) {
            return {nullptr, [this](DbConnection* conn){}}; // 无可用连接
        }

        // 从队列取出连接,并用自定义删除器(归还连接到池)
        auto conn = std::move(pool_.front());
        pool_.pop();
        return {conn.release(), [this](DbConnection* conn){
            std::lock_guard<std::mutex> lock(mtx_);
            pool_.push(std::unique_ptr<DbConnection>(conn));
        }};
    }

private:
    DbConnectionPool() = default;

    std::queue<std::unique_ptr<DbConnection>> pool_; // 连接池
    int maxSize_ = 0; // 最大连接数
    bool isInited_ = false; // 是否初始化
    std::mutex mtx_; // 线程安全锁
};

#endif // DB_CONNECTION_POOL_H

使用示例:

#include "DbConnectionPool.h"
#include "ConfigManager.h"

int main() {
    ConfigManager::getInstance().load("config.ini");

    // 从配置获取数据库信息,初始化连接池
    std::string dbHost = ConfigManager::getInstance().get("db_host");
    int dbPort = std::stoi(ConfigManager::getInstance().get("db_port", "3306"));
    std::string dbUser = ConfigManager::getInstance().get("db_user");
    std::string dbPwd = ConfigManager::getInstance().get("db_pwd");
    DbConnectionPool::getInstance().init(dbHost, dbPort, dbUser, dbPwd, 5);

    // 获取连接并使用
    auto conn = DbConnectionPool::getInstance().getConnection();
    if (conn) {
        conn->executeSql("SELECT * FROM users");
    }

    // conn超出作用域时,自动归还到连接池(自定义删除器生效)
    return 0;
}

为什么用单例:

  • 连接池全局唯一,避免多个池导致的资源浪费;
  • 所有模块共享连接,确保连接被高效复用;
  • 连接池的初始化和管理逻辑集中,便于维护。

五、总结:单例模式的选择指南与核心价值

5.1 6 种实现方式对比与选择建议

实现方式线程安全(C++11)延迟初始化支持传参实现复杂度适用场景
饿汉式简单单例体积小,启动后必用,无参数需求
基础懒汉式简单单线程环境
线程安全懒汉式中等多线程,需要参数,C++03 环境
局部静态变量极简C++11 及以上,希望代码简洁(推荐首选)
模板单例是(依赖局部静态)中等多个单例类,希望复用代码
枚举单例极简无状态工具类,不需要初始化

选择优先级:局部静态变量单例(C++11+) > 线程安全懒汉式(C++03) > 模板单例(多单例场景) > 饿汉式(简单无参) > 枚举单例(无状态) > 基础懒汉式(仅单线程)。

5.2 单例模式的核心价值

单例模式的本质不是 “全局访问”,而是 **“控制实例唯一性”**。它解决的是 “如何确保一个类在任何情况下都只有一个实例,并统一管理其生命周期” 的问题。

在实际开发中,单例的价值体现在:

  • 资源保护:避免全局资源(如配置、日志文件)被多个实例竞争导致的错乱;
  • 性能优化:减少重量级对象(如连接池、大型缓存)的重复创建成本;
  • 逻辑清晰:通过getInstance()明确标识 “全局唯一对象”,比匿名全局变量更易维护。

5.3 最后一句忠告

单例模式是 “双刃剑”:用得好能解决全局资源管理问题,用得不好会导致代码耦合、测试困难。记住:不要为了 “全局访问” 而用单例,只有当 “唯一性” 是核心需求时,才考虑单例

如果你的项目中到处都是单例,可能需要反思:这些对象真的需要全局唯一吗?有没有更合适的设计(如依赖注入)?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值