QT 隐式共享/写时复制详解

一、前言

先看个例子:

cpp 复制代码
    QVector<int> a, b;
    a.resize(100);
    QVector<int>::iterator i = a.begin();
    b = a;
    a[0] = 5;
    qDebug() << a[0] << b[0] << *i;
    // b.clear();  // Now the iterator i is completely invalid.
    *i = 12;
    qDebug() << a[0] << b[0] << *i;

输出结果是:

5 0 0

5 12 12

是不是有些出人意料?为何会出现这种现象,原因是QVector 是隐式共享(copy-on-write)的容器。以上代码中,b = a; 并不会马上复制一份独立的数据。它只是让 ba 共享同一块内部数据缓冲区。此时:ab 共享同一个数据内存,i 仍然指向那块内存。QVector<int>::iterator 本质上指向内部数组,当修改 ab 时,若发生写时复制(detach),迭代器可能失效。

修改迭代器 i 就是用了隐式共享容器的 "裸指针式" 修改方式。

  1. QVector<int>::iterator i = a.begin();

    iteratorQVector 中本质上就是一个指向内部数组的指针。

  2. b = a;

    ab 共享同一块内存,引用计数加 1。

  3. *i = 5;

    通过这个迭代器直接写入内部数组,这种写法绕过了 QVector 的"写时复制"检测机制。

因此结果是:

  • 并不是通过 a 的成员函数修改数据
  • QVector 无法触发 detach()
  • 所以修改会作用在"共享内存"上,而且行为是不安全、不可预测的

正确理解

  • QVector 的 COW 机制只能保证通过容器的写操作(operator[], replace, append, 等)修改时自动 detach。
  • 通过 iterator / data() 直接修改内部数组,已经脱离了容器管理。
  • 这时你得到的是"未定义行为"结果,可能只见到 b 改变,或者其它奇怪表现。

正确做法

如果想修改 a,应该这样做:a0 = 5; // 这会触发 detach

如果你要在 shared 后修改其中一个对象,先确保它独立:

a.detach();

a0 = 5;

结论

*i = 5 在这里不是"安全修改 a 的方式",而是绕过了 QVector 的 copy-on-write 机制,所以表现会很奇怪。

当然了,如果用C++的vector就不会出现这种现象,如下所示:

cpp 复制代码
    vector<int> vec1, vec2;
    vec1.resize(100);
    vector<int>::iterator it = vec1.begin();
    vec2 = vec1;
    vec1[0] = 5;
    vec2.clear();
    qDebug() << vec1[0] << vec2[0] << *it;
    *it = 12;
    qDebug() << vec1[0] << vec2[0] << *it;

输出结果:

5 0 5

12 0 12

二、隐式共享的概念

Qt 隐式共享也称为"写时复制(Copy-On-Write)",它是 Qt 设计中一个重要的性能优化机制。

核心概念

  • 多个对象可以共享同一块内部数据
  • 只要不修改数据,就不会发生复制
  • 一旦需要写操作,才会复制出独立的数据
  • 这样既节省内存,又提高拷贝性能

典型行为

cpp 复制代码
    QString a = "hello";
    QString b = a;            // a 和 b 共享同一份数据
    b[0] = 'H';               // b 发生写操作 => 自动 detach,复制独立数据
    
    qDebug() << a << b;

输出结果:"hello" "Hello"

这里 ab 初始共享,只有当 b 修改时,才会进行真正的数据复制。


内部机制

隐式共享通常由这些部分构成:

  • 一个共享数据结构,包含真实数据和引用计数
  • 一个对象持有指向共享数据的指针
  • 引用计数记录当前有多少对象共享这份数据
  • 读取操作不影响引用计数
  • 写操作先检查引用计数,必要时复制数据

这相当于:

  • 共享阶段:0 复制
  • 写入阶段:才复制

detach()isDetached()

detach() 是隐式共享的关键:

  • 如果引用计数为 1,说明当前对象已独占数据,不做任何事
  • 如果引用计数 > 1,则复制数据、减小原数据引用计数、当前对象转向新数据

isDetached() 可以检查当前对象是否已经独占数据。


哪些 Qt 类型支持隐式共享

常见支持隐式共享的 Qt 类型:

  • QString
  • QByteArray
  • QList
  • QVector
  • QMap
  • QHash
  • QImage
  • QPixmap
  • QFont
  • QVariant
  • QJsonDocument

这些类型的赋值、拷贝都很轻量。


为什么这么设计

优点:

  • 拷贝开销小:赋值只是拷贝指针、增加引用计数
  • 读取高效:只要不修改,就避免了大量复制
  • 内存利用率高:多个对象共享同一份数据

适合场景:

  • 读取多,写入少
  • 频繁传递值类型对象

什么时候会复制

