C++ 设计模式之单例模式详细介绍

单例模式是 创建型设计模式 的核心成员,其核心目标是:确保一个类在程序生命周期内仅有一个实例,并提供一个全局统一的访问点

在 C++ 开发中,单例模式广泛应用于管理全局资源(如日志、配置、数据库连接池)、避免重复初始化(如重型对象)、维护全局状态(如计数器)等场景。本文将从核心原理、实现方式、关键问题、适用场景等维度,全面解析 C++ 单例模式的设计与实践。

一、单例模式的核心定义与特点

1.1 核心目标

  • 唯一性 :类的实例在整个程序中只能有一个,禁止外部通过 new、拷贝等方式创建多个实例。
  • 全局访问 :提供一个静态方法(如 getInstance()),让程序任何地方都能便捷访问该实例。
  • 可控初始化:根据需求选择"提前初始化"(饿汉式)或"延迟初始化"(懒汉式),平衡资源占用与启动速度。

1.2 核心设计约束(必须满足)

要实现单例模式,需通过以下约束禁止"多实例":

  1. 私有构造函数private: Singleton() {},禁止外部直接 new Singleton() 创建实例。
  2. 私有拷贝构造函数private: Singleton(const Singleton&) = delete;(C++11),禁止拷贝实例。
  3. 私有赋值运算符private: Singleton& operator=(const Singleton&) = delete;(C++11),禁止赋值实例。
  4. 静态实例与全局访问点:通过静态成员变量存储唯一实例,静态成员方法提供访问接口。

二、C++ 单例模式的常见实现方式

单例模式的实现核心矛盾是 "线程安全""性能/资源占用" 的平衡。以下是 C++ 中最常用的 5 种实现方式,按推荐优先级排序:

2.1 推荐方案:Meyers 单例(C++11 局部静态变量)

这是 目前最推荐的单例实现,由 C++ 大师 Scott Meyers 提出,借助 C++11 标准的特性,实现了"简单、线程安全、延迟初始化"的完美平衡。

核心原理

C++11 标准明确规定:局部静态变量的初始化是线程安全的 ------当多个线程同时调用 getInstance() 时,编译器会自动保证局部静态变量 instance 仅被初始化一次,无需手动加锁。

实现代码
cpp 复制代码
#include <iostream>

class Singleton {
public:
    // 3. 全局访问点:返回唯一实例(C++11 线程安全)
    static Singleton& getInstance() {
        static Singleton instance;  // 局部静态变量,仅初始化一次
        return instance;
    }

    // 业务方法(示例)
    void doSomething() {
        std::cout << "Meyers Singleton: " << this << std::endl;
    }

    // 2. 禁止拷贝和赋值(C++11 推荐用 delete)
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    // 1. 私有构造函数(禁止外部实例化)
    Singleton() {
        std::cout << "Singleton 构造(仅调用一次)" << std::endl;
    }

    // 私有析构函数(可选,防止外部 delete)
    ~Singleton() {
        std::cout << "Singleton 析构(程序退出时调用)" << std::endl;
    }
};

// 测试代码
int main() {
    // 多线程调用也不会创建多个实例(C++11 线程安全)
    Singleton& s1 = Singleton::getInstance();
    Singleton& s2 = Singleton::getInstance();

    s1.doSomething();  // 输出地址相同
    s2.doSomething();

    return 0;
}
输出结果
复制代码
Singleton 构造(仅调用一次)
Meyers Singleton: 0x7f8a1b400008
Meyers Singleton: 0x7f8a1b400008
Singleton 析构(程序退出时调用)
核心优点
  • 简单高效:代码极简,无需手动管理线程安全和实例销毁。
  • 线程安全:C++11 标准保证局部静态变量初始化的原子性,无数据竞争。
  • 延迟初始化 :实例在第一次调用 getInstance() 时才创建,避免提前占用资源。
  • 自动销毁:程序退出时,局部静态变量会自动调用析构函数,无内存泄漏风险。
适用场景
  • 绝大多数日常开发场景(无特殊需求时,优先选择此方案)。
  • 不需要传递参数初始化、对启动速度有要求的场景。
注意事项
  • 兼容性:仅支持 C++11 及以上标准(目前主流编译器均支持)。
  • 析构顺序:若多个单例存在依赖关系,析构顺序可能不确定(需通过主动销毁机制解决)。

2.2 饿汉式(提前初始化)

饿汉式的核心是 "提前创建实例"------在程序启动时(全局变量初始化阶段)就创建单例实例,无需延迟初始化。

实现代码
cpp 复制代码
#include <iostream>

