跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 设计模式实战:工厂模式与单例模式深度应用

C++ 设计模式实战聚焦工厂模式与单例模式。内容涵盖简单工厂、工厂方法、抽象工厂三种层次,以及饿汉式、懒汉式单例实现。通过计算器、日志系统、跨平台 UI 等案例,演示对象创建分离、全局唯一实例管理及线程安全优化。总结核心原则、优缺点、常见陷阱及实战技巧,助力编写高内聚低耦合的 C++ 代码。

云间运维发布于 2026/2/21更新于 2026/5/3021 浏览
C++ 设计模式实战:工厂模式与单例模式深度应用

C++ 设计模式实战:工厂模式与单例模式的深度应用

17.1 本章学习目标与重点

  • 掌握工厂模式(简单工厂、工厂方法、抽象工厂)的核心设计思想、适用场景及 C++ 实现细节
  • 理解单例模式的两种核心实现(饿汉式、懒汉式)及线程安全优化方案
  • 能够结合实际开发场景(如组件创建、配置管理)灵活选择设计模式
  • 规避设计模式使用中的常见陷阱(如过度设计、线程安全问题)

重点: 工厂模式的层次演进逻辑、单例模式的线程安全实现、设计模式在项目中的落地技巧

17.2 设计模式基础认知

在 C++ 开发中,'设计模式'是前人总结的代码设计经验模板,用于解决特定场景下的代码复用、扩展性、维护性问题。设计模式不直接提供可运行的代码,而是提供一套抽象的设计思路,帮助我们编写'高内聚、低耦合'的优质代码。

17.2.1 为什么需要设计模式?

实际开发中,我们常遇到以下问题:

  • 新增功能时,需要修改大量现有代码(如新增产品类型时,到处修改 if-else 判断);
  • 代码耦合严重,一个模块的修改会影响多个其他模块;
  • 组件创建逻辑复杂,分散在代码各处,难以维护;
  • 多人协作时,代码风格不统一,扩展性差。

设计模式通过标准化的设计思路,将'变化的部分'与'不变的部分'分离,例如工厂模式负责统一管理对象创建,单例模式确保全局唯一实例,从而解决上述问题。

17.2.2 设计模式的核心原则

在学习具体模式前,需牢记以下核心原则(为后续模式应用奠定基础):

  1. 单一职责原则:一个类只负责一项功能(如工厂类只负责创建对象,不负责对象的业务逻辑);
  2. 开放 - 封闭原则:对扩展开放,对修改关闭(如新增产品时,无需修改工厂核心代码);
  3. 依赖倒置原则:依赖抽象,不依赖具体实现(如工厂依赖产品抽象类,而非具体产品类);
  4. 接口隔离原则:使用多个专门的接口,而非一个统一的接口。

17.3 工厂模式:对象创建的标准化解决方案

工厂模式是创建型设计模式的核心,其核心思想是'将对象的创建与使用分离',由专门的工厂类负责对象创建,使用者无需关心对象的创建细节(如构造函数参数、初始化流程)。

根据复杂度和扩展性,工厂模式分为三个层次:简单工厂模式、工厂方法模式、抽象工厂模式。

17.3.1 简单工厂模式:基础对象创建器
1. 核心定义

简单工厂模式(Simple Factory):定义一个工厂类,根据传入的参数,动态创建不同产品类的实例(所有产品都继承自同一个抽象基类)。

2. 适用场景
  • 产品类型较少(通常不超过 5 种);
  • 产品创建逻辑简单,且变化频率低;
  • 使用者无需关心产品创建细节,只需通过参数指定产品类型。
3. C++ 实现示例:计算器运算器

假设我们需要开发一个简单计算器,支持加法、减法、乘法、除法运算。若直接在主函数中创建运算对象,会导致 if-else 冗余且扩展性差,使用简单工厂模式可解决该问题。

步骤 1:定义产品抽象基类

首先定义所有运算的抽象基类 Operation,声明统一的接口 GetResult():

#include <iostream>
#include <string>
using namespace std;

// 产品抽象基类:运算
class Operation {
public:
    double numA; // 运算数 A
    double numB; // 运算数 B

    // 纯虚函数:计算结果(子类必须实现)
    virtual double GetResult() const = 0;

    // 虚析构函数:确保子类析构时正确释放资源
    virtual ~Operation() {}
};
步骤 2:实现具体产品类

为每种运算实现具体产品类,继承自 Operation 并实现 GetResult():

// 具体产品:加法运算
class OperationAdd : public Operation {
public:
    double GetResult() const override {
        return numA + numB;
    }
};

// 具体产品:减法运算
class OperationSub : public Operation {
public:
    double GetResult() const override {
        return numA - numB;
    }
};

// 具体产品:乘法运算
class OperationMul : public Operation {
public:
    double GetResult() const override {
        return numA * numB;
    }
};

// 具体产品:除法运算(增加除数不为 0 的校验)
class OperationDiv : public Operation {
public:
    double GetResult() const override {
        if (numB == 0) {
            throw runtime_error("除数不能为 0!");
        }
        return numA / numB;
    }
};
步骤 3:实现简单工厂类

创建工厂类 OperationFactory,提供静态方法 CreateOperation(),根据传入的运算符参数创建对应的运算对象:

// 简单工厂类:负责创建运算对象
class OperationFactory {
public:
    // 静态方法:根据运算符创建对应的运算对象
    static Operation* CreateOperation(char op) {
        Operation* operation = nullptr;
        switch (op) {
        case '+': operation = new OperationAdd(); break;
        case '-': operation = new OperationSub(); break;
        case '*': operation = new OperationMul(); break;
        case '/': operation = new OperationDiv(); break;
        default: throw invalid_argument("不支持的运算符!");
        }
        return operation;
    }
};
步骤 4:客户端使用示例

客户端只需调用工厂类的静态方法,传入参数即可获取对象,无需关心对象创建细节:

int main() {
    try {
        // 1. 通过工厂创建加法对象
        Operation* addOp = OperationFactory::CreateOperation('+');
        addOp->numA = 10;
        addOp->numB = 20;
        cout << "10 + 20 = " << addOp->GetResult() << endl; // 输出:30
        delete addOp; // 释放资源

        // 2. 通过工厂创建除法对象
        Operation* divOp = OperationFactory::CreateOperation('/');
        divOp->numA = 50;
        divOp->numB = 5;
        cout << "50 / 5 = " << divOp->GetResult() << endl; // 输出:10
        delete divOp;

        // 3. 测试除数为 0 的异常
        Operation* errDivOp = OperationFactory::CreateOperation('/');
        errDivOp->numA = 30;
        errDivOp->numB = 0;
        cout << "30 / 0 = " << errDivOp->GetResult() << endl;
        delete errDivOp;
    } catch (const exception& e) {
        cout << "错误:" << e.what() << endl; // 输出:除数不能为 0!
    }
    return 0;
}
4. 简单工厂模式的优缺点

✅ 优点:

  • 实现对象创建与使用分离,客户端代码简洁,无需关心创建细节;
  • 集中管理对象创建逻辑,便于维护。

⚠️ 缺点:

  • 违反'开放 - 封闭原则':新增产品时,需修改工厂类的 switch 逻辑;
  • 工厂类职责过重,若产品类型过多,会导致工厂类代码臃肿(如支持 10 种运算时,switch 会非常长)。
17.3.2 工厂方法模式:解决简单工厂的扩展性问题
1. 核心定义

工厂方法模式(Factory Method):定义一个创建对象的接口(抽象工厂),让子类决定实例化哪个产品类。即'将工厂的创建逻辑延迟到子类',每个产品对应一个专属工厂。

2. 适用场景
  • 产品类型较多,且未来可能持续新增;
  • 希望遵守'开放 - 封闭原则',新增产品时无需修改现有代码;
  • 产品创建逻辑复杂,不同产品的创建流程差异较大。
3. C++ 实现示例:日志系统

假设我们需要开发一个日志系统,支持文件日志(写入文件)、控制台日志(打印到控制台)、数据库日志(存入数据库),且未来可能新增'网络日志'。使用工厂方法模式,可实现无侵入式扩展。

步骤 1:定义产品抽象基类
#include <iostream>
#include <string>
#include <fstream>
using namespace std;

// 产品抽象基类:日志器
class Logger {
public:
    // 纯虚函数:写入日志
    virtual void WriteLog(const string& message) = 0;
    virtual ~Logger() {}
};
步骤 2:实现具体产品类
// 具体产品 1:控制台日志
class ConsoleLogger : public Logger {
public:
    void WriteLog(const string& message) override {
        cout << "[控制台日志] " << message << endl;
    }
};

// 具体产品 2:文件日志(写入 log.txt)
class FileLogger : public Logger {
public:
    void WriteLog(const string& message) override {
        ofstream logFile("log.txt", ios::app); // 追加模式写入文件
        if (logFile.is_open()) {
            logFile << "[文件日志] " << message << endl;
            logFile.close();
        } else {
            throw runtime_error("无法打开日志文件!");
        }
    }
};

// 具体产品 3:数据库日志(模拟写入数据库)
class DatabaseLogger : public Logger {
public:
    void WriteLog(const string& message) override {
        // 模拟数据库连接与写入
        cout << "[数据库日志] 连接数据库成功,写入日志:" << message << endl;
    }
};
步骤 3:定义抽象工厂基类

抽象工厂类声明创建产品的纯虚方法,由子类实现:

// 抽象工厂基类:日志工厂
class LoggerFactory {
public:
    // 纯虚函数:创建日志器对象
    virtual Logger* CreateLogger() = 0;
    virtual ~LoggerFactory() {}
};
步骤 4:实现具体工厂类

每个产品对应一个专属工厂,实现 CreateLogger() 方法:

// 具体工厂 1:控制台日志工厂
class ConsoleLoggerFactory : public LoggerFactory {
public:
    Logger* CreateLogger() override {
        return new ConsoleLogger();
    }
};

// 具体工厂 2:文件日志工厂
class FileLoggerFactory : public LoggerFactory {
public:
    Logger* CreateLogger() override {
        return new FileLogger();
    }
};

// 具体工厂 3:数据库日志工厂
class DatabaseLoggerFactory : public LoggerFactory {
public:
    Logger* CreateLogger() override {
        return new DatabaseLogger();
    }
};
步骤 5:客户端使用示例

