【c++面向对象编程】第40篇:单例模式(Singleton)的多种C++实现

目录

一、单例模式是什么?

[二、饿汉式(Eager Initialization)](#二、饿汉式(Eager Initialization))

[三、懒汉式(Lazy Initialization)](#三、懒汉式(Lazy Initialization))

版本1:基础版(线程不安全)

版本2:加锁版(线程安全但性能差)

[版本3:双检锁(Double-Checked Locking)------ 经典但有问题](#版本3:双检锁(Double-Checked Locking)—— 经典但有问题)

[四、Meyers Singleton(C++11 最佳实践)](#四、Meyers Singleton(C++11 最佳实践))

为什么它是线程安全的?

五、完整例子:配置管理器

六、单例模式的问题与批评

替代方案

七、四种实现对比

八、常见错误

[1. 忘记删除拷贝构造和赋值](#1. 忘记删除拷贝构造和赋值)

[2. 多线程下使用裸指针懒汉式](#2. 多线程下使用裸指针懒汉式)

[3. Meyers Singleton 在 C++11 之前的编译器](#3. Meyers Singleton 在 C++11 之前的编译器)

[4. 单例中持有资源但不正确释放](#4. 单例中持有资源但不正确释放)

九、这一篇的收获


一、单例模式是什么?

cpp

复制代码
// 单例模式的核心特征:
// 1. 私有构造函数 —— 防止外部创建
// 2. 静态成员函数 getInstance() —— 全局访问点
// 3. 静态成员变量 —— 持有唯一实例

class Singleton {
private:
    Singleton() {}  // 私有构造
public:
    static Singleton& getInstance() {
        static Singleton instance;  // 唯一实例
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

使用场景

  • 全局配置管理器

  • 日志记录器

  • 数据库连接池

  • 硬件接口(如打印机)


二、饿汉式(Eager Initialization)

在程序启动时就创建实例,无论是否使用。

cpp

复制代码
class EagerSingleton {
private:
    static EagerSingleton instance;  // 静态实例
    int data;
    
    EagerSingleton() : data(0) {
        cout << "饿汉式单例:程序启动时创建" << endl;
    }
    
public:
    static EagerSingleton& getInstance() {
        return instance;
    }
    
    void setData(int d) { data = d; }
    int getData() const { return data; }
    
    EagerSingleton(const EagerSingleton&) = delete;
    EagerSingleton& operator=(const EagerSingleton&) = delete;
};

// 静态成员必须在类外定义
EagerSingleton EagerSingleton::instance;

int main() {
    // 即使不使用 getInstance,实例已存在
    auto& s = EagerSingleton::getInstance();
    s.setData(42);
}
优点 缺点
线程安全(静态初始化由编译器保证) 程序启动慢(所有单例都提前创建)
实现简单 可能创建永远不用的实例
访问快(无锁) 多个单例的初始化顺序不确定

三、懒汉式(Lazy Initialization)

首次调用 getInstance() 时才创建实例。

版本1:基础版(线程不安全)

cpp

复制代码
class LazySingleton {
private:
    static LazySingleton* instance;
    LazySingleton() { cout << "懒汉式:首次使用时创建" << endl; }
    
public:
    static LazySingleton* getInstance() {
        if (instance == nullptr) {
            instance = new LazySingleton();  // ❌ 多线程下可能创建多个
        }
        return instance;
    }
    
    LazySingleton(const LazySingleton&) = delete;
    LazySingleton& operator=(const LazySingleton&) = delete;
};

LazySingleton* LazySingleton::instance = nullptr;

线程安全问题 :多个线程同时进入 if (instance == nullptr),会创建多个实例。

版本2:加锁版(线程安全但性能差)

cpp

复制代码
#include <mutex>

class LockedSingleton {
private:
    static LockedSingleton* instance;
    static mutex mtx;
    LockedSingleton() { cout << "加锁懒汉式创建" << endl; }
    
public:
    static LockedSingleton* getInstance() {
        lock_guard<mutex> lock(mtx);  // 每次调用都加锁
        if (instance == nullptr) {
            instance = new LockedSingleton();
        }
        return instance;
    }
};

LockedSingleton* LockedSingleton::instance = nullptr;
mutex LockedSingleton::mtx;

问题:每次调用都加锁,即使实例已存在,性能差。

版本3:双检锁(Double-Checked Locking)------ 经典但有问题

cpp

复制代码
static LockedSingleton* getInstance() {
    if (instance == nullptr) {           // 第一次检查(无锁)
        lock_guard<mutex> lock(mtx);
        if (instance == nullptr) {       // 第二次检查(有锁)
            instance = new LockedSingleton();
        }
    }
    return instance;
}

DCLP 在 C++11 之前有内存可见性问题 (指令重排可能导致返回未完全构造的对象)。C++11 之后可以用 std::atomic 和内存屏障修复,但实现复杂,不推荐。


四、Meyers Singleton(C++11 最佳实践)

Scott Meyers 在《Effective C++》中提出的方案,C++11 起是线程安全的。

cpp

复制代码
class MeyersSingleton {
private:
    int data;
    MeyersSingleton() : data(0) {
        cout << "Meyers Singleton 创建" << endl;
    }
    
public:
    static MeyersSingleton& getInstance() {
        static MeyersSingleton instance;  // 函数静态变量
        return instance;
    }
    
    void setData(int d) { data = d; }
    int getData() const { return data; }
    
    MeyersSingleton(const MeyersSingleton&) = delete;
    MeyersSingleton& operator=(const MeyersSingleton&) = delete;
};

// 使用
auto& s = MeyersSingleton::getInstance();

为什么它是线程安全的?

C++11 标准保证:函数内的静态变量初始化是线程安全的 。多个线程同时调用 getInstance() 时,只有一个线程执行初始化,其他线程等待。

特性 Meyers Singleton
线程安全 ✅ C++11 起保证
懒加载 ✅ 首次调用时创建
无锁 ✅ 初始化后无锁访问
实现简单 ✅ 仅 5 行代码
内存安全 ✅ 程序结束时自动析构

这是现代 C++ 中实现单例的推荐方式。


五、完整例子:配置管理器

cpp

复制代码
#include <iostream>
#include <string>
#include <unordered_map>
#include <mutex>
using namespace std;

class ConfigManager {
private:
    unordered_map<string, string> config;
    
    // 私有构造
    ConfigManager() {
        cout << "加载配置文件..." << endl;
        // 模拟从文件加载
        config["db_host"] = "localhost";
        config["db_port"] = "3306";
        config["log_level"] = "info";
    }
    
    // 清理日志(可选)
    void logAccess(const string& key) const {
        // 可以记录配置访问日志
    }
    
public:
    // 删除拷贝构造和赋值
    ConfigManager(const ConfigManager&) = delete;
    ConfigManager& operator=(const ConfigManager&) = delete;
    
    // 唯一访问点
    static ConfigManager& getInstance() {
        static ConfigManager instance;
        return instance;
    }
    
    // 获取配置
    string get(const string& key, const string& defaultVal = "") const {
        logAccess(key);
        auto it = config.find(key);
        if (it != config.end()) {
            return it->second;
        }
        return defaultVal;
    }
    
    // 设置配置(运行时修改)
    void set(const string& key, const string& value) {
        config[key] = value;
    }
    
    // 打印所有配置
    void printAll() const {
        cout << "\n=== 当前配置 ===" << endl;
        for (const auto& [k, v] : config) {
            cout << k << " = " << v << endl;
        }
    }
};

// ========== 其他需要使用配置的类 ==========
class Database {
public:
    void connect() {
        auto& config = ConfigManager::getInstance();
        string host = config.get("db_host");
        string port = config.get("db_port");
        cout << "连接数据库: " << host << ":" << port << endl;
    }
};

class Logger {
public:
    void log(const string& msg) {
        auto& config = ConfigManager::getInstance();
        string level = config.get("log_level", "debug");
        cout << "[" << level << "] " << msg << endl;
    }
};

int main() {
    // 第一次调用 getInstance() 时创建实例
    auto& config = ConfigManager::getInstance();
    config.printAll();
    
    Database db;
    db.connect();
    
    Logger logger;
    logger.log("应用程序启动");
    
    // 运行时修改配置
    config.set("log_level", "warning");
    logger.log("这是一条警告");
    
    // 证明是同一个实例
    auto& config2 = ConfigManager::getInstance();
    cout << "\n地址相同: " << (&config == &config2) << endl;
    
    return 0;
}

输出:

text

复制代码
加载配置文件...

=== 当前配置 ===
db_host = localhost
db_port = 3306
log_level = info
连接数据库: localhost:3306
[info] 应用程序启动
[warning] 这是一条警告

地址相同: 1

六、单例模式的问题与批评

单例模式虽然常用,但也经常被批评为"反模式":

问题 说明
全局状态 单例本质上是全局变量,增加了耦合
测试困难 无法 mock 单例,单元测试时状态会残留
隐藏依赖 函数内部调用 getInstance(),依赖关系不明显
多例难以扩展 如果需要变成多例(如不同环境的配置),改造成本高

替代方案

场景 推荐方式
配置管理 依赖注入(传引用)
日志系统 传递 Logger 引用
全局唯一 Meyer Singleton(实在需要时)

原则 :优先考虑依赖注入,只有在确实全局唯一没有更好的设计时才用单例。


七、四种实现对比

实现方式 线程安全 懒加载 实现复杂度 推荐度
饿汉式 ✅(C++98) 简单 不推荐(启动慢)
懒汉式(裸指针) 简单
懒汉式(加锁) 中等 不推荐(性能差)
双检锁 ✅(C++11后需 careful) 复杂
Meyers Singleton ✅(C++11) 极简 ✅ 推荐

八、常见错误

1. 忘记删除拷贝构造和赋值

cpp

复制代码
// ❌ 错误:没有删除拷贝构造
Singleton s1 = Singleton::getInstance();
Singleton s2 = s1;  // 可以拷贝,破坏了单例

2. 多线程下使用裸指针懒汉式

cpp

复制代码
// ❌ 多线程不安全
static Singleton* getInstance() {
    if (!instance) instance = new Singleton();
    return instance;
}

3. Meyers Singleton 在 C++11 之前的编译器

C++98/03 标准不保证函数静态变量初始化的线程安全。如果编译器不支持 C++11,需要加锁。

4. 单例中持有资源但不正确释放

Meyers Singleton 在程序结束时自动析构,但如果有复杂的资源依赖(如 A 单例析构时用到 B 单例),可能出问题。解决方案:让单例简单,复杂资源用 unique_ptr 管理。


九、这一篇的收获

你现在应该理解:

  • 饿汉式:启动时创建,线程安全但可能浪费

  • 懒汉式:首次使用时创建,基础版线程不安全

  • Meyers Singleton :函数内静态变量,C++11 起线程安全,最推荐

  • 单例的缺点:全局状态、测试困难、隐藏依赖

  • 原则:优先依赖注入,确实需要时用 Meyers Singleton

💡 小作业:实现一个 Logger 单例,支持不同日志级别(INFO、WARNING、ERROR),线程安全,并提供 setLogFile() 方法。写一个多线程测试,验证 getInstance() 只构造一次。


下一篇预告:第41篇《函数模板与类模板:泛型编程的基石》------结束设计模式章节,进入模板与泛型编程。模板让代码与类型解耦,一个函数/类可以处理多种类型。下篇讲清楚函数模板和类模板的基本语法。

相关推荐
_日拱一卒8 小时前
LeetCode:114二叉树展开为链表
java·开发语言·算法
天天进步20158 小时前
从零打造 Python 全栈项目:智能教学辅助系统
开发语言·人工智能·python
一个不知名程序员www8 小时前
算法学习入门---算法题DAY1
c++·算法
kkeeper~8 小时前
0基础C语言积跬步之内存函数
c语言·开发语言
吃好睡好便好8 小时前
在Matlab中绘制杆状图
开发语言·学习·算法·matlab·信息可视化
桀人8 小时前
C++——内存管理——new和delete的超详细解析
开发语言·c++
Shadow(⊙o⊙)8 小时前
Shell进程替换,自定义Shell解释器——字符串库函数灵活操作!
linux·运维·服务器·开发语言·c++·学习
_F_y8 小时前
树形 DP 从入门到进阶:普通树形DP、树形背包、换根DP
c++·动态规划
数智工坊8 小时前
PyCharm 运行 Python 脚本总自动进 Test 模式?附 RT-DETRv2 依赖缺失终极排坑
开发语言·ide·人工智能·python·pycharm