C++单例模式

前言

单例模式(Singleton Pattern)是 C++ 中最常用的设计模式之一,它保证一个类仅有一个实例,并提供一个全局访问点。在开发中,日志系统、配置管理器、连接池等场景都非常适合使用单例模式。本文将深入探讨单例模式的设计思想、多种实现方式及各自的优缺点。

一、单例模式的核心要素

一个规范的单例模式实现需要满足以下两个核心条件:

  1. 唯一实例:类只能有一个实例
  2. 全局访问:提供一个全局访问点获取该实例

为了实现这两个条件,单例模式通常会:

  • 将构造函数私有化,防止外部直接创建对象
  • 提供一个静态成员函数作为全局访问点
  • 在类内部维护唯一的实例对象

二、单例模式的实现方式

1. 饿汉式(Eager Initialization)

饿汉式是最简单的实现方式,在程序启动时就创建实例:

cpp 复制代码
class Singleton {
private:
    // 私有构造函数
    Singleton() {}
    
    // 禁用拷贝构造和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    // 静态实例
    static Singleton instance;

public:
    // 全局访问点
    static Singleton& getInstance() {
        return instance;
    }
};

// 在类外初始化静态成员
Singleton Singleton::instance;

优点

  • 实现简单,线程安全(C++11 后静态对象初始化是线程安全的)
  • 没有动态分配的开销

缺点

  • 无论是否使用都会创建实例,可能造成资源浪费
  • 无法处理依赖关系,若实例创建依赖其他模块的初始化,则可能出错

2. 懒汉式(Lazy Initialization)

懒汉式在第一次使用时才创建实例,避免了资源浪费:

cpp 复制代码
class Singleton {
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    // 静态指针
    static Singleton* instance;

public:
    static Singleton& getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
		return *instance;
    }
    
    // 可选:销毁实例
    static void destroyInstance() {
        if (instance != nullptr) {
            delete instance;
            instance = nullptr;
        }
    }
};

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

优点

  • 延迟初始化,节约资源
  • 实现相对简单

缺点

  • 非线程安全,多线程环境下可能创建多个实例
  • 需要手动管理内存释放

3. 线程安全的懒汉式

为了解决线程安全问题,可以使用互斥锁:

cpp 复制代码
#include <mutex>

class Singleton {
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    static Singleton* instance;
    static std::mutex mtx;

public:
    static Singleton& getInstance() {
        // 双重检查锁定(Double-Checked Locking)
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return *instance;
    }
};

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

两次检查的作用

  1. 第一次检查(无锁)当 instance 已初始化时,直接返回,避免每次调用都加锁,大幅减少锁开销。这是性能优化的核心。
  2. 第二次检查(加锁后):防止多线程并发时的 "竞态条件"。例如:
    • 线程 A 执行第一次检查(instance 为空),准备加锁;
    • 线程 B 已加锁并创建了 instance,随后释放锁;
    • 线程 A 获得锁后,如果不再次检查,会重复创建 instance,破坏单例唯一性。
    • 第二次检查确保:只有当 instance 仍为空时,才执行初始化。
      注意:静态成员变量需要在类外初始化

优点

  • 线程安全
  • 延迟初始化
  • 双重检查锁定减少了锁的开销

缺点

  • 实现较复杂
  • 仍需手动管理内存释放
  • 在某些内存模型下可能存在指令重排问题

