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() 就是"把共享数据变成独占数据"的过程。

相关推荐
用户805533698031 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner1 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz6 天前
QML Hello World 入门示例
qt
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner10 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner10 天前
DicomViewer (添加模型类)3
qt
xcyxiner11 天前
DicomViewer (目录调整) 2
qt
xcyxiner11 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
桥田智能13 天前
桥田智能 QT-650S:面向白车身焊装的 800kg 重载快换解决方案
开发语言·qt·系统架构
森G13 天前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt