文章

Qt的隐式共享

Qt隐式共享通过引用计数实现对象数据共享,拷贝时仅增加计数,写时复制确保独占,提升性能并减少内存开销。

Qt的隐式共享

Qt 的隐式共享

Qt 中很多数据类型(字符串 QString、字节数组 QByteArray、图片 QImage、容器类等)都涉及大量内存数据的复制。传统的“值拷贝”如果直接深拷贝,会导致性能开销非常大。

隐式共享技术结合了:

  • 引用计数(Reference Counting)
  • 写时复制(Copy-On-Write, COW)

来实现高效的值语义,即:

  • 拷贝对象时只增加引用计数,不复制数据;
  • 只有在修改数据时才复制底层数据,保证对象间互不影响。

核心概念

术语说明
共享数据块实际数据存储和引用计数的结构体
引用计数记录有多少对象共享此数据块
写时复制修改时复制数据,保持数据独立
detach()保证当前对象持有独立数据的操作

隐式共享的典型结构

共享数据类

1
2
3
4
5
6
7
8
9
10
class SharedData {
public:
    QAtomicInt ref;  // 原子引用计数,支持多线程
    // 数据成员...

    SharedData() : ref(1) {}
    SharedData(const SharedData& other) : ref(1) {
        // 复制数据成员
    }
};
  • QAtomicInt 用于线程安全地管理引用计数。

  • 构造时引用计数初始化为 1。

使用共享数据的外部类(如 QString)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class MyString {
private:
    SharedData* d;  // 指向共享数据块的指针

public:
    MyString(const char* str) {
        d = new SharedData();
        // 初始化数据
    }

    MyString(const MyString& other) {
        d = other.d;
        d->ref.ref();  // 引用计数加一
    }

    ~MyString() {
        if (!d->ref.deref())  // 引用计数减一,若为0释放内存
            delete d;
    }

    MyString& operator=(const MyString& other) {
        if (d != other.d) {
            if (!d->ref.deref())
                delete d;
            d = other.d;
            d->ref.ref();
        }
        return *this;
    }

    // 写时复制调用点
    void detach() {
        if (d->ref > 1) {   // 多个对象共享数据时,复制数据
            SharedData* newData = new SharedData(*d); // 复制构造
            d->ref.deref();
            d = newData;
        }
        // 引用计数此时必为1,表示独立拥有数据
    }

    // 修改成员前调用detach()
    void setChar(int index, char c) {
        detach();
        // 修改 d->data[index]
    }
};

工作流程详解

拷贝构造/赋值

  • 仅复制指针 d
  • 调用 d->ref.ref() 增加引用计数
  • 不复制数据本身

修改数据前(写时复制)

  • 调用 detach()
  • 检查引用计数:若 >1,说明数据被多个对象共享,需要复制数据块
  • 创建数据块副本,引用计数减一,当前对象指向新数据
  • 若引用计数 == 1,直接修改,无需复制

对象析构

  • 调用 d->ref.deref(),引用计数减一
  • 若引用计数为 0,释放共享数据块

为什么用 QAtomicInt

  • 在多线程环境中,多个线程可能访问同一共享数据块
  • 引用计数的递增和递减操作必须是原子操作
  • QAtomicInt 保证了线程安全

隐式共享示意流程图

1
2
3
4
5
6
7
8
创建 s1 (ref=1)
     |
拷贝 s2 = s1 (ref=2)
     |
s2 修改数据 -> 调用 detach()
     |          |
     |       复制数据s2 拥有新数据(ref=1)
     |       s1 仍指向原数据(ref=1)

Qt 内置隐式共享类举例

类名用途
QString字符串
QByteArray字节数组
QImage图像
QPixmap显示用图像
QVector向量
QList列表
QVariant变体类型

这些类都采用隐式共享设计,避免频繁深拷贝。

