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 小时前
Vue3 零基础每日笔记(076):防抖节流的正确使用——搜索框、按钮防重复提交、滚动监听
javascript·vue.js·笔记
海绵宝宝转agent2 小时前
2026-10-9 leetcode100刷题+面经整理
笔记·算法·面试
yi0112 小时前
DAY24: LeetCode 167:两数之和 II|从暴力枚举到左右双指针
笔记·python·算法·leetcode·二分查找·双指针
yi0113 小时前
LeetCode 15:三数之和|排序 + 双指针,如何避免重复答案?
笔记·python·算法·leetcode·双指针
北风toto3 小时前
数据库笔记:Armstrong 公理系统与集合论的深度辨析
数据库·笔记
EasyBr指纹浏览器3 小时前
我的开源项目:小红书笔记归档,把来源和缺项一起留下
笔记·小红书·开源工具·素材管理·内容归档
海海不掉头发3 小时前
图论及其应用_课堂笔记整理_CSDN博客
笔记·图论
志尊宝3 小时前
Vue3 零基础每日笔记(075):图片懒加载与骨架屏
javascript·vue.js·笔记
会博通·代码搬运工3 小时前
家装数字化交付笔记:图纸归档、工地采集与档案系统对接的工程实践
笔记·数字化转型·档案管理系统·业务档案系统·家装数字化
Code_Solitude4 小时前
高数第一章函数笔记(原稿手写+AI识别优化)
人工智能·笔记