4. 局部静态变量式(Meyers' Singleton)

这是 C++11 后推荐的实现方式,利用局部静态变量的特性:

cpp 复制代码
class Singleton {
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    static Singleton& getInstance() {
        // 局部静态变量,第一次调用时初始化
        static Singleton instance;
        return instance;
    }
};

优点

  • 线程安全(C++11 标准保证局部静态变量初始化是线程安全的)
  • 自动管理内存,程序结束时自动销毁
  • 实现简洁优雅
  • 真正的延迟初始化

缺点

  • 不适合需要显式控制销毁顺序的场景
  • C++11 之前的编译器可能不支持线程安全的初始化

三、单例模式的应用场景

单例模式适用于以下场景:

  1. 资源管理器:如数据库连接池、线程池等,需要统一管理资源
  2. 配置管理器:全局配置信息只需要加载一次
  3. 日志系统:全局唯一的日志输出点,避免日志混乱
  4. 设备驱动:物理设备的驱动程序通常只需要一个实例

四、单例模式的注意事项

  1. 线程安全:多线程环境下必须保证实例创建的线程安全性
  2. 拷贝控制:必须禁用拷贝构造函数和赋值运算符
  3. 析构顺序:多个单例之间的析构顺序可能导致问题
  4. 测试困难:单例模式会使单元测试变得困难,因为它引入了全局状态
  5. 不要过度使用:并非所有场景都需要单例,过度使用会导致代码耦合度提高

五、总结

单例模式是一种强大但也容易被滥用的设计模式。在 C++ 中,推荐使用局部静态变量式(Meyers' Singleton),它兼具线程安全、延迟初始化和实现简洁的优点。

选择单例模式时,应该权衡其带来的便利和可能的副作用,确保它确实是解决问题的最佳方案。在大多数情况下,对于需要全局访问且唯一存在的组件,单例模式仍然是一个优秀的选择。

扩展--模板单例模式

C++ 模板单例模式(Template Singleton)是一种通过模板实现"单例逻辑复用"的设计,核心思想是定义一个通用的单例模板类,让需要成为单例的类通过继承或实例化该模板,自动获得单例特性,避免重复编写单例代码。

1. 模板单例模式的实现方式

最常用的实现是基于 CRTP(奇异递归模板模式,Curiously Recurring Template Pattern),即让目标类继承模板单例类,并将自身作为模板参数传入。这种方式既能保证单例的唯一性,又能让每个目标类拥有独立的单例实例。

实现示例:线程安全的模板单例
cpp 复制代码
#include <iostream>

// 模板单例基类(CRTP模式)
template <typename T>
class Singleton {
public:
    // 禁用拷贝构造和赋值(防止单例被复制)
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 全局访问点:返回子类的唯一实例
    static T& getInstance() {
        // 局部静态变量:C++11后初始化线程安全,且自动销毁
        static T instance;
        return instance;
    }

protected:
    // 保护构造函数:允许子类(T)调用,禁止外部直接创建
    Singleton() = default;
    ~Singleton() = default; // 保护析构,避免外部delete
};


// 示例:让Logger类成为单例(继承模板单例)
class Logger : public Singleton<Logger> {
    // 友元声明:允许Singleton<Logger>访问Logger的私有构造函数
    friend class Singleton<Logger>;

private:
    // 私有构造函数:确保只能通过getInstance创建
    Logger() {
        std::cout << "Logger instance created\n";
    }

public:
    void log(const std::string& message) {
        std::cout << "[Log] " << message << std::endl;
    }
};


// 示例:让ConfigManager类成为单例
class ConfigManager : public Singleton<ConfigManager> {
    friend class Singleton<ConfigManager>;

private:
    ConfigManager() {
        std::cout << "ConfigManager instance created\n";
    }

public:
    std::string getConfig(const std::string& key) {
        return "value of " + key;
    }
};


// 测试
int main() {
    // 获取Logger单例并使用
    Logger::getInstance().log("Hello Singleton");

    // 获取ConfigManager单例并使用
    std::cout << ConfigManager::getInstance().getConfig("port") << std::endl;

    // 验证唯一性(地址相同)
    Logger* log1 = &Logger::getInstance();
    Logger* log2 = &Logger::getInstance();
    std::cout << "Logger instances same? " << (log1 == log2 ? "Yes" : "No") << std::endl;

    return 0;
}

2. 实现关键点解析

  1. CRTP 模式的作用

    模板参数 T 是目标类(如 Logger),通过 Singleton<T> 继承,让模板类能访问 T 的私有构造函数(需通过 friend 声明),同时确保每个 T 对应唯一的单例实例(LoggerConfigManager 是两个独立单例)。

  2. 线程安全性

    利用 C++11 特性:局部静态变量的初始化是线程安全的(编译器保证只有一个线程执行初始化),因此 getInstance() 无需额外加锁,简洁且安全。

  3. 构造函数保护

    模板基类 Singleton<T> 的构造函数为 protected,允许子类(T)调用;子类的构造函数为 private,并通过 friend 让基类访问,确保外部无法直接创建实例,只能通过 getInstance() 获取。

  4. 禁止拷贝

    显式删除拷贝构造和赋值运算符,防止单例被复制(避免产生多个实例)。

3. 是否推荐使用模板单例?

推荐场景:
  • 多个类需要单例特性 :如果项目中有多个类(如日志、配置、连接池等)都需要实现单例,模板单例可以消除重复代码,减少开发工作量和出错概率。
  • 追求代码一致性:模板单例能保证所有单例类的实现逻辑统一(如线程安全、销毁方式等),便于团队协作和维护。
不推荐场景/注意事项:
  1. 过度使用单例:模板单例降低了实现成本,可能导致开发者滥用单例模式(如将本不该是单例的类也做成单例),增加代码耦合度和测试难度。
  2. 析构顺序问题 :多个单例的析构顺序是不确定的(由编译器决定),如果单例之间有依赖关系(如 A 析构依赖 B 存在),可能导致程序退出时崩溃。
  3. 继承灵活性受限 :目标类必须继承 Singleton<T>,如果类本身需要继承其他基类,可能引入菱形继承问题(需谨慎设计继承关系)。
  4. 模板的局限性:模板会导致代码膨胀(每个实例化的单例类都会生成独立代码),但对现代编译器和大多数项目来说,影响通常可忽略。

4. 总结

模板单例模式是 "单例逻辑复用"的优秀方案,适合多单例场景,能显著减少重复代码并保证实现一致性。但需注意:单例模式本身应谨慎使用(避免过度设计),且需处理好多个单例间的依赖关系。

如果项目中确实需要多个单例,模板单例是推荐的实现方式;如果仅需一两个单例,直接手写可能更简洁直观。

相关推荐
sorryhc23 分钟前
【AI解读源码系列】ant design mobile——Button按钮
前端·javascript·react.js
VOLUN24 分钟前
PageLayout布局组件封装技巧
前端·javascript·vue.js
掘金安东尼24 分钟前
React 的 use() API 或将取代 useContext
前端·javascript·react.js
祁同伟.44 分钟前
【C++】模版(初阶)
c++
蓝胖子的小叮当1 小时前
JavaScript基础(十三)函数柯里化curry
前端·javascript
sTone873751 小时前
android studio之外使用NDK编译生成android指定架构的动态库
android·c++
前端Hardy1 小时前
HTML&CSS:有趣的SVG路径动画效果
javascript·css
前端Hardy1 小时前
HTML&CSS:超酷炫的3D动态卡片
前端·javascript·css
江城开朗的豌豆2 小时前
我在项目中这样处理useEffect依赖引用类型,同事直呼内行
前端·javascript·react.js
听风的码2 小时前
Vue2封装Axios
开发语言·前端·javascript·vue.js