class Singleton {
public:
    // 3. 全局访问点:返回提前创建的实例
    static Singleton& getInstance() {
        return instance;  // 直接返回全局静态实例
    }

    void doSomething() {
        std::cout << "Hungry Singleton: " << this << std::endl;
    }

    // 禁止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    // 1. 私有构造函数
    Singleton() {
        std::cout << "Hungry Singleton 构造(程序启动时调用)" << std::endl;
    }

    ~Singleton() {
        std::cout << "Hungry Singleton 析构" << std::endl;
    }

    // 2. 全局静态实例(程序启动时初始化)
    static Singleton instance;
};

// 全局静态实例初始化(类外定义,触发构造函数)
Singleton Singleton::instance;

// 测试代码
int main() {
    std::cout << "main 函数启动" << std::endl;
    Singleton& s1 = Singleton::getInstance();
    Singleton& s2 = Singleton::getInstance();

    s1.doSomething();
    s2.doSomething();

    return 0;
}
输出结果
复制代码
Hungry Singleton 构造(程序启动时调用)
main 函数启动
Hungry Singleton: 0x6020c8
Hungry Singleton: 0x6020c8
Hungry Singleton 析构
核心优点
  • 线程安全:实例在程序启动时(单线程初始化阶段)创建,无多线程竞争问题。
  • 性能最优getInstance() 仅返回引用,无锁开销、无初始化判断,访问速度最快。
  • 实现简单:无需处理线程同步和延迟初始化逻辑。
核心缺点
  • 资源浪费:若单例实例占用大量资源(如内存、数据库连接),且程序运行中可能未使用,会造成资源闲置。
  • 初始化顺序不确定:若多个饿汉式单例存在依赖(如 A 依赖 B 的实例),可能因初始化顺序导致崩溃(全局变量初始化顺序不可控)。
适用场景
  • 单例实例占用资源少、启动时必须初始化的场景(如配置管理类)。
  • 对访问性能要求极高,无需延迟初始化的场景。

2.3 懒汉式(线程安全,互斥锁)

懒汉式的核心是 "延迟初始化" ------仅在第一次调用 getInstance() 时创建实例。为解决多线程安全问题,需通过互斥锁(std::mutex)保护实例创建过程。

实现代码
cpp 复制代码
#include <iostream>
#include <mutex>  // 需包含互斥锁头文件

class Singleton {
public:
    // 3. 全局访问点(加锁保证线程安全)
    static Singleton* getInstance() {
        // 双重检查锁定(DCLP):减少锁开销
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);  // 加锁
            if (instance == nullptr) {
                instance = new Singleton();  // 仅第一次调用时创建实例
            }
        }
        return instance;
    }

    void doSomething() {
        std::cout << "Lazy Singleton: " << this << std::endl;
    }

    // 主动销毁接口(可选,解决堆内存泄漏)
    static void destroyInstance() {
        std::lock_guard<std::mutex> lock(mtx);
        if (instance != nullptr) {
            delete instance;
            instance = nullptr;
        }
    }

    // 禁止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    // 1. 私有构造函数
    Singleton() {
        std::cout << "Lazy Singleton 构造" << std::endl;
    }

    ~Singleton() {
        std::cout << "Lazy Singleton 析构" << std::endl;
    }

    // 2. 静态成员变量(堆上存储,延迟初始化)
    static Singleton* instance;
    static std::mutex mtx;  // 互斥锁,保护实例创建
};

// 静态成员变量初始化(类外定义)
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

// 测试代码
int main() {
    Singleton* s1 = Singleton::getInstance();
    Singleton* s2 = Singleton::getInstance();

    s1->doSomething();
    s2->doSomething();

    // 主动销毁(若不调用,进程退出时操作系统会回收内存,但析构函数不会执行)
    Singleton::destroyInstance();

    return 0;
}
核心亮点:双重检查锁定(DCLP)
  • 第一次检查 instance == nullptr:避免每次调用 getInstance() 都加锁(大部分场景下实例已存在,直接返回),减少锁开销。
  • 加锁后第二次检查 instance == nullptr:防止多个线程同时通过第一次检查,导致创建多个实例。
核心优点
  • 延迟初始化:仅在需要时创建实例,避免资源浪费。
  • 线程安全:通过互斥锁保证实例创建过程的原子性。
核心缺点
  • 实现复杂:需手动管理锁、实例销毁,易出错。
  • 锁开销:第一次调用后仍有一次空判断,但比每次加锁高效。
  • 内存泄漏风险 :若忘记调用 destroyInstance(),堆上的实例不会调用析构函数(若析构函数需释放资源,会导致资源泄漏)。
  • C++11 前的可见性问题 :旧标准中,instance = new Singleton() 可能被编译器重排(分配内存 → 赋值指针 → 调用构造函数),导致其他线程看到"半初始化"的实例。C++11 后可通过 std::atomic 解决,但会增加复杂度。
