欢迎光临
我们一直在努力

【C++】一篇文章学会最常用的单例设计模式及其使用场景与常见问题

单例模式

单例模式是一种创建型设计模式,核心目标是:保证一个类在程序生命周期内只有一个实例,并提供一个全局访问点。

常见的使用场景:配置管理器、日志管理器、数据库连接池等(这类对象只需要一个实例,多实例会导致资源浪费或逻辑冲突)。

实现要点

  • 私有化构造函数 / 拷贝构造 / 赋值运算符,防止外部创建

  • 提供静态方法获取唯一实例

  • 线程安全(C++11 后静态局部变量天生线程安全)

    • 函数内的静态局部变量初始化时,若多个线程同时进入初始化语句,只有一个线程完成初始化,其他线程阻塞等待初始化完成。

      因此 static Singleton UniqueInstance; 是天然线程安全的,无需手动加锁(如 std::mutex)

#include <iostream>
#include <mutex>
class Singleton {
private:
// 私有化构造函数,禁止外部创建
Singleton() {
std::cout << "Singleton 实例创建" << std::endl;
}
// 传统懒汉式(new出的单例,因为 new 强制要求生命周期由程序员管理
// 要手动delete)退出时不会调用虚构函数,需要手动释放资源,否则会造成内存泄漏
// ~Singleton() {
// std::cout << "Release Resources!" << std::endl;
// };
// 禁用拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;

public:
// 全局访问点
static Singleton& getInstance() {
// C++11起,静态局部变量初始化是线程安全的
static Singleton instance;
return instance;
}
// 示例方法
void doSomething() {
std::cout << "单例对象执行操作" << std::endl;
}

};
// 测试
int main() {
// 获取唯一实例
Singleton& s1 = Singleton::getInstance();
Singleton& s2 = Singleton::getInstance();
// 验证是同一个实例
std::cout << "&s1: " << &s1 << std::endl;
std::cout << "&s2: " << &s2 << std::endl;
s1.doSomething();
return 0;
}


场景题

场景1:配置管理器的单例实现

需求描述