通常在以下写操作发生时:

  • operator[] 写入
  • append(), insert(), replace()
  • fill(), remove()
  • QImage::bits()QByteArray::data() 等非 const 获取底层可写指针时

换句话说,只要会修改内部数据,Qt 会自动触发复制。


需要注意的坑

  1. 直接用非 const 迭代器或 data() 修改

    • 可能绕开自动 detach
    • 导致共享数据被同时修改
  2. const 访问不会 detach

    • const QString &s = a; 只是读取,不会复制
  3. 线程安全

    • 隐式共享对象本身是可复制的
    • 共享数据的读取是线程安全的
    • 但写操作不是线程安全的,不能同时在多个线程修改同一个实例
  4. QVector/QList 等在 Qt 5/Qt 6 中实现细节不同

    • 只是语义相同,不用关心底层实现
    • 但仍建议不要依赖内部指针行为

什么时候手动调用 detach()

通常不需要,但可以用于优化:

  • 你知道马上要修改对象
  • 希望提前复制,避免后续多次写时重复 detach

例如:

a.detach();

a0 = 1;


总结

  • Qt 隐式共享是"共享只读、修改时复制"的机制
  • 它让 QString, QVector 等类型赋值非常高效
  • 写操作会自动触发 detach()
  • 不要通过直接修改底层指针绕过机制
  • 线程写入时要特别小心

隐式共享的本质是"让对象的拷贝变成轻量级共享,再在需要写时才付出复制成本"。

三、Qt 中的 detach() 是什么

detach() 是 Qt 隐式共享容器里的一个操作,用于保证对象拥有"独占的数据副本"。

它的作用

在 Qt 里,很多类都是隐式共享的,比如:

  • QString
  • QVector
  • QImage
  • QByteArray
  • QMap
  • QHash

当你做赋值 b = a; 时,这些对象通常不会立即复制数据,而是共享同一块内部内存,并且内部维护一个引用计数。

detach() 的作用就是:

  • 检查这块共享数据是否被多个对象共享
  • 如果是,则复制一份独立的数据
  • 让当前对象指向这份独立数据
  • 让当前对象"独占"这份数据
  • 使引用计数对原共享数据递减

detach() 具体做了哪些工作

通常它会执行以下步骤:

  1. 读取内部引用计数
  2. 如果引用计数为 1
    • 说明当前对象已经是唯一持有者
    • 直接返回,不做额外操作
  3. 如果引用计数大于 1
    • 分配新的内存
    • 将原数据复制到新内存
    • 将当前对象的内部指针指向新内存
    • 原数据的引用计数减 1
    • 新数据的引用计数设为 1

什么时候会调用 detach()

一般在"写操作"时自动触发:

  • a[i] = value
  • a.append(...)
  • a.replace(...)
  • a.insert(...)
  • a.fill(...)

这些操作会自动调用 detach(),保证修改只影响当前对象,不影响其他共享该数据的对象。


为什么需要 detach()

这是 Qt 的"写时复制"(copy-on-write)机制:

  • 读取时不复制,节省内存和性能
  • 修改时才复制,保证语义正确

比如:

cpp 复制代码
QString a = "hello";
QString b = a; // 共享数据
b[0] = 'H';   // 自动 detach,b 得到自己的副本

在这个例子里,a 保持原样,b 先 detach 再修改。


额外提醒

  • detach() 通常是隐式调用的,你很少需要手动调用它
  • 手动调用 detach() 可以提前让对象独立,以避免后续修改造成额外开销
  • 直接通过迭代器或 data() 修改底层数据,可能绕开 detach(),这会导致不安全行为

简单来说,detach() 就是"把共享数据变成独占数据"的过程。

相关推荐
San813_LDD2 小时前
[QT]Qt对象树笔记:父子关系与内存管理
开发语言·qt
luoyayun3612 小时前
Qt/QML 音频波形图模块实现:从 PCM 数据到可缩放波形
qt·音视频·波形图绘制
资深流水灯工程师3 小时前
PySide6 + Qt Designer + PyCharm 完整开发流程
开发语言·qt·pycharm
BAGAE3 小时前
FEC-RS前向纠错编码理论及工程实施研究
c语言·c++·qt·算法·决策树·链表
ALINX技术博客3 小时前
【黑金云课堂】FPGA技术教程Linux开发:摄像头GPU渲染显示/Qt OpenGLES使用
linux·qt·fpga开发·gpu
1379003403 小时前
uBuntu20运行QGC RTSP拉流失败解决记录
qt·qgroundcontrol
满天星830357716 小时前
【Qt】信号和槽(二) (自定义信号和槽)
开发语言·数据库·qt
Jun62617 小时前
QT(19)-VISA控制仪器
开发语言·qt
Jun62619 小时前
QT(2)-通过管道关联CMD
开发语言·qt·命令模式