适用场景
  • C++11 前的旧项目(无法使用 Meyers 单例)。
  • 需手动控制实例销毁时机的场景(如释放文件句柄、数据库连接)。

2.4 带参数的单例

默认单例无法传递参数初始化(如配置类需要读取配置文件路径),需对核心逻辑改造,支持参数传递。

实现代码(基于 Meyers 单例扩展)
cpp 复制代码
#include <iostream>
#include <string>
#include <mutex>

class ConfigSingleton {
public:
    // 全局访问点:支持传递参数(仅第一次调用有效)
    static ConfigSingleton& getInstance(const std::string& config_path = "") {
        static ConfigSingleton instance(config_path);  // 传递参数初始化
        return instance;
    }

    // 获取配置(示例业务方法)
    std::string getConfig() const {
        return config_;
    }

    // 禁止拷贝和赋值
    ConfigSingleton(const ConfigSingleton&) = delete;
    ConfigSingleton& operator=(const ConfigSingleton&) = delete;

private:
    // 带参数的私有构造函数
    explicit ConfigSingleton(const std::string& config_path) {
        // 模拟读取配置文件
        if (config_path.empty()) {
            config_ = "默认配置";
        } else {
            config_ = "从路径 " + config_path + " 加载的配置";
        }
        std::cout << "ConfigSingleton 构造:" << config_ << std::endl;
    }

    ~ConfigSingleton() {
        std::cout << "ConfigSingleton 析构" << std::endl;
    }

    std::string config_;  // 配置数据
};

// 测试代码
int main() {
    // 第一次调用:传递参数初始化
    ConfigSingleton& config1 = ConfigSingleton::getInstance("./config.json");
    std::cout << "config1: " << config1.getConfig() << std::endl;

    // 后续调用:参数无效,返回已创建的实例
    ConfigSingleton& config2 = ConfigSingleton::getInstance("./other.json");
    std::cout << "config2: " << config2.getConfig() << std::endl;

    return 0;
}
输出结果
复制代码
ConfigSingleton 构造:从路径 ./config.json 加载的配置
config1: 从路径 ./config.json 加载的配置
config2: 从路径 ./config.json 加载的配置
ConfigSingleton 析构
关键说明
  • 仅第一次调用 getInstance() 时,参数有效;后续调用的参数会被忽略(保证实例唯一性)。
  • 若需强制参数必须传递(禁止默认值),可移除 config_path 的默认值,并在构造函数中检查参数合法性。
  • 线程安全:依赖 Meyers 单例的局部静态变量初始化特性,仍为线程安全。
适用场景
  • 单例实例需要初始化参数的场景(如配置类、数据库连接池类)。

2.5 可自动销毁的懒汉式(解决内存泄漏)

针对普通懒汉式"析构函数不执行"的问题,可通过 内部销毁类 实现自动销毁。

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

class Singleton {
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            if (instance == nullptr) {
                instance = new Singleton();
                // 创建销毁器(全局静态变量,程序退出时调用析构)
                static Destroyer destroyer;
            }
        }
        return instance;
    }

    void doSomething() {
        std::cout << "Auto-Destroy Lazy Singleton: " << this << std::endl;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    // 内部销毁类(友元,可访问私有析构函数)
    class Destroyer {
    public:
        ~Destroyer() {
            // 程序退出时,销毁单例实例
            if (Singleton::instance != nullptr) {
                delete Singleton::instance;
                Singleton::instance = nullptr;
                std::cout << "Destroyer 销毁 Singleton" << std::endl;
            }
        }
    };

    Singleton() {
        std::cout << "Auto-Destroy Lazy Singleton 构造" << std::endl;
    }

    ~Singleton() {
        std::cout << "Auto-Destroy Lazy Singleton 析构" << std::endl;
    }

    static Singleton* instance;
    static std::mutex mtx;
};

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

// 测试代码
int main() {
    Singleton* s1 = Singleton::getInstance();
    s1->doSomething();
    return 0;
}
输出结果
复制代码
Auto-Destroy Lazy Singleton 构造
Auto-Destroy Lazy Singleton: 0x7f9b3c400000
Auto-Destroy Lazy Singleton 析构
Destroyer 销毁 Singleton
核心原理
  • 内部 Destroyer 类是全局静态变量,程序退出时会自动调用其析构函数。
  • DestroyerSingleton 的友元(或通过全局访问点),可安全删除 instance,确保析构函数执行。