客户端根据需求选择对应的工厂,再通过工厂创建产品:

int main() {
    try {
        // 1. 使用控制台日志
        LoggerFactory* consoleFactory = new ConsoleLoggerFactory();
        Logger* consoleLogger = consoleFactory->CreateLogger();
        consoleLogger->WriteLog("用户登录成功"); // 输出:[控制台日志] 用户登录成功
        delete consoleLogger;
        delete consoleFactory;

        // 2. 使用文件日志(会生成 log.txt 文件)
        LoggerFactory* fileFactory = new FileLoggerFactory();
        Logger* fileLogger = fileFactory->CreateLogger();
        fileLogger->WriteLog("系统启动完成"); // 写入 log.txt
        delete fileLogger;
        delete fileFactory;

        // 3. 使用数据库日志
        LoggerFactory* dbFactory = new DatabaseLoggerFactory();
        Logger* dbLogger = dbFactory->CreateLogger();
        dbLogger->WriteLog("数据备份成功"); // 输出:[数据库日志] 连接数据库成功...
        delete dbLogger;
        delete dbFactory;

        // 【扩展】新增网络日志时,只需添加 NetworkLogger 和 NetworkLoggerFactory,无需修改现有代码
    } catch (const exception& e) {
        cout << "日志写入失败:" << e.what() << endl;
    }
    return 0;
}
4. 工厂方法模式的优缺点

✅ 优点:

  • 完全遵守'开放 - 封闭原则':新增产品时,只需新增产品类和对应工厂类,无需修改现有代码;
  • 工厂职责单一,每个工厂只负责创建一种产品,代码结构清晰;
  • 扩展性强,支持不同产品的创建逻辑差异化。

⚠️ 缺点:

  • 类数量爆炸:每增加一个产品,需同时新增产品类和工厂类,增加代码复杂度;
  • 客户端需要知道具体工厂的存在(如 ConsoleLoggerFactory),增加了客户端的学习成本。
17.3.3 抽象工厂模式:多产品族的创建解决方案
1. 核心定义

抽象工厂模式(Abstract Factory):提供一个接口,用于创建一系列相关或相互依赖的对象(产品族),而无需指定它们的具体类。

2. 关键概念
  • 产品族:一组相关的产品(如'Windows 系统的按钮、文本框、下拉框'属于一个产品族,'Mac 系统的按钮、文本框、下拉框'属于另一个产品族);
  • 产品等级结构:同一类产品的不同实现(如'按钮'是一个产品等级结构,包含 Windows 按钮、Mac 按钮)。

简单来说,工厂方法模式解决'同一产品等级结构'的扩展问题,抽象工厂模式解决'同一产品族'的创建问题。

3. 适用场景
  • 系统需要支持多个产品族(如跨平台 UI 组件、不同数据库的驱动套件);
  • 产品族中的产品相互依赖,需要统一创建和管理;
  • 希望客户端能统一使用多个相关产品,而无需关心它们的具体实现。
4. C++ 实现示例:跨平台 UI 组件库

假设我们需要开发一个跨平台 UI 库,支持 Windows 和 Mac 两个平台,每个平台包含'按钮(Button)'和'文本框(TextBox)'两个组件(即两个产品族:Windows 产品族、Mac 产品族)。

步骤 1:定义产品族的抽象基类
#include <iostream>
#include <string>
using namespace std;

// 产品等级 1:按钮
class Button {
public:
    virtual void Display() = 0; // 显示按钮
    virtual ~Button() {}
};

// 产品等级 2:文本框
class TextBox {
public:
    virtual void Display() = 0; // 显示文本框
    virtual ~TextBox() {}
};
步骤 2:实现具体产品族(Windows 和 Mac)
// Windows 产品族
class WindowsButton : public Button {
public:
    void Display() override {
        cout << "显示 Windows 风格按钮(蓝色、矩形)" << endl;
    }
};
class WindowsTextBox : public TextBox {
public:
    void Display() override {
        cout << "显示 Windows 风格文本框(白色背景、黑色文字)" << endl;
    }
};

// Mac 产品族
class MacButton : public Button {
public:
    void Display() override {
        cout << "显示 Mac 风格按钮(灰色、圆角)" << endl;
    }
};
class MacTextBox : public TextBox {
public:
    void Display() override {
        cout << "显示 Mac 风格文本框(浅灰色背景、黑色文字)" << endl;
    }
};
步骤 3:定义抽象工厂基类(创建产品族)

抽象工厂类声明创建产品族中所有产品的纯虚方法:

// 抽象工厂:UI 组件工厂(创建一个产品族的所有产品)
class UIFactory {
public:
    virtual Button* CreateButton() = 0; // 创建按钮
    virtual TextBox* CreateTextBox() = 0; // 创建文本框
    virtual ~UIFactory() {}
};
步骤 4:实现具体工厂类(每个工厂对应一个产品族)
// 具体工厂 1:Windows UI 工厂(创建 Windows 产品族)
class WindowsUIFactory : public UIFactory {
public:
    Button* CreateButton() override { return new WindowsButton(); }
    TextBox* CreateTextBox() override { return new WindowsTextBox(); }
};

