17.1 本章学习目标与重点
- 掌握工厂模式三层次的核心思想、适用场景及 C++ 实现细节
- 理解单例模式的饿汉式/懒汉式实现及线程安全方案
- 能够在实际项目中(如组件创建、配置管理)灵活选择模式
- 规避常见陷阱(过度设计、线程安全隐患、资源泄漏等)
17.2 设计模式基础认知
设计模式不是'代码模板',而是**'变化点隔离 + 职责单一 + 接口依赖抽象'**的设计思想集合。
(真实案例):
本文详解 C++ 设计模式实践,涵盖工厂模式(简单工厂、工厂方法、抽象工厂)、单例模式(饿汉式、懒汉式及 Meyers 写法)和策略模式。结合 Qt 项目场景,提供现代 C++(C++17/20)代码示例,包括动态加载插件系统实现、跨平台配置及常见陷阱避坑指南。旨在帮助开发者在组件创建、全局管理及行为切换中灵活应用设计模式,提升代码扩展性与可维护性。
设计模式不是'代码模板',而是**'变化点隔离 + 职责单一 + 接口依赖抽象'**的设计思想集合。
(真实案例):
| 场景 | 常用模式 | Qt 典型实现 | 典型类/组件 |
|---|---|---|---|
| 动态创建控件/组件 | 工厂模式 | QPluginLoader、自定义工厂 | QAbstractItemModel、QWidget 子类 |
| 全局配置/日志/连接池 | 单例模式 | 局部静态变量 + QSettings | QSettings、QApplication |
| 不同支付/渲染/日志方式 | 策略模式 | 信号槽 + 接口类 | QAbstractSocket、QStyle |
| 撤销/重做操作 | 命令模式 | QUndoStack | QUndoCommand |
| 动态扩展功能 | 插件/抽象工厂 | QPluginLoader + Q_DECLARE_INTERFACE | Qt 插件系统 |
适用场景(Qt 项目中常见):
Qt 真实案例:创建不同类型的传感器(电压、电流、温度)
代码实现(现代 C++ 写法):
// sensorfactory.h
#pragma once
#include <memory>
#include <string>
#include <stdexcept>
class Sensor {
public:
virtual ~Sensor() = default;
virtual double readValue() const = 0;
virtual std::string type() const = 0;
};
class VoltageSensor : public Sensor {
public:
double readValue() const override { return 3.3 + (rand() % 10) / 10.0; }
std::string type() const override { return "电压"; }
};
class CurrentSensor : public Sensor {
public:
double readValue() const override { return 0.5 + (rand() % 5) / 10.0; }
std::string type() const override { return "电流"; }
};
class TemperatureSensor : public Sensor {
public:
double readValue() const override { return 25.0 + (rand() % 20); }
std::string type() const override { return "温度"; }
};
class SensorFactory {
public:
static std::unique_ptr<Sensor> create(const std::string& type) {
if (type == "voltage") return std::make_unique<VoltageSensor>();
if (type == "current") return std::make_unique<CurrentSensor>();
if (type == "temperature") return std::make_unique<TemperatureSensor>();
throw std::invalid_argument("不支持的传感器类型:" + type);
}
};
使用方式(Qt 主窗口):
auto sensor = SensorFactory::create("temperature");
ui->label->setText(QString("%1: %2 °C").arg(sensor->type().c_str()).arg(sensor->readValue()));
优缺点总结:
| 优点 | 缺点 |
|---|---|
| 代码简单、易上手 | 新增传感器类型需修改工厂类(违反开闭原则) |
| 集中管理创建逻辑 | 工厂类职责膨胀 |
核心思想:每个产品对应一个专属工厂,新增产品时只加一对(产品类 + 工厂类),不改现有代码。
Qt 项目真实案例:不同平台 UI 组件工厂(Windows / Mac / Linux)
代码结构:
// uifactory.h
#pragma once
#include <memory>
class Button {
public:
virtual ~Button() = default;
virtual void render() const = 0;
};
class WindowsButton : public Button {
public:
void render() const override { qDebug() << "Windows 风格按钮"; }
};
class MacButton : public Button {
public:
void render() const override { qDebug() << "Mac 风格圆角按钮"; }
};
// 抽象工厂
class UIFactory {
public:
virtual std::unique_ptr<Button> createButton() const = 0;
virtual ~UIFactory() = default;
};
class WindowsUIFactory : public UIFactory {
public:
std::unique_ptr<Button> createButton() const override {
return std::make_unique<WindowsButton>();
}
};
class MacUIFactory : public UIFactory {
public:
std::unique_ptr<Button> createButton() const override {
return std::make_unique<MacButton>();
}
};
使用(配置驱动):
std::unique_ptr<UIFactory> factory;
if (platform == "windows") {
factory = std::make_unique<WindowsUIFactory>();
} else {
factory = std::make_unique<MacUIFactory>();
}
auto btn = factory->createButton();
btn->render();
优点:完全符合开闭原则,新增平台只需新增工厂类,无需改动现有代码。
适用场景:需要同时创建一组相关对象(如 UI 组件族:按钮 + 文本框 + 窗口)
Qt 项目真实案例:跨平台 UI 组件族
完整代码(简版):
class AbstractUI {
public:
virtual std::unique_ptr<Button> createButton() const = 0;
virtual std::unique_ptr<TextBox> createTextBox() const = 0;
virtual ~AbstractUI() = default;
};
class WindowsUI : public AbstractUI {
public:
std::unique_ptr<Button> createButton() const override {
return std::make_unique<WindowsButton>();
}
std::unique_ptr<TextBox> createTextBox() const override {
return std::make_unique<WindowsTextBox>();
}
};
推荐写法(C++11 后最优):局部静态变量(Meyers Singleton)
class Logger {
private:
Logger() { qDebug() << "Logger 初始化"; }
~Logger() { qDebug() << "Logger 销毁"; }
public:
static Logger& instance() {
static Logger inst; // C++11 保证线程安全 + 延迟初始化
return inst;
}
void info(const QString& msg) {
qDebug() << "[INFO]" << msg;
}
};
使用:
Logger::instance().info("系统启动");
为什么这是最优写法:
其他写法对比:
| 写法 | 线程安全 | 延迟初始化 | 代码复杂度 | 推荐指数 |
|---|---|---|---|---|
| 饿汉式 | 是 | 否 | 低 | ★★☆☆☆ |
| 局部静态(Meyers) | 是 | 是 | 极低 | ★★★★★ |
| 双检锁(DCL) | 有隐患 | 是 | 高 | 不推荐 |
| std::call_once | 是 | 是 | 中 | ★★★☆☆ |
项目名称:QtDesignPatternsDemo
核心结构:
mainwindow.cpp(综合使用):
Logger::instance().setStrategy(LoggerFactory::create("file"));
Logger::instance().log("INFO", "系统启动成功");
| 项目阶段 | 推荐模式组合 | 为什么推荐 |
|---|---|---|
| 小型工具 | 单例 + 简单工厂 | 实现最快 |
| 中型桌面应用 | 工厂方法 + 策略 + 单例 | 扩展性好,代码清晰 |
| 大型框架/插件 | 抽象工厂 + 插件 + 单例 | 支持跨平台、动态加载 |
| 高性能后台 | 局部静态单例 + CRTP 静态多态 | 零运行时开销 |
Qt 插件本质上是动态链接库(.dll / .so / .dylib),但必须满足以下条件:
Q_PLUGIN_METADATA(IID "...") 宏声明一个唯一的接口标识符(IID)QPluginLoader::instance() 获取插件对象qobject_cast<YourInterface*>(plugin) 转换为自定义接口类型最关键的两个宏(必须成对出现):
#define MyPluginInterface_iid "org.example.MyPluginInterface/1.0"
Q_DECLARE_INTERFACE(MyPluginInterface, MyPluginInterface_iid)
Q_PLUGIN_METADATA(IID MyPluginInterface_iid)
QtPluginDemo/
├── main/ 主程序
│ ├── main.cpp
│ ├── mainwindow.h/cpp/ui
│ └── CMakeLists.txt
├── plugins/ 插件目录(可独立编译)
│ ├── plugin_core/ 插件核心接口
│ │ ├── plugininterface.h
│ │ └── CMakeLists.txt
│ ├── plugin_echo/ 示例插件 1:回显插件
│ │ ├── echo_plugin.h/cpp
│ │ └── CMakeLists.txt
│ └── plugin_calculator/ 示例插件 2:计算器插件
│ ├── calc_plugin.h/cpp
│ └── CMakeLists.txt
└── CMakeLists.txt 根 CMake(可选)
// plugin_core/plugininterface.h
#pragma once
#include <QtPlugin>
#include <QString>
#include <QWidget>
class PluginInterface {
public:
virtual ~PluginInterface() = default;
// 插件名称(显示在主程序界面)
virtual QString name() const = 0;
// 插件描述
virtual QString description() const = 0;
// 插件版本
virtual QString version() const = 0;
// 创建插件的 UI 部件(可以返回 nullptr 表示无界面)
virtual QWidget* createWidget(QWidget *parent = nullptr) = 0;
// 可选:插件初始化(加载资源、连接信号等)
virtual void initialize() {}
};
#define PluginInterface_iid "org.example.PluginInterface/1.0"
Q_DECLARE_INTERFACE(PluginInterface, PluginInterface_iid)
echo_plugin.h
#pragma once
#include "plugininterface.h"
class EchoPlugin : public QObject, public PluginInterface {
Q_OBJECT
Q_PLUGIN_METADATA(IID PluginInterface_iid)
Q_INTERFACES(PluginInterface)
public:
QString name() const override { return "回显插件"; }
QString description() const override { return "简单输入输出回显演示"; }
QString version() const override { return "1.0.0"; }
QWidget* createWidget(QWidget *parent = nullptr) override;
void initialize() override;
};
echo_plugin.cpp
#include "echo_plugin.h"
#include <QVBoxLayout>
#include <QLineEdit>
#include <QTextEdit>
#include <QPushButton>
#include <QDebug>
QWidget* EchoPlugin::createWidget(QWidget *parent) {
QWidget *w = new QWidget(parent);
QVBoxLayout *layout = new QVBoxLayout(w);
QLineEdit *input = new QLineEdit(w);
input->setPlaceholderText("输入内容,按回车回显");
QTextEdit *output = new QTextEdit(w);
output->setReadOnly(true);
QPushButton *clearBtn = new QPushButton("清空", w);
layout->addWidget(input);
layout->addWidget(output);
layout->addWidget(clearBtn);
connect(input, &QLineEdit::returnPressed, this, [=]() {
QString text = input->text();
if (!text.isEmpty()) {
output->append(">> " + text);
input->clear();
}
});
connect(clearBtn, &QPushButton::clicked, output, &QTextEdit::clear);
return w;
}
void EchoPlugin::initialize() {
qDebug() << "回显插件初始化完成";
}
#include <QPluginLoader>
#include <QDir>
#include <QDebug>
#include "plugininterface.h"
void MainWindow::loadPlugins() {
QDir pluginsDir(qApp->applicationDirPath());
#if defined(Q_OS_WIN)
pluginsDir.cd("plugins");
#elif defined(Q_OS_MAC)
pluginsDir.cdUp();
pluginsDir.cd("Plugins");
#else
pluginsDir.cd("plugins");
#endif
const QStringList files = pluginsDir.entryList(QDir::Files);
for (const QString &fileName : files) {
if (!QLibrary::isLibrary(fileName)) continue;
QPluginLoader loader(pluginsDir.absoluteFilePath(fileName));
QObject *plugin = loader.instance();
if (!plugin) continue;
if (auto* myPlugin = qobject_cast<PluginInterface*>(plugin)) {
qDebug() << "加载插件成功:" << myPlugin->name() << "版本:" << myPlugin->version();
QWidget *w = myPlugin->createWidget();
if (w) ui->tabWidget->addTab(w, myPlugin->name());
myPlugin->initialize();
} else {
qDebug() << "插件" << fileName << "未实现 PluginInterface 接口";
}
}
}
根 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(QtPluginDemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
# 主程序
add_executable(QtPluginDemo main.cpp mainwindow.cpp)
target_link_libraries(QtPluginDemo PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets)
# 插件接口(静态库,供插件和主程序链接)
add_library(plugin_interface INTERFACE)
target_include_directories(plugin_interface INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/plugin_core)
# 插件:echo_plugin
add_library(echo_plugin MODULE plugins/plugin_echo/echo_plugin.cpp)
target_link_libraries(echo_plugin PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets plugin_interface)
set_target_properties(echo_plugin PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins")
| 问题 | 原因 | 解决方法 |
|---|---|---|
| QPluginLoader::instance() 返回 nullptr | 插件未正确编译或路径不对 | 确认插件输出到 plugins 目录,检查文件后缀(.dll / .so) |
| qobject_cast<PluginInterface*> 失败 | 未定义 IID 或 Q_DECLARE_INTERFACE | 必须在接口头文件中定义 IID 并使用 Q_DECLARE_INTERFACE |
| 插件加载成功但无输出 | 插件未调用 initialize() | 主动调用插件的初始化函数 |
| Windows 报'无法加载插件' | 缺少 Qt 运行时 DLL | 把 Qt 的 bin 目录加入 PATH,或使用 windeployqt 工具部署 |
| Linux 插件加载失败 | 缺少 .so 依赖 | 使用 ldd plugins/libxxx.so 检查依赖,安装缺失库 |
策略模式(Strategy Pattern)是行为型设计模式的一种,它定义了一系列算法(策略),将每个算法封装起来,并使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端,从而实现'算法与客户端的解耦'。
在实际开发中,当一个类需要根据不同条件选择不同行为时(如支付系统选择微信/支付宝/银行卡),直接用 if-else 会导致代码膨胀、难以扩展。策略模式通过多态接口解决这个问题,让行为像'插件'一样可插拔。
策略模式的核心价值:开闭原则(对扩展开放、对修改封闭),特别适合 Qt 项目中的动态行为切换(如渲染策略、事件处理策略)。
概念: 策略模式将可变的行为抽象为一个策略接口(Strategy),然后由上下文类(Context)持有策略对象的引用。客户端通过上下文类调用策略,从而实现行为动态切换。
工作原理:
核心思想(为什么用策略模式):
策略模式有三个主要角色:
UML 图简述(文字版):
Context - strategy: Strategy*
+ executeStrategy()
Strategy (接口)
+ algorithmInterface() = 0
ConcreteStrategyA
+ algorithmInterface() { /* 实现 A */ }
ConcreteStrategyB
+ algorithmInterface() { /* 实现 B */ }
优点:
缺点:
不适用场景:行为固定、变体少时(简单 if-else 就够)。
示例场景:排序策略(快速排序 vs 冒泡排序),上下文是数据处理器。
strategy.h(抽象策略)
#pragma once
class SortStrategy {
public:
virtual ~SortStrategy() = default;
virtual void sort(std::vector<int>& data) = 0;
virtual std::string name() const = 0;
};
quick_sort.h
#pragma once
#include "strategy.h"
#include <vector>
class QuickSort : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
std::sort(data.begin(), data.end());
}
std::string name() const override { return "快速排序"; }
};
context.h(上下文)
#pragma once
#include <memory>
#include "strategy.h"
class DataProcessor {
private:
std::unique_ptr<SortStrategy> strategy_;
public:
void setStrategy(std::unique_ptr<SortStrategy> s) {
strategy_ = std::move(s);
}
void process(std::vector<int>& data) {
if (strategy_) {
strategy_->sort(data);
}
}
};
main.cpp
#include "context.h"
#include "quick_sort.h"
#include <iostream>
int main() {
std::vector<int> data = {5, 3, 8, 1, 9};
DataProcessor proc;
proc.setStrategy(std::make_unique<QuickSort>());
proc.process(data);
std::cout << "排序后:";
for (int n : data) std::cout << n << " ";
std::cout << "\n";
return 0;
}
Qt 典型场景:动态切换绘制策略(2D vs 3D)
drawstrategy.h
#pragma once
#include <QObject>
#include <QPainter>
class DrawStrategy : public QObject {
Q_OBJECT
public:
virtual void draw(QPainter *painter, const QRect &rect) = 0;
};
2dstrategy.h
#pragma once
#include "drawstrategy.h"
class TwoDStrategy : public DrawStrategy {
Q_OBJECT
public:
void draw(QPainter *painter, const QRect &rect) override {
painter->fillRect(rect, Qt::blue);
}
};
mainwindow.cpp(上下文)
class Canvas : public QWidget {
std::unique_ptr<DrawStrategy> strategy_;
public:
void setStrategy(std::unique_ptr<DrawStrategy> s) {
strategy_ = std::move(s);
update();
}
protected:
void paintEvent(QPaintEvent *) override {
QPainter painter(this);
if (strategy_) {
strategy_->draw(&painter, rect());
}
}
};
// 使用
Canvas *canvas = new Canvas(this);
canvas->setStrategy(std::make_unique<TwoDStrategy>());
| 陷阱 | 后果 | 最佳实践 |
|---|---|---|
| 基类无虚析构 | 内存泄漏 | 所有接口类写 virtual ~Base() = default; |
| 忘记 override | 误写成隐藏而非覆盖 | C++11+ 强制写 override |
| 多态指针 delete 未用虚析构 | 未调用派生析构 | 用 std::unique_ptr 管理 |
| Qt 信号槽未写 Q_OBJECT | 信号失效 | 所有 QObject 子类写 Q_OBJECT |
| 策略类内存管理不当 | 泄漏或野指针 | 用 std::unique_ptr 持有 |
最佳实践:
项目名称:StrategyPatternQt
strategy.h(策略接口)
#pragma once
#include <QObject>
#include <QPainter>
class ShapeStrategy : public QObject {
Q_OBJECT
public:
virtual void draw(QPainter *painter, const QRect &rect) = 0;
};
mainwindow.cpp(使用)
// 切换策略
void MainWindow::on_switchStrategy_clicked() {
canvas->setStrategy(std::make_unique<CircleStrategy>());
}
策略模式的核心是'行为封装 + 运行时替换',在 Qt 项目中常用于算法切换、事件处理、渲染等场景。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online