三、单例模式的关键问题与解决方案

3.1 线程安全问题

  • 核心矛盾 :多线程同时调用 getInstance() 时,可能创建多个实例。
  • 解决方案
    1. 优先使用 Meyers 单例(C++11 线程安全,无需手动处理)。
    2. 饿汉式(提前初始化,无多线程竞争)。
    3. 懒汉式加互斥锁(双重检查锁定,适合旧标准)。

3.2 内存泄漏问题

  • 场景 :堆上创建的单例实例(如普通懒汉式)未调用 delete,导致析构函数不执行(若析构需释放资源,如文件句柄、数据库连接,会造成资源泄漏)。
  • 解决方案
    1. 用局部静态变量(Meyers 单例、饿汉式):程序退出时自动销毁,析构函数执行。
    2. 内部销毁类(自动销毁的懒汉式):通过全局静态销毁器自动调用 delete
    3. 主动销毁接口(如 destroyInstance()):手动控制销毁时机(需确保仅调用一次)。

3.3 单例被破坏的场景与防护

单例的"唯一性"可能被以下方式破坏,需针对性防护:

(1)拷贝或赋值创建实例
  • 破坏方式Singleton s = Singleton::getInstance();(调用拷贝构造函数)。
  • 防护方案 :将拷贝构造函数和赋值运算符设为 delete(C++11)或私有且不实现(C++11 前)。
(2)继承破坏单例
  • 破坏方式:子类继承单例类,并重写构造函数,创建子类实例。
  • 防护方案
    1. 将单例类的析构函数设为 private(禁止继承,因子类析构需访问父类析构)。
    2. final 关键字修饰单例类(C++11),禁止继承:class Singleton final { ... }
(3)反射破坏(C++ 中风险极低)
  • 破坏方式 :通过反射机制(如 dynamic_cast + 内存操作)绕过私有构造函数。
  • 防护方案:C++ 无原生反射,且反射破坏需底层内存操作,实际开发中几乎无需考虑;若需极致防护,可在构造函数中添加全局计数器,判断是否已创建实例。
(4)多进程/多线程fork破坏
  • 破坏方式 :多进程场景中,fork() 会复制父进程的内存空间,导致子进程拥有独立的单例实例。
  • 防护方案fork() 后,子进程重新初始化单例(或通过进程间通信同步实例状态)。

3.4 初始化顺序与依赖问题

  • 问题:多个单例存在依赖(如 A 初始化需调用 B 的实例),若初始化顺序不确定(如饿汉式的全局变量初始化顺序),可能导致 A 初始化时 B 未创建,触发崩溃。
  • 解决方案
    1. 用 Meyers 单例(延迟初始化):A 初始化时调用 B 的 getInstance(),确保 B 先创建。
    2. 显式初始化:提供 init() 接口,手动控制单例的初始化顺序。

四、单例模式的适用场景与反场景

4.1 适用场景

单例模式适合管理 全局唯一、资源密集、需统一访问 的对象:

  1. 日志类:全局唯一的日志器,所有模块通过同一实例写入日志(避免多日志文件冲突)。
  2. 配置管理类:加载全局配置(如数据库地址、端口),所有模块共享配置数据。
  3. 数据库连接池:全局唯一的连接池,统一管理连接资源,避免重复创建连接。
  4. 全局计数器:统计程序运行状态(如请求数、错误数),需全局统一计数。
  5. 缓存管理器:全局缓存实例,统一管理缓存的增删改查。

4.2 反场景(不建议使用单例)

  1. 需要多个实例的场景:如多个数据库连接(应使用连接池,而非单例连接)。
  2. 依赖注入优先的场景:单例会增加代码耦合(全局访问点导致模块依赖单例类),若项目使用依赖注入(DI)框架,应通过注入方式传递实例,而非单例。
  3. 状态频繁变化的场景:单例的全局状态易被多个模块修改,导致状态混乱(需加锁保护,影响性能)。
  4. 单元测试困难的场景:单例的全局状态会影响测试独立性,需在每个测试用例前重置单例状态(复杂度高)。

五、单例模式的优缺点

5.1 优点

  1. 唯一性保证:确保全局仅有一个实例,避免资源竞争和状态不一致。
  2. 全局访问:无需传递实例指针,简化模块间的资源共享。
  3. 资源优化:延迟初始化(如 Meyers 单例)避免提前占用资源;提前初始化(如饿汉式)避免运行时初始化开销。
  4. 生命周期可控:可手动或自动管理实例的创建与销毁。