// 具体工厂 2:Mac UI 工厂(创建 Mac 产品族)
class MacUIFactory : public UIFactory {
public:
    Button* CreateButton() override { return new MacButton(); }
    TextBox* CreateTextBox() override { return new MacTextBox(); }
};
步骤 5:客户端使用示例(切换平台只需更换工厂)
// 客户端函数:根据传入的工厂,创建并显示 UI 组件
void ShowUI(UIFactory* factory) {
    Button* button = factory->CreateButton();
    TextBox* textBox = factory->CreateTextBox();
    button->Display();
    textBox->Display();
    delete button;
    delete textBox;
    delete factory;
}

int main() {
    cout << "=== Windows 平台 UI ===" << endl;
    ShowUI(new WindowsUIFactory()); // 使用 Windows 工厂
    cout << "\n=== Mac 平台 UI ===" << endl;
    ShowUI(new MacUIFactory()); // 使用 Mac 工厂

    // 【扩展】新增 Linux 平台时,只需添加 Linux 产品族(LinuxButton、LinuxTextBox)和 LinuxUIFactory,无需修改客户端代码
    return 0;
}

运行结果:

=== Windows 平台 UI ===
显示 Windows 风格按钮(蓝色、矩形)
显示 Windows 风格文本框(白色背景、黑色文字)
=== Mac 平台 UI ===
显示 Mac 风格按钮(灰色、圆角)
显示 Mac 风格文本框(浅灰色背景、黑色文字)
5. 抽象工厂模式的优缺点

✅ 优点:

  • 统一管理产品族的创建,确保产品族内的产品相互匹配(如 Windows 按钮和 Windows 文本框不会混用);
  • 支持产品族的整体切换(如从 Windows 平台切换到 Mac 平台,只需更换工厂);
  • 遵守'开放 - 封闭原则',新增产品族时无需修改现有代码。

⚠️ 缺点:

  • 扩展产品等级结构困难:若需在现有产品族中新增产品(如新增'下拉框'),需修改抽象工厂类及所有具体工厂类,违反'开放 - 封闭原则';
  • 类结构复杂,理解和维护成本较高。
17.3.4 三种工厂模式的选择策略
模式类型核心特点适用场景扩展方式
简单工厂模式一个工厂创建所有产品产品类型少、变化少修改工厂类(违反开放 - 封闭)
工厂方法模式一个产品对应一个工厂产品类型多、需单独扩展新增产品类 + 工厂类(无侵入)
抽象工厂模式一个工厂创建一个产品族需管理多个相关产品族新增产品族(无侵入)、扩展产品等级(困难)

💡 实战技巧: 大多数场景下,'工厂方法模式'是性价比最高的选择;若需管理相关产品族,再考虑'抽象工厂模式';'简单工厂模式'仅适用于小型、简单的项目。

17.4 单例模式:全局唯一实例的设计方案

单例模式(Singleton)是创建型设计模式的另一个核心,其核心思想是'确保一个类在整个系统中只有一个实例,并提供一个全局访问点'。

17.4.1 为什么需要单例模式?

实际开发中,以下场景需要确保对象唯一:

  • 配置管理类:系统的配置信息(如数据库连接参数、服务器地址)只需加载一次,全局共享;
  • 日志管理器:所有模块的日志都需写入同一个日志文件,避免多实例导致文件冲突;
  • 数据库连接池:连接池是稀缺资源,多实例会导致资源浪费和连接混乱;
  • 缓存管理器:全局缓存需统一读写,确保数据一致性。

若不使用单例模式,可能导致:

  • 资源浪费(如重复创建数据库连接);
  • 数据不一致(如多个配置实例加载不同的配置文件);
  • 并发冲突(如多个日志实例同时写入同一个文件)。
17.4.2 单例模式的核心要求
  1. 私有构造函数:禁止外部通过 new 创建实例;
  2. 私有拷贝构造函数和赋值运算符:禁止通过拷贝创建新实例;
  3. 静态私有实例:存储唯一的实例对象;
  4. 静态公有方法:提供全局访问点(如 GetInstance())。
17.4.3 饿汉式单例:预加载实例
1. 核心思想

饿汉式(Hungry Singleton):在程序启动时(类加载阶段)就创建实例,无论是否使用,实例都已存在。

2. C++ 实现示例:配置管理类
#include <iostream>
#include <string>
#include <map>
using namespace std;

// 饿汉式单例:配置管理类(加载系统配置)
class ConfigManager {
private:
    // 1. 私有构造函数:禁止外部创建实例
    ConfigManager() {
        // 模拟加载配置文件(程序启动时执行)
        LoadConfig();
        cout << "饿汉式单例:配置文件加载完成" << endl;
    }

    // 2. 私有拷贝构造函数和赋值运算符:禁止拷贝
    ConfigManager(const ConfigManager&) = delete;
    ConfigManager& operator=(const ConfigManager&) = delete;

    // 3. 静态私有实例:程序启动时初始化
    static ConfigManager instance;

    // 配置数据存储
    map<string, string> configMap;