示例代码(简化版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <QAtomicInt>
#include <iostream>

class SharedData {
public:
    QAtomicInt ref;
    char* data;
    int size;

    SharedData(const char* str) {
        ref = 1;
        size = strlen(str);
        data = new char[size + 1];
        strcpy(data, str);
    }
    SharedData(const SharedData& other) {
        ref = 1;
        size = other.size;
        data = new char[size + 1];
        strcpy(data, other.data);
    }
    ~SharedData() {
        delete[] data;
    }
};

class MyString {
private:
    SharedData* d;
public:
    MyString(const char* str) {
        d = new SharedData(str);
    }
    MyString(const MyString& other) {
        d = other.d;
        d->ref.ref();
    }
    ~MyString() {
        if (!d->ref.deref())
            delete d;
    }
    MyString& operator=(const MyString& other) {
        if (d != other.d) {
            if (!d->ref.deref())
                delete d;
            d = other.d;
            d->ref.ref();
        }
        return *this;
    }
    void detach() {
        if (d->ref > 1) {
            SharedData* newData = new SharedData(*d);
            d->ref.deref();
            d = newData;
        }
    }
    void setChar(int index, char c) {
        detach();
        if (index >= 0 && index < d->size) {
            d->data[index] = c;
        }
    }
    void print() {
        std::cout << d->data << std::endl;
    }
};

int main() {
    MyString s1("hello");
    MyString s2 = s1;   // 共享数据
    s2.setChar(0, 'H'); // 写时复制
    s1.print();         // hello
    s2.print();         // Hello
}

信号槽参数的推荐写法

Qt 官方明确推荐:

Signal arguments should be passed by value for safety. Slot arguments can use const & to avoid unnecessary copies.

Qt 推荐写法

1
2
3
4
5
6
7
// 信号:值传递
signals:
    void textChanged(QString text);

// 槽函数:const 引用传递
slots:
    void onTextChanged(const QString &text);

信号为什么用值传递?(QString

信号参数必须独立存储,不能用引用

Qt 信号的实现依赖 元对象系统(Meta-Object System),发出信号时会将参数打包成事件,传入事件队列中(跨线程是异步的):

1
emit textChanged(str);
  • 如果参数是 const QString&信号只传引用,可能在事件处理前就失效,导致悬空引用、崩溃。

  • 所以 必须值传递,确保信号发出时参数独立存在,与外部变量无关。

隐式共享让值传递变得“便宜”

Qt 的 QString 是一个隐式共享类

  • 拷贝对象时只是增加引用计数,不会复制字符串内容
  • 修改时才复制数据(写时复制)

所以信号参数写成 QString,虽然是“拷贝”,但成本非常低

槽函数为什么用 const QString&

槽函数是在信号触发时被调用的函数,作为信号的接收者。

避免不必要的拷贝
  • 槽函数只需要读数据,不需要修改
  • const QString & 就可以直接引用信号参数(事件队列中那份),避免再次拷贝对象
不影响信号系统的安全性
  • 信号已经做了“值传递”,槽拿到的是安全独立的那一份数据,用引用不会出错。
  • 所以这时候引用使用是安全 + 高效的。

图示理解

1
2
3
4
5
6
7
8
9
10
11
QString str = "hello";

// emit textChanged(str);
         ↓(拷贝构造,仅增加引用计数)
QString param = str;    // param.ref = str.ref + 1

// Qt 内部发出信号,事件队列存 param
// 槽函数接收时:
void onTextChanged(const QString &text) {
    // 直接引用 param,不触发拷贝
}

非隐式共享类型的建议

如果传递的是自定义类对象,遵循:

  • 有合适的拷贝构造函数
  • 或者注册为 Q_DECLARE_METATYPE + qRegisterMetaType<T>()
  • 在信号中使用 值传递

即使担心拷贝成本大,也不能用 T& 作为信号参数。 因为 Qt 的元对象系统只支持值语义。

小技巧:可以用 QSharedPointer<T> 值传递

如果传的是自定义大对象,可以用:

1
void resultReady(QSharedPointer<HeavyData> data);

它本质上是一个智能指针,值传递也只是引用计数 +1,成本低、线程安全。

信号槽参数类型推荐表

类型信号参数写法槽函数写法说明
QStringQStringconst QString &隐式共享,值传递成本低
QByteArrayQByteArrayconst QByteArray &同上
QVector<int>QVector<int>const QVector<int> &同上
自定义类(轻量)MyTypeconst MyType &有拷贝构造,按值传递安全
自定义类(重量)QSharedPointer<T>const QSharedPointer<T>&减少深拷贝
非共享容器、裸指针T&T*❌ 禁止生命周期不受控,不能作为信号参数
本文由作者按照 CC BY 4.0 进行授权