开发一个应用配置管理器(ConfigManager),要求:

  • 整个程序中只能有一个配置管理器实例;
  • 支持加载本地配置文件(如 config.json);
  • 提供全局访问接口获取配置项(如数据库地址、端口、日志级别);
  • 配置加载完成后不允许外部修改配置内容。
  • 实现代码

    #include <iostream>
    #include <string>
    #include <unordered_map>
    #include <fstream>
    #include <nlohmann/json.hpp> // 需安装nlohmann/json库:https://github.com/nlohmann/json

    // 配置管理器单例类
    class ConfigManager {
    private:
    // 私有化构造函数,在构造时加载配置文件
    ConfigManager(const std::string& configPath) {
    loadConfig(configPath);
    std::cout << "配置管理器实例创建,加载配置文件:" << configPath << std::endl;
    }

    // 禁用拷贝、移动语义
    ConfigManager(const ConfigManager&) = delete;
    ConfigManager& operator=(const ConfigManager&) = delete;
    ConfigManager(ConfigManager&&) = delete;
    ConfigManager& operator=(ConfigManager&&) = delete;

    // 存储配置项的容器
    std::unordered_map<std::string, std::string> configMap;

    // 加载配置文件的私有方法
    void loadConfig(const std::string& configPath) {
    std::ifstream file(configPath);
    if (!file.is_open()) {
    throw std::runtime_error("配置文件打开失败:" + configPath);
    }

    nlohmann::json jsonConfig;
    file >> jsonConfig;

    // 将JSON配置转换为键值对存储
    for (auto& [key, value] : jsonConfig.items()) {
    configMap[key] = value.get<std::string>();
    }
    }

    public:
    // 全局访问点:懒加载+线程安全
    static ConfigManager& getInstance(const std::string& configPath = "config.json") {
    static ConfigManager instance(configPath);
    return instance;
    }

    // 获取配置项(只读)
    std::string getConfig(const std::string& key) const {
    auto it = configMap.find(key);
    if (it == configMap.end()) {
    throw std::invalid_argument("配置项不存在:" + key);
    }
    return it->second;
    }
    };

    // 测试代码
    int main() {
    try {
    // 获取配置管理器实例并读取配置
    ConfigManager& config = ConfigManager::getInstance("config.json");
    std::cout << "数据库地址:" << config.getConfig("db_host") << std::endl;
    std::cout << "数据库端口:" << config.getConfig("db_port") << std::endl;
    std::cout << "日志级别:" << config.getConfig("log_level") << std::endl;

    // 验证单例唯一性
    ConfigManager& config2 = ConfigManager::getInstance();
    std::cout << "\\n实例地址验证:" << std::endl;
    std::cout << "&config: " << &config << std::endl;
    std::cout << "&config2: " << &config2 << std::endl;

    } catch (const std::exception& e) {
    std::cerr << "错误:" << e.what() << std::endl;
    return 1;
    }
    return 0;
    }

    配置文件示例(config.json)

    {
    "db_host": "127.0.0.1",
    "db_port": "3306",
    "log_level": "INFO",
    "max_connection": "100"
    }

    实现说明
  • 单例核心:通过私有化构造函数+静态局部变量实现懒加载,C++11保证线程安全;
  • 配置只读:仅提供getConfig方法读取配置,无修改接口,避免配置被意外篡改;
  • 异常处理:加载配置文件、获取不存在的配置项时抛出异常,增强程序健壮性;
  • 依赖说明:需引入nlohmann/json库解析JSON配置,可通过vcpkg install nlohmann-json或手动下载头文件集成。

  • 场景2:单例模式的常见面试题

    问题1:饿汉式 vs 懒汉式单例,各自的优缺点?
    实现方式核心特点优点缺点
    饿汉式 程序启动时(静态成员初始化阶段)创建实例 1. 天生线程安全;2. 实现简单 1. 提前占用内存;2. 若实例依赖其他未初始化的资源,可能导致初始化失败
    懒汉式 第一次调用getInstance时创建实例 1. 懒加载,节约内存;2. 实例化时机可控 1. C++11前需手动加锁保证线程安全;2. 可能存在竞态条件(C++11已解决)

    饿汉式实现示例:

    class HungrySingleton {
    private:
    // 程序启动时初始化,全局唯一
    static HungrySingleton instance;

    HungrySingleton() = default;
    HungrySingleton(const HungrySingleton&) = delete;
    HungrySingleton& operator=(const HungrySingleton&) = delete;

    public:
    static HungrySingleton& getInstance() {
    return instance;
    }
    };
    // 类外初始化静态成员
    HungrySingleton HungrySingleton::instance;

    问题2:如何解决单例模式的“内存泄漏”问题?

    传统懒汉式若用new创建实例(如下),实例不会自动析构,易导致内存泄漏:

    // 有内存泄漏风险的懒汉式
    static Singleton* getInstance() {
    static Singleton* instance = new Singleton(); // 不会自动delete
    return instance;
    }

    解决方案:

  • 推荐方案:使用静态局部对象(如前文示例),程序退出时自动调用析构函数;
  • 手动释放:提供destroyInstance方法,在程序结束前手动释放;
  • 智能指针:使用std::unique_ptr管理实例生命周期(C++11+)。
  • 问题3:单例模式的局限性?
  • 违反单一职责:单例类既负责自身业务逻辑,又负责实例管理;
  • 扩展性差:若后续需要多个实例(如多数据库连接池),需大幅修改代码;
  • 测试困难:单例依赖全局状态,单元测试时难以隔离;
  • 并发问题:C++11前的懒汉式若未正确加锁,会导致多线程创建多个实例。
  • 总结

  • 单例模式的核心是私有化构造+静态全局访问点,C++11后静态局部变量实现的懒汉式是最优解(线程安全+懒加载);
  • 单例适用于“全局唯一、资源密集”的场景(配置/日志/连接池),但需注意其扩展性和测试性问题;
  • 实际开发中优先选择懒汉式(静态局部变量),避免饿汉式的提前初始化和传统懒汉式的线程安全问题。
  • 赞(0)
    未经允许不得转载:171主机测评 » 【C++】一篇文章学会最常用的单例设计模式及其使用场景与常见问题
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址