    // 加载配置文件(模拟)
    void LoadConfig() {
        configMap["db_host"] = "127.0.0.1";
        configMap["db_port"] = "3306";
        configMap["db_user"] = "root";
        configMap["db_pwd"] = "123456";
    }

public:
    // 4. 静态公有方法:全局访问点
    static ConfigManager& GetInstance() {
        return instance;
    }

    // 获取配置值
    string GetConfig(const string& key) {
        auto iter = configMap.find(key);
        if (iter != configMap.end()) {
            return iter->second;
        }
        return "配置不存在";
    }
};

// 初始化静态实例(程序启动时执行,线程安全)
ConfigManager ConfigManager::instance;

// 客户端使用
int main() {
    // 第一次获取实例(实例已存在)
    ConfigManager& config1 = ConfigManager::GetInstance();
    cout << "数据库地址:" << config1.GetConfig("db_host") << endl;

    // 第二次获取实例(返回同一个实例)
    ConfigManager& config2 = ConfigManager::GetInstance();
    cout << "数据库端口:" << config2.GetConfig("db_port") << endl;

    // 验证是否为同一个实例(地址相同)
    cout << "config1 地址:" << &config1 << endl;
    cout << "config2 地址:" << &config2 << endl;
    return 0;
}

运行结果:

饿汉式单例:配置文件加载完成
数据库地址:127.0.0.1
数据库端口:3306
config1 地址:0x404068
config2 地址:0x404068
3. 饿汉式单例的优缺点

✅ 优点:

  • 实现简单,无需考虑线程安全问题(静态实例在程序启动时初始化,仅执行一次);
  • 访问速度快,实例已预加载,无需动态创建。

⚠️ 缺点:

  • 资源浪费:若程序全程未使用该实例,仍会占用内存;
  • 无法延迟初始化:若实例创建成本高(如加载大型配置文件),会延长程序启动时间。
17.4.4 懒汉式单例:延迟加载实例
1. 核心思想

懒汉式(Lazy Singleton):在第一次调用 GetInstance() 时才创建实例,实现'延迟加载',节省内存。

2. 线程安全问题分析