5.2 缺点

  1. 代码耦合度高:全局访问点导致模块依赖单例类,不利于代码解耦和重构。
  2. 单元测试困难:单例的全局状态会影响测试独立性,需额外处理测试环境的状态重置。
  3. 线程安全开销:懒汉式的锁机制会增加少量性能开销(Meyers 单例无此问题)。
  4. 扩展性差:单例类通常禁止继承和多实例,后续若需多个实例(如多租户场景),需重构代码。

六、进阶:单例模式的替代方案

若单例的"高耦合""难测试"问题影响项目架构,可考虑以下替代方案:

6.1 依赖注入(DI)

通过构造函数或接口将实例注入到需要的模块,而非全局访问。例如:

cpp 复制代码
// 不使用单例:通过依赖注入传递配置实例
class Service {
private:
    Config& config_;
public:
    // 构造函数注入配置实例
    Service(Config& config) : config_(config) {}
    void doWork() { /* 使用 config_  */ }
};

// 调用方:创建配置实例,注入到 Service
int main() {
    Config config("./config.json");
    Service service(config);
    service.doWork();
    return 0;
}
  • 优点:解耦模块依赖,便于单元测试(可注入mock实例)。
  • 适用场景:中大型项目,追求代码可测试性和可扩展性。

6.2 全局静态变量(不推荐)

  • 方案:直接使用全局静态变量(如 extern Config g_config;),替代单例。
  • 缺点:无访问控制(可被任意修改)、初始化顺序不确定、无生命周期管理,仅适用于简单场景。

6.3 局部单例(按上下文唯一)

  • 方案:单例的唯一性不是"全局唯一",而是"上下文唯一"(如每个线程一个实例)。

  • 实现:用 thread_local 修饰静态实例(C++11),确保每个线程有独立实例:

    cpp 复制代码
    static Singleton& getThreadLocalInstance() {
        thread_local Singleton instance;  // 每个线程独立实例
        return instance;
    }
  • 适用场景:线程私有资源管理(如线程本地存储 TLS)。

七、总结与推荐实践

7.1 不同实现方式对比

实现方式 线程安全(C++11+) 延迟初始化 代码复杂度 内存泄漏风险 推荐优先级
Meyers 单例(局部静态) 极低 ★★★★★
饿汉式(全局静态) ★★★★☆
懒汉式(互斥锁) 有(需手动销毁) ★★★☆☆
带参数的 Meyers 单例 ★★★★☆

7.2 推荐实践

  1. 日常开发首选:Meyers 单例(局部静态变量),简单、高效、线程安全,无内存泄漏。
  2. 需参数初始化:带参数的 Meyers 单例(仅第一次调用传递参数)。
  3. 启动时必须初始化:饿汉式(如配置类、日志类),性能最优。
  4. 旧项目兼容:懒汉式(互斥锁+双重检查),需注意销毁机制。
  5. 高可扩展性需求:避免单例,使用依赖注入替代。

7.3 核心原则

  • 单例是"必要之恶":仅在确实需要全局唯一实例时使用,避免过度设计。
  • 优先保证线程安全和无内存泄漏:选择成熟的实现方式(如 Meyers 单例),避免手动管理锁和销毁逻辑。
  • 防护单例破坏:必须禁用拷贝、赋值和继承,确保唯一性。

通过以上内容,可全面掌握 C++ 单例模式的设计、实现与工程实践,在实际开发中根据场景选择合适的方案,平衡唯一性、性能和可扩展性。

相关推荐
wanhengidc1 小时前
云手机面向的用户群体都有哪些?
运维·服务器·科技·智能手机·云计算
qq_266348731 小时前
aspose处理模板,并去掉水印 jdk17
java·开发语言
Evan芙1 小时前
Rocky Linux 9 网卡地址定制
linux·服务器·网络
Mr_WangAndy1 小时前
C++17 新特性_第一章 C++17 语言特性_if constexpr,类模板参数推导 (CTAD)
c++·c++40周年·if constexpr·类模板参数推导 ctad·c++17新特性
小年糕是糕手1 小时前
【C++】类和对象(三) -- 拷贝构造函数、赋值运算符重载
开发语言·c++·程序人生·考研·github·个人开发·改行学it
xunyan62341 小时前
面向对象(下)-设计模式与单例设计模式
java·单例模式·设计模式
隔山打牛牛1 小时前
单例模式:高效实现全局唯一实例
单例模式
艾莉丝努力练剑1 小时前
【C++:C++11收尾】解构C++可调用对象:从入门到精通,掌握function包装器与bind适配器包装器详解
java·开发语言·c++·人工智能·c++11·右值引用