Qt 设计模式:享元模式与隐式共享 (COW)
系列:Qt / VTK 设计模式 · Qt 09/11
参考:Qt 6 Implicit Sharing、QSharedDataPointer
引子
如果程序中有 10000 个控件各持有一张相同的背景图片,若每个控件都深拷贝一份像素点阵数组,内存将会迅速被吃满崩溃。但在 Qt 中,把一个包含千万像素的 QImage 或 QPixmap 赋值给另一个变量,时间复杂度是 ,内存开销仅增加一个指针。
这就是**享元模式(Flyweight Pattern)在 Qt 中的落地:运用共享技术有效地支持大量细粒度对象的复用。Qt 将享元模式与值类型语义结合,演化出了闻名遐迩的隐式共享(Implicit Sharing / Copy-On-Write 写时复制)**技术。
核心痛点与 GoF 享元结构
痛点场景:大量重复对象的内存风暴
// 坏方案:频繁进行像素点阵或数组数据的深拷贝
QImage img1("huge_photo.png"); // 占用 50MB 内存
QImage img2 = img1; // 如果是深拷贝,立刻额外占用 50MB 内存!- 内存爆炸:大尺寸数组、点云数据、图像矩阵在多次传参和拷贝时引发严重的内存冗余。
- 性能低下:分配海量内存并进行内存块复制耗时剧烈。
GoF 享元模式结构与 Qt 对应映射
享元模式的核心是将对象状态划分为 内部状态 (Intrinsic State) 与 外部状态 (Extrinsic State):
| 享元模式概念 | 职责说明 | Qt 隐式共享实现 (QSharedData) |
|---|---|---|
| 内部状态 (Intrinsic State) | 独立于环境、可全局共享且不可变重数据 | 存储在底层引用计数块(如 QImageData 像素点阵)中 |
| 外部状态 (Extrinsic State) | 随环境变化、由客户端维护的轻量数据 | 浅拷贝句柄对象本身(如 QImage 壳对象的尺寸、坐标引用) |
| 享元工厂 / 享元句柄 | 维护并分配共享资源句柄 | QSharedDataPointer<T> / QExplicitlySharedDataPointer<T> |
底层原理:写时复制 (Copy-On-Write / COW) 流程
Qt 隐式共享对象的内部结构分为“外壳句柄类 (Value Host)”与“底层引用计数数据块 (Shared Data)”:
- 浅拷贝(读共享):执行
b = a时,只复制指向QSharedData的指针并原子递增ref计数,耗时极短。 - 写时分裂 (
detach()):当尝试对b进行写入修改(如b.bits()或修改像素)时,b检查到ref > 1,立刻触发detach()申请新内存完成深拷贝,使a的数据完全不受影响。
Qt 隐式共享类与 Qt 6 变迁
经典隐式共享类
- 图像与图形类:
QImage,QPixmap,QBitmap,QFont,QPen,QBrush - 容器与字节流:
QByteArray,QList,QVector,QHash,QMap
Qt 6 中的 QString 演进重点
在 Qt 5 中,QString 广泛采用了 COW 机制;但从 Qt 6 开始,QString 默认取消了 COW,转而采用 SSO(Small String Optimization 小字符串优化) 和强推荐使用 QStringView / std::u16string_view。这是因为在现代 CPU 架构下,多线程频繁原子增减 ref 计数的开销有时甚至超出了小字符串直接内存拷贝的成本。
完整代码示例:自定义一个隐式共享 (COW) 享元类
Qt 提供了 QSharedData 和 QSharedDataPointer 帮助开发者用极简的代码打造符合写时复制语义的自定义类:
#include <QSharedData>
#include <QSharedDataPointer>
#include <QString>
#include <QDebug>
// 1. 定义底层内部共享数据块 (Intrinsic State)
class HugePointCloudData : public QSharedData {
public:
HugePointCloudData() = default;
HugePointCloudData(const HugePointCloudData& other)
: QSharedData(other), m_points(other.m_points) {
qDebug() << "--- Physical Deep Copy PointCloud Memory ---";
}
~HugePointCloudData() = default;
QList<float> m_points; // 假设包含百万级点云坐标
};
// 2. 定义面向用户的轻量享元外壳类 (Extrinsic Host)
class PointCloud {
public:
PointCloud() : d(new HugePointCloudData) {}
// 增加点数据(会触发写时分离)
void addPoint(float p) {
// d.detach() 会自动检测 refCount,若 > 1 则调用 HugePointCloudData 拷贝构造函数分裂
d->m_points.append(p);
}
int count() const {
// 只读访问不会触发 detach
return d->m_points.size();
}
private:
QSharedDataPointer<HugePointCloudData> d; // 自动化 COW 智能指针
};
int main() {
PointCloud cloudA;
cloudA.addPoint(1.0f);
cloudA.addPoint(2.0f);
// 浅拷贝:零内存深拷贝开销,仅指针传递与引用计数+1
PointCloud cloudB = cloudA;
qDebug() << "Cloud A Count:" << cloudA.count(); // 2
qDebug() << "Cloud B Count:" << cloudB.count(); // 2
// 触发修改:cloudB 内部自动调用 detach() 独立分裂
cloudB.addPoint(3.0f);
qDebug() << "After Detach Cloud A Count:" << cloudA.count(); // 2
qDebug() << "After Detach Cloud B Count:" << cloudB.count(); // 3 (独立的物理数据)
return 0;
}易混淆模式对比
| 设计模式 | 核心目的 | 共享机制与状态 |
|---|---|---|
| 享元模式 (Flyweight) | 减少细粒度对象内存 | 分离内部/外部状态,只读共享内部重数据,写入时分离 (COW) |
| 单例模式 (Singleton) | 保证全局唯一实例 | 全局只有一个对象句柄,没有写时分离的概念 |
| 原型模式 (Prototype) | 通过克隆创建对象 | 每次克隆通常直接进行独立的深拷贝 |
最佳实践与踩坑避坑
- 传递隐式共享参数尽量使用
const引用:- 函数传参如
void processImage(const QImage& img)。如果直接传值void processImage(QImage img),尽管拷贝构造非常快,但在函数体内如果误调了非 const 方法,会意外触发detach()引发无谓的深拷贝。
- 函数传参如
- 多线程并发安全规则:
- 隐式共享对象的引用计数增减是原子线程安全的。多个线程同时只读访问同一个
QImage是完全安全的;但如果有线程尝试写入修改该共享对象,必须在外部加锁同步,防止在detach()过程中产生竞态。
- 隐式共享对象的引用计数增减是原子线程安全的。多个线程同时只读访问同一个
- 性能敏感热路径避免非 const 迭代器:
- 对
QList/QByteArray使用非const的operator[]或begin()会隐式触发detach()!即使你只是读取。只读遍历请务必使用cbegin()/cend()或at()。
- 对
重点与注意
重点:享元模式在 Qt 中的核心体现是隐式共享(COW 写时复制):浅拷贝极快,非必要不深拷贝。
重点:使用QSharedData与QSharedDataPointer可以非常优雅地为自定义大对象实现 COW 写时分离。
注意:非 const 方法(如operator[]、bits())会主动触发detach(),只读操作请使用at()或const引用。
小结
Qt 享元模式(隐式共享)优雅地平衡了 C++ 值语义的易用性与引用传递的高性能,是写出高性能 Qt 数据的利器。
延伸阅读
- Implicitly Shared Classes
- 上一篇:08 代理 · 下一篇:10 中介者
- 系列索引:README