naive 的懒汉式实现(非线程安全):

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}
public:
    static Singleton* GetInstance() {
        if (instance == nullptr) {
            instance = new Singleton(); // 多线程下可能创建多个实例
        }
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;

问题:多线程环境下,若两个线程同时进入 if (instance == nullptr),会导致创建多个实例,违反单例原则。

3. 线程安全的懒汉式实现(C++11 推荐方案)

C++11 标准规定:局部静态变量的初始化是线程安全的(在初始化完成前,其他线程会阻塞)。利用这一特性,可实现简洁、高效的线程安全懒汉式单例。

实现示例:日志管理器
#include <iostream>
#include <string>
#include <fstream>
#include <mutex>
#include <thread>
using namespace std;

// 线程安全的懒汉式单例:日志管理器
class LogManager {
private:
    // 私有构造函数(延迟初始化)
    LogManager() {
        cout << "懒汉式单例:日志管理器初始化" << endl;
    }

    // 禁止拷贝
    LogManager(const LogManager&) = delete;
    LogManager& operator=(const LogManager&) = delete;

    // 日志文件路径
    string logFilePath = "app.log";

public:
    // 静态公有方法:全局访问点(C++11 线程安全)
    static LogManager& GetInstance() {
        static LogManager instance; // 局部静态变量,第一次调用时初始化(线程安全)
        return instance;
    }

    // 写入日志
    void WriteLog(const string& level, const string& message) {
        ofstream logFile(logFilePath, ios::app);
        if (logFile.is_open()) {
            logFile << "[" << level << "] " << message << endl;
            logFile.close();
        } else {
            throw runtime_error("日志文件打开失败!");
        }
    }
};

// 多线程测试(验证线程安全)
void TestThread(int threadId) {
    string message = "线程" + to_string(threadId) + "执行日志";
    LogManager::GetInstance().WriteLog("INFO", message);
}

int main() {
    cout << "程序启动,未创建日志实例" << endl;

    // 第一次调用:创建实例
    LogManager::GetInstance().WriteLog("INFO", "系统启动成功");

    // 多线程测试(10 个线程同时调用)
    thread threads[10];
    for (int i = 0; i < 10; ++i) {
        threads[i] = thread(TestThread, i);
    }
    for (int i = 0; i < 10; ++i) {
        threads[i].join();
    }

    cout << "日志写入完成,查看 app.log 文件" << endl;
    return 0;
}

运行结果:

程序启动,未创建日志实例
懒汉式单例:日志管理器初始化
日志写入完成,查看 app.log 文件

app.log 文件内容:

[INFO] 系统启动成功
[INFO] 线程 0 执行日志
[INFO] 线程 1 执行日志
...
[INFO] 线程 9 执行日志
4. 懒汉式单例的优缺点

✅ 优点:

  • 延迟加载:仅在需要时创建实例,节省内存;
  • 线程安全(C++11 实现):无需手动加锁,依赖标准库保证安全性;
  • 实现简洁,无内存泄漏风险(局部静态变量会自动析构)。

⚠️ 缺点:

  • C++11 之前的版本不支持局部静态变量的线程安全,需手动加锁(如使用 std::mutex),但会引入性能开销;
  • 第一次访问时会有初始化开销(影响首次访问速度)。
17.4.5 单例模式的常见陷阱与避坑指南
1. 陷阱 1:拷贝与赋值导致多实例

若未禁用拷贝构造函数和赋值运算符,可能通过以下方式创建新实例:

ConfigManager config3 = ConfigManager::GetInstance(); // 调用拷贝构造函数

✅ 避坑方案: 显式删除拷贝构造函数和赋值运算符(C++11 及以上):

ConfigManager(const ConfigManager&) = delete;
ConfigManager& operator=(const ConfigManager&) = delete;
2. 陷阱 2:多线程环境下的线程安全问题

naive 懒汉式在多线程下会创建多个实例,即使手动加锁,也可能存在'双重检查锁定'的坑(如指令重排导致的空指针)。

✅ 避坑方案: 优先使用 C++11 局部静态变量的实现方式,无需手动处理线程安全。

3. 陷阱 3:析构顺序问题

若单例对象依赖其他全局对象,可能出现'单例已析构,但其他对象仍在使用它'的情况。

✅ 避坑方案:

  • 尽量避免单例依赖全局对象;
  • 若必须依赖,确保依赖对象的析构顺序在单例之后(可通过局部静态变量的析构顺序保证)。
4. 陷阱 4:过度使用单例模式

将单例模式当作'全局变量'滥用,会导致:

  • 代码耦合度高(所有模块依赖单例);
  • 测试困难(单例状态难以模拟和重置)。

✅ 避坑方案: 仅在确实需要'全局唯一实例'的场景下使用单例(如配置、日志、连接池),避免将单例作为全局变量的替代品。

17.5 实战案例:结合工厂模式与单例模式开发配置化日志系统

17.5.1 需求分析

开发一个支持多日志类型(控制台、文件、数据库)且可配置的日志系统,要求:

  1. 日志类型可通过配置文件动态指定(如配置为'文件日志',则系统自动使用文件日志);
  2. 日志系统全局唯一(单例模式),避免多实例冲突;
  3. 支持日志类型的扩展(如未来新增'网络日志',无需修改核心代码)。
17.5.2 设计思路
  • 单例模式:日志系统核心管理器(LogSystem)为单例,确保全局唯一;
  • 工厂方法模式:创建日志器(控制台、文件、数据库),支持扩展;
  • 配置驱动:通过单例 ConfigManager 加载日志类型配置,由 LogSystem 自动选择对应的工厂创建日志器。
17.5.3 完整实现代码
#include <iostream>
#include <string>
#include <fstream>
#include <map>
#include <thread>
using namespace std;

// -------------------------- 步骤 1:单例配置管理器 --------------------------
class ConfigManager {
private:
    ConfigManager() { LoadConfig(); }
    ConfigManager(const ConfigManager&) = delete;
    ConfigManager& operator=(const ConfigManager&) = delete;
    static ConfigManager& GetInstance() {
        static ConfigManager instance;
        return instance;
    }
    map<string, string> configMap;
    void LoadConfig() {
        // 模拟加载配置文件:日志类型配置为 "file"(支持 "console"、"db"、"file")
        configMap["log_type"] = "file";
        configMap["log_file"] = "system.log";
    }

public:
    // 提供全局访问接口,获取配置值
    static string GetConfig(const string& key) {
        return GetInstance().configMap[key];
    }
};

// -------------------------- 步骤 2:工厂模式 - 日志器与工厂 --------------------------
// 日志器抽象基类
class Logger {
public:
    virtual void WriteLog(const string& level, const string& message) = 0;
    virtual ~Logger() {}
};

// 具体日志器:控制台日志
class ConsoleLogger : public Logger {
public:
    void WriteLog(const string& level, const string& message) override {
        cout << "[" << level << "] [控制台] " << message << endl;
    }
};

// 具体日志器:文件日志
class FileLogger : public Logger {
private:
    string logFile;
public:
    FileLogger(const string& filePath) : logFile(filePath) {}
    void WriteLog(const string& level, const string& message) override {
        ofstream file(logFile, ios::app);
        if (file.is_open()) {
            file << "[" << level << "] [文件] " << message << endl;
            file.close();
        } else {
            throw runtime_error("无法打开日志文件:" + logFile);
        }
    }
};

// 具体日志器:数据库日志
class DatabaseLogger : public Logger {
public:
    void WriteLog(const string& level, const string& message) override {
        cout << "[" << level << "] [数据库] " << message << endl;
    }
};

// 日志工厂抽象基类
class LoggerFactory {
public:
    virtual Logger* CreateLogger() = 0;
    virtual ~LoggerFactory() {}
};

// 具体工厂:控制台日志工厂
class ConsoleLoggerFactory : public LoggerFactory {
public:
    Logger* CreateLogger() override { return new ConsoleLogger(); }
};

// 具体工厂:文件日志工厂(从配置获取文件路径)
class FileLoggerFactory : public LoggerFactory {
public:
    Logger* CreateLogger() override {
        string logFile = ConfigManager::GetConfig("log_file");
        return new FileLogger(logFile);
    }
};

// 具体工厂:数据库日志工厂
class DatabaseLoggerFactory : public LoggerFactory {
public:
    Logger* CreateLogger() override { return new DatabaseLogger(); }
};

// -------------------------- 步骤 3:单例日志系统核心 --------------------------
class LogSystem {
private:
    LogSystem() {
        // 从配置获取日志类型,创建对应的工厂和日志器
        string logType = ConfigManager::GetConfig("log_type");
        factory = CreateLoggerFactory(logType);
        logger = factory->CreateLogger();
        cout << "日志系统初始化完成,当前日志类型:" << logType << endl;
    }
    LogSystem(const LogSystem&) = delete;
    LogSystem& operator=(const LogSystem&) = delete;

    // 根据日志类型创建工厂
    LoggerFactory* CreateLoggerFactory(const string& logType) {
        if (logType == "console") {
            return new ConsoleLoggerFactory();
        } else if (logType == "file") {
            return new FileLoggerFactory();
        } else if (logType == "db") {
            return new DatabaseLoggerFactory();
        } else {
            throw invalid_argument("不支持的日志类型:" + logType);
        }
    }

    static LogSystem& GetInstance() {
        static LogSystem instance;
        return instance;
    }

    LoggerFactory* factory;
    Logger* logger;

public:
    // 全局日志写入接口
    static void Log(const string& level, const string& message) {
        GetInstance().logger->WriteLog(level, message);
    }

    // 析构时释放资源
    ~LogSystem() {
        delete logger;
        delete factory;
    }
};

// -------------------------- 客户端测试 --------------------------
void TestLog(int moduleId) {
    string message = "模块" + to_string(moduleId) + "执行完成";
    LogSystem::Log("INFO", message);
}

int main() {
    try {
        // 1. 单线程测试
        LogSystem::Log("DEBUG", "系统启动中...");
        LogSystem::Log("INFO", "系统启动成功");
        LogSystem::Log("WARN", "内存使用率过高(80%)");
        LogSystem::Log("ERROR", "数据库连接超时");

        // 2. 多线程测试(10 个模块同时写入日志)
        thread modules[10];
        for (int i = 0; i < 10; ++i) {
            modules[i] = thread(TestLog, i);
        }
        for (int i = 0; i < 10; ++i) {
            modules[i].join();
        }
        cout << "日志写入完成,查看 system.log 文件" << endl;
    } catch (const exception& e) {
        cout << "日志系统错误:" << e.what() << endl;
    }
    return 0;
}
17.5.4 代码说明与运行效果
1. 核心设计亮点
  • 单例 + 工厂结合:LogSystem 是单例,确保全局唯一;内部通过工厂模式创建日志器,支持扩展;
  • 配置驱动:日志类型由 ConfigManager 配置,修改配置即可切换日志类型(如将 log_type 改为 'console' 则切换为控制台日志);
  • 线程安全:LogSystem 和 ConfigManager 均使用 C++11 局部静态变量实现单例,支持多线程安全访问。
2. 运行结果
日志系统初始化完成,当前日志类型:file
[DEBUG] [文件] 系统启动中...
[INFO] [文件] 系统启动成功
[WARN] [文件] 内存使用率过高(80%)
[ERROR] [文件] 数据库连接超时
日志写入完成,查看 system.log 文件

system.log 文件内容:

[DEBUG] [文件] 系统启动中...
[INFO] [文件] 系统启动成功
[WARN] [文件] 内存使用率过高(80%)
[ERROR] [文件] 数据库连接超时
[INFO] [文件] 模块 0 执行完成
[INFO] [文件] 模块 1 执行完成
...
[INFO] [文件] 模块 9 执行完成
3. 扩展演示(新增网络日志)

若需新增'网络日志',只需添加以下代码,无需修改现有核心代码:

// 新增具体日志器:网络日志
class NetworkLogger : public Logger {
public:
    void WriteLog(const string& level, const string& message) override {
        cout << "[" << level << "] [网络] 发送日志到服务器:" << message << endl;
    }
};

// 新增具体工厂:网络日志工厂
class NetworkLoggerFactory : public LoggerFactory {
public:
    Logger* CreateLogger() override { return new NetworkLogger(); }
};

// 修改配置文件(或 ConfigManager 的 LoadConfig 方法)
configMap["log_type"] = "network";

// 在 LogSystem::CreateLoggerFactory 中添加分支
else if (logType == "network") {
    return new NetworkLoggerFactory();
}

17.6 本章总结

本章重点讲解了 C++ 开发中最常用的两种设计模式:工厂模式和单例模式,核心要点总结如下:

  1. 工厂模式分为三个层次,需根据产品数量和扩展性需求选择:
    • 简单工厂:适用于产品少、变化少的场景,实现简单但扩展性差;
    • 工厂方法:适用于产品多、需灵活扩展的场景,遵守开放 - 封闭原则;
    • 抽象工厂:适用于管理多个相关产品族的场景,支持产品族整体切换。
  2. 单例模式的两种核心实现:
    • 饿汉式:预加载实例,线程安全,适合实例创建成本低的场景;
    • 懒汉式(C++11):延迟加载,线程安全,适合实例创建成本高的场景。
  3. 设计模式的核心价值是'解决特定场景下的代码问题',而非'炫技'。实际开发中,需避免过度设计,优先选择简单、易维护的方案。
  4. 实战技巧:工厂模式用于'对象创建的标准化',单例模式用于'全局唯一实例的管理',两者可结合使用(如单例的核心系统 + 工厂的组件创建),提升代码的扩展性和维护性。

通过本章学习,你应能在实际项目中灵活运用工厂模式和单例模式,解决对象创建和全局实例管理的问题,编写高内聚、低耦合的优质 C++ 代码。

目录

  1. C++ 设计模式实战:工厂模式与单例模式的深度应用
  2. 17.1 本章学习目标与重点
  3. 17.2 设计模式基础认知
  4. 17.2.1 为什么需要设计模式?
  5. 17.2.2 设计模式的核心原则
  6. 17.3 工厂模式:对象创建的标准化解决方案
  7. 17.3.1 简单工厂模式:基础对象创建器
  8. 1. 核心定义
  9. 2. 适用场景
  10. 3. C++ 实现示例:计算器运算器
  11. 步骤 1:定义产品抽象基类
  12. 步骤 2:实现具体产品类
  13. 步骤 3:实现简单工厂类
  14. 步骤 4:客户端使用示例
  15. 4. 简单工厂模式的优缺点
  16. 17.3.2 工厂方法模式:解决简单工厂的扩展性问题
  17. 1. 核心定义
  18. 2. 适用场景
  19. 3. C++ 实现示例:日志系统
  20. 步骤 1:定义产品抽象基类
  21. 步骤 2:实现具体产品类
  22. 步骤 3:定义抽象工厂基类
  23. 步骤 4:实现具体工厂类
  24. 步骤 5:客户端使用示例
  25. 4. 工厂方法模式的优缺点
  26. 17.3.3 抽象工厂模式:多产品族的创建解决方案
  27. 1. 核心定义
  28. 2. 关键概念
  29. 3. 适用场景
  30. 4. C++ 实现示例:跨平台 UI 组件库
  31. 步骤 1:定义产品族的抽象基类
  32. 步骤 2:实现具体产品族(Windows 和 Mac)
  33. 步骤 3:定义抽象工厂基类(创建产品族)
  34. 步骤 4:实现具体工厂类(每个工厂对应一个产品族)
  35. 步骤 5:客户端使用示例(切换平台只需更换工厂)
  36. 5. 抽象工厂模式的优缺点
  37. 17.3.4 三种工厂模式的选择策略
  38. 17.4 单例模式:全局唯一实例的设计方案
  39. 17.4.1 为什么需要单例模式?
  40. 17.4.2 单例模式的核心要求
  41. 17.4.3 饿汉式单例:预加载实例
  42. 1. 核心思想
  43. 2. C++ 实现示例:配置管理类
  44. 3. 饿汉式单例的优缺点
  45. 17.4.4 懒汉式单例:延迟加载实例
  46. 1. 核心思想
  47. 2. 线程安全问题分析
  48. 3. 线程安全的懒汉式实现(C++11 推荐方案)
  49. 实现示例:日志管理器
  50. 4. 懒汉式单例的优缺点
  51. 17.4.5 单例模式的常见陷阱与避坑指南
  52. 1. 陷阱 1:拷贝与赋值导致多实例
  53. 2. 陷阱 2:多线程环境下的线程安全问题
  54. 3. 陷阱 3:析构顺序问题
  55. 4. 陷阱 4:过度使用单例模式
  56. 17.5 实战案例:结合工厂模式与单例模式开发配置化日志系统
  57. 17.5.1 需求分析
  58. 17.5.2 设计思路
  59. 17.5.3 完整实现代码
  60. 17.5.4 代码说明与运行效果
  61. 1. 核心设计亮点
  62. 2. 运行结果
  63. 3. 扩展演示(新增网络日志)
  64. 17.6 本章总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Deep-Live-Cam 模型配置指南:GFPGAN 与 inswapper 安装步骤
  • 哈希表概念、冲突解决与 C++ 实现
  • Faiss 数据结构与索引类型详解
  • OpenClaw 对接飞书机器人配置踩坑:消息不回与 Gateway 断开排查
  • 使用 Anthropic Skill 提升大模型前端设计审美
  • DeepSeek 团队架构分析:清北应届生主导大模型研发
  • 小程序 Web 访问慢?SSL 与 TLS 版本优化实战指南
  • C++ 入门:命名空间(namespace)详解
  • AIGC Bar API 站接入与工程化最佳实践
  • Python 数据可视化入门教程
  • 昇腾平台 DeepSeek-R1 与 Qwen2.5 强化学习训练优化实践
  • AVL 树原理与 C++ 实现:构建自平衡二叉搜索树
  • Windows 系统安装鸿蒙模拟器
  • Python 使用 json-repair 修复大模型返回的非法 JSON
  • AI 编程工具选型:Copilot、Cursor、Codex 核心差异
  • AI 大模型技术入门与全栈开发实战指南
  • Windows 系统安装 Python 的最佳方式:推荐 uv 管理而非传统安装
  • AI 编程工具选型:Copilot、Cursor、Codex 核心差异
  • JBoss Seam 实战:使用 JBoss Tools 构建首个 CRUD 项目
  • ClawdBot 本地部署实战:vLLM 后端与设备授权全链路解析

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online