C++ 单例模式

前言

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

单例模式的核心要素

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

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

适用场景

  1. 日志系统 - 全局唯一的日志输出
  2. 配置管理 - 应用配置的集中存储
  3. 资源池 - 数据库连接池、线程池等
  4. 缓存系统 - 应用级缓存管理
  5. 硬件抽象 - 唯一硬件资源访问

一、局部静态变量式单例(C++11及以上)

1.1 局部静态变量

局部 】指的是作用域,【静态】指的是生命周期。

特性:

  • 生命周期延长整个程序的运行周期,函数第一次被调用时,变量被创建并初始化一次,之后函数多次被调用,变量不会被销毁、不会被重新初始化,值会被永久保留,直到程序退出。
  • 作用域限制 :不会改变变量的作用域!变量依然是「局部作用域」,只能在所在的函数/代码块内部被访问,函数外部完全无法访问这个变量。
  • 单次初始化只会在函数第一次被调用时执行一次,后续无论函数被调用多少次,这条初始化语句都不会再执行,变量会沿用上次的结果。

1.2 C++扩展应用

C++继承并扩展了这一特性,允许将静态局部变量机制应用于类实例管理,从而创造出优雅的单例模式实现。这种设计巧妙地"绕开"了作用域限制,通过返回引用让外部可以访问函数内部的静态实例,同时利用类的访问控制保证安全性**。**

1.3 局部静态变量式单例代码

cpp 复制代码
class Singleton {
private:
    // 私有构造函数:防止外部创建实例
    Singleton() {
        // 初始化逻辑
    }
    
    // 删除拷贝操作:防止复制
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
public:
    // 获取单例实例的全局访问点
    static Singleton& getInstance() {
        static Singleton instance;  // 静态局部变量 - 关键所在
        return instance;           // 返回引用 - "绕开"作用域限制
    }
    
    // 业务接口
    void operation() {
        // 实现业务逻辑
    }
};

1.4 技术要点解析

1.4.1 静态局部变量的内存行为
cpp 复制代码
内存布局示意图:
┌─────────────────────────────┐
│    全局数据区 (Static Storage)    │
│  ┌─────────────────────────┐ │
│  │  getInstance::instance  │←─┐
│  │   (实际存储位置)         │  │
│  └─────────────────────────┘  │
│                                │引用传递
└─────────────────────────────┘  │
                                 │
调用者:                          │
Singleton& ref = ────────────────┘
  Singleton::getInstance();
1.4.2 初始化机制对比
cpp 复制代码
// 传统全局变量方式 - 立即初始化
Singleton* g_instance = new Singleton();  // 程序启动即创建

// 静态局部变量方式 - 延迟初始化
static Singleton& getInstance() {
    static Singleton instance;  // 首次调用时创建
    return instance;
}

1.5 与传统单例模式的对比

1.5.1 实现方式比较
特性 指针成员方式 静态局部变量方式
线程安全 需手动加锁 C++11自动保证
内存管理 手动delete 自动管理
代码复杂度 较高 极简
初始化时机 可控 首次调用时
析构保证 可能泄漏 自动析构
1.5.2 内存生命周期 分析
cpp 复制代码
// 程序执行时间线分析
// t0: 程序启动
// t1: 首次调用getInstance()
//     → 构造instance
//     → 存储于静态存储区
//     → 返回引用
// t2: 后续调用getInstance()
//     → 直接返回现有引用
// t3: 程序结束
//     → 自动调用instance的析构函数

二、传统单例模式

2.1 饿汉式

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

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.2 懒汉式

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

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;

优点

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

缺点

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

2.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. 第二次检查(加锁后):防止多线程并发时的 "竞态条件"。例如:
    1. 线程 A 执行第一次检查(instance 为空),准备加锁;
    2. 线程 B 已加锁并创建了 instance,随后释放锁;
    3. 线程 A 获得锁后,如果不再次检查,会重复创建 instance,破坏单例唯一性。
    4. 第二次检查确保:只有当 instance 仍为空时,才执行初始化。
    5. 注意:静态成员变量需要在类外初始化

优点

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

缺点

  • 实现较复杂
  • 仍需手动管理内存释放
  • 在某些内存模型下可能存在指令重排问题
相关推荐
中屹指纹浏览器2 小时前
2026指纹浏览器底层性能优化:内存管理与进程调度实战解析
经验分享·笔记
悠哉悠哉愿意3 小时前
【物联网学习笔记】OLED
笔记·单片机·嵌入式硬件·物联网·学习
YuanDaima20483 小时前
解决Conda环境下RTX 50系列显卡PyTorch+Transformers+PEFT微调报错
人工智能·pytorch·笔记·python·深度学习·机器学习·conda
郑同学zxc3 小时前
Claude Code 的学习笔记
人工智能·笔记·学习
南境十里·墨染春水3 小时前
C++笔记 继承中重载规则 公有私有继承的区别(面向对象)
开发语言·c++·笔记
iiiiii113 小时前
【LLM学习笔记】Batch Normalization vs Layer Normalization,为什么 NLP 中使用 LN 而非 BN
笔记·深度学习·学习·语言模型·大模型·llm·transformer
今儿敲了吗3 小时前
49| 枚举排列
数据结构·c++·笔记·学习·算法
智算菩萨3 小时前
【Tkinter】14 事件处理机制深度解析:从基础绑定到高级传播,构建交互式绘图笔记应用
开发语言·笔记·python·microsoft·ui·ai编程·tkinter
老毛肚3 小时前
云原生笔记
笔记