Qt 对象模型与容器类

[TOC]

1 对象模型

普通 C++ 类一般只有:

1
2
3
4
5
6
成员变量
成员函数
构造函数
析构函数
继承
多态

Qt 在 C++ 基础上又加了一些特性,比如:

1
2
3
4
5
6
7
8
9
信号和槽
对象属性系统
事件和事件过滤器
国际化字符串翻译机制
定时器
对象树
守卫指针
对象转换机制
自定义类型

QObject 是 Qt 对象模型的核心。QWidget 继承自 QObject,所以 QWidget 也拥有 QObject 的能力。

  • QObject:Qt 对象能力
  • QWidget:Qt 对象能力 + 图形界面能力

1.1 objectName

每个 QObject 都可以有对象名:

1
button->setObjectName("saveButton");

获取对象名:

1
QString name = button->objectName();

常用函数:

1
2
void setObjectName(const QString &name);
QString objectName() const;

这个对象名不是变量名。

比如:

1
2
QPushButton *button = new QPushButton("保存", this);
button->setObjectName("saveButton");
  • button:C++ 变量名
  • saveButton:Qt 对象名

对象名常用于:

1
2
3
4
样式表选择器
findChild 查找对象
自动连接信号槽
调试输出

例如样式表:

1
2
button->setObjectName("saveButton");
button->setStyleSheet("#saveButton { font-weight: bold; }");

1.2 findChild

因为 QObject 有对象树,所以可以从父对象里查找子对象。

例如:

1
QPushButton *button = this->findChild<QPushButton *>("saveButton");

意思是:在 this 的子对象里,查找 objectName 是 saveButton 的 QPushButton。

常用:

1
2
T findChild(const QString &name = QString()) const;
QList<T> findChildren(const QString &name = QString()) const;

例子:

1
QList<QLineEdit *> edits = this->findChildren<QLineEdit *>();

意思是:找到 this 下面所有 QLineEdit 子对象。

不过实际项目里,不要过度依赖 findChild()。自己有成员变量时,直接用成员变量更清楚:

1
2
private:
QLineEdit *apiKeyEdit = nullptr;

1.3 Q_OBJECT 宏

1
2
3
4
5
6
7
class MyWidget : public QWidget
{
Q_OBJECT

public:
explicit MyWidget(QWidget *parent = nullptr);
};

Q_OBJECT 是一个宏。它不是普通变量,也不是函数。

它的作用是:告诉 Qt,这个类需要使用 Qt 的元对象能力,比如:

1
2
3
4
5
自定义信号
自定义槽
属性系统
运行时类型信息
tr() 翻译

尤其是自己写信号时,必须写:

1
Q_OBJECT

例如:

1
2
3
4
5
6
7
class ChatWindow : public QWidget
{
Q_OBJECT

signals:
void messageSent(const QString &text);
};

没有 Q_OBJECT,自定义信号槽、属性系统等元对象相关功能会出问题。

2 信号和槽

信号和槽用于两个对象之间的通信,是Qt框架的核心特征。

2.1 概述

1
2
3
信号:某件事发生了
槽:收到通知后要执行的函数
connect:把信号和槽连起来

例如:

1
2
3
4
5
按钮被点击了

按钮发出 clicked() 信号

某个函数被调用

代码:

1
connect(button, &QPushButton::clicked, this, &MyWidget::onButtonClicked);

含义:

1
2
当 button 发出 clicked 信号时,
调用 this 的 onButtonClicked 函数。

2.2 connect 基本公式

connect() 的返回值类型是 QMetaObject::Connection,可以保存下来用于之后断开连接。

旧式字符串写法可以简化理解为:

1
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection);

现代 Qt 更推荐成员函数指针写法,可以简化理解为:

1
QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection);

最常见形式:

1
connect(发送者, 信号, 接收者, 槽函数);

例如:

1
connect(button, &QPushButton::clicked, this, &MyWidget::save);

拆开看:

1
button

发送者。谁发信号。

1
&QPushButton::clicked

信号。发什么信号。

1
this

接收者。谁来响应。

1
&MyWidget::save

槽函数。响应时调用哪个函数。

整体意思:button 被点击时,调用 this->save()。

  • 例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <QPushButton>
#include <QDebug>

class MyWidget : public QWidget
{
Q_OBJECT

public:
explicit MyWidget(QWidget *parent = nullptr) : QWidget(parent)
{
QPushButton *button = new QPushButton("保存", this);
connect(button, &QPushButton::clicked, this, &MyWidget::save);
}

private:
void save()
{
qDebug() << "保存数据";
}
};

这里 save() 是普通成员函数,也可以作为槽函数使用。

2.3 信号和槽的参数

信号可以带参数。

比如 QLineEdit::textChanged 信号会带一个参数:

1
void textChanged(const QString &text);

所以可以这样连接:

1
connect(edit, &QLineEdit::textChanged, this, &MyWidget::onTextChanged);

槽函数:

1
2
3
4
void MyWidget::onTextChanged(const QString &text)
{
qDebug() << "当前文本:" << text;
}

整体意思:输入框文本变化时,Qt 自动把新文本传给 onTextChanged。

信号有参数时,槽函数可以接收这些参数。

例如:

1
connect(slider, &QSlider::valueChanged, this, &MyWidget::setVolume);

如果 valueChanged 是:

1
void valueChanged(int value);

那么槽函数可以是:

1
2
3
4
void MyWidget::setVolume(int value)
{
qDebug() << "音量:" << value;
}

参数类型要能对应上。

2.4 Lambda 表达式

信号槽经常配合 lambda 使用。

例如:

1
2
3
connect(button, &QPushButton::clicked, this, [=]() {
qDebug() << "按钮被点击";
});

意思是:

1
按钮被点击时,直接执行大括号里的代码。

这适合处理很短的逻辑。

比如:

1
2
3
connect(closeButton, &QPushButton::clicked, this, [=]() {
close();
});

但是逻辑复杂时,更推荐写成成员函数:

1
connect(saveButton, &QPushButton::clicked, this, &MyWidget::saveSettings);

2.5 自定义信号

可以在自己的类里定义信号。

例如聊天窗口发送消息:

1
2
3
4
5
6
7
8
9
10
class ChatInput : public QWidget
{
Q_OBJECT

public:
explicit ChatInput(QWidget *parent = nullptr);

signals:
void messageSubmitted(const QString &text);
};

注意:

1
signals:

下面声明的是信号。

信号只需要声明,不需要你写函数体。

也就是说,写:

1
2
signals:
void messageSubmitted(const QString &text);

不要写成:

1
2
3
void ChatInput::messageSubmitted(const QString &text)
{
}

信号的实现由 Qt 的 moc 工具生成。

2.6 发射信号 emit

定义了信号之后,可以在类内部发射信号:

1
2
3
4
5
6
7
8
9
10
void ChatInput::submit()
{
QString text = inputEdit->text();

if (text.isEmpty()) {
return;
}

emit messageSubmitted(text);
}

这里:

1
emit messageSubmitted(text);

意思是:

1
告诉外部:用户提交了一条消息,内容是 text。

然后别的对象可以连接它:

1
connect(chatInput, &ChatInput::messageSubmitted, this, &ChatWindow::sendMessage);

整体流程:

1
2
3
4
5
6
7
用户点击发送按钮

ChatInput::submit()

emit messageSubmitted(text)

ChatWindow::sendMessage(text)
  • 例子
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
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QDebug>

class ChatInput : public QWidget
{
Q_OBJECT

public:
explicit ChatInput(QWidget *parent = nullptr) : QWidget(parent)
{
inputEdit = new QLineEdit(this);
sendButton = new QPushButton("发送", this);

QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(inputEdit);
layout->addWidget(sendButton);

connect(sendButton, &QPushButton::clicked, this, &ChatInput::submit);
connect(inputEdit, &QLineEdit::returnPressed, this, &ChatInput::submit);
}

signals:
void messageSubmitted(const QString &text);

private:
void submit()
{
QString text = inputEdit->text();

if (text.isEmpty()) {
return;
}

emit messageSubmitted(text);
inputEdit->clear();
}

private:
QLineEdit *inputEdit = nullptr;
QPushButton *sendButton = nullptr;
};

另一个窗口使用它:

1
2
ChatInput *chatInput = new ChatInput(this);
connect(chatInput, &ChatInput::messageSubmitted, this, &ChatWindow::sendMessage);

槽函数:

1
2
3
4
void ChatWindow::sendMessage(const QString &text)
{
qDebug() << "发送消息:" << text;
}

2.7 互联

  • 一个信号可以连接多个槽。
1
2
3
connect(button, &QPushButton::clicked, this, &MyWidget::save);
connect(button, &QPushButton::clicked, this, &MyWidget::showSavedTip);
connect(button, &QPushButton::clicked, this, &MyWidget::writeLog);

按钮点击后:

1
2
3
save()
showSavedTip()
writeLog()

都会被调用。

  • 多个信号可以连接同一个槽

例如多个按钮都关闭窗口:

1
2
3
connect(okButton, &QPushButton::clicked, this, &QWidget::close);
connect(cancelButton, &QPushButton::clicked, this, &QWidget::close);
connect(closeButton, &QPushButton::clicked, this, &QWidget::close);

也可以多个输入框共用一个槽:

1
2
3
connect(nameEdit, &QLineEdit::textChanged, this, &SettingsWindow::markDirty);
connect(apiKeyEdit, &QLineEdit::textChanged, this, &SettingsWindow::markDirty);
connect(baseUrlEdit, &QLineEdit::textChanged, this, &SettingsWindow::markDirty);

意思是:

1
任意一个输入框变化,都标记设置已修改。
  • 一个信号也可以连接到另一个信号。

例如:

1
connect(inputEdit, &QLineEdit::returnPressed, this, &ChatInput::submitRequested);

其中:

1
2
signals:
void submitRequested();

意思是:

1
2
inputEdit 发出 returnPressed()
this 接着发出 submitRequested()

这相当于把内部控件的信号转发给外部。

2.8 信号和槽的连接

2.8.1 自动连接

1
void on_objectName_signalName();

比如按钮对象名是:saveButton

信号是:clicked()

那么槽函数可以写成:

1
void on_saveButton_clicked();

Qt 可以自动把它们连接起来。

只要名字严格匹配,Qt 就把它们连起来。

2.8.2 断开连接

可以用:

1
disconnect(sender, signal, receiver, slot);

例如:

1
disconnect(button, &QPushButton::clicked, this, &MyWidget::save);

意思是:取消 button.clicked 和 this.save 的连接。

也可以保存连接对象:

1
2
QMetaObject::Connection connection = connect(button, &QPushButton::clicked, this, &MyWidget::save);
disconnect(connection);

这种写法适合以后单独断开某一个连接。

2.8.3 关联类型 ConnectionType

2.8.3.1 自动关联
1
connect(sender, &Sender::signalName, receiver, &Receiver::slotName, Qt::AutoConnection);

这是第五个参数的默认值。

1
connect(button, &QPushButton::clicked, this, &MyWidget::save);

等价于:

1
connect(button, &QPushButton::clicked, this, &MyWidget::save, Qt::AutoConnection);

它的规则是:

1
2
3
4
5
如果发送者和接收者在同一个线程:
像 DirectConnection 一样立即调用槽函数。

如果发送者和接收者在不同线程:
像 QueuedConnection 一样投递到接收者线程的事件队列。
2.8.3.2 直接关联
1
connect(sender, &Sender::signalName, receiver, &Receiver::slotName, Qt::DirectConnection);

意思是:信号一发出,槽函数立刻执行。

例如:

1
emit somethingHappened();

如果是 DirectConnection,大概相当于马上执行:

1
receiver->slotName();

特点:

1
2
3
同步执行
立即调用
槽函数在发信号的那个线程中执行

同一个 GUI 线程里,默认 AutoConnection 通常就会表现得像 DirectConnection

2.8.3.3 队列关联
1
connect(sender, &Sender::signalName, receiver, &Receiver::slotName, Qt::QueuedConnection);

意思是:

1
2
3
信号发出后,不立刻执行槽函数。
Qt 把这次调用放进接收者所属线程的事件队列。
等事件循环处理到它时,再执行槽函数。

类比:

1
2
3
4
5
DirectConnection:
现在当面说,现在立刻处理。

QueuedConnection:
留一张纸条放进对方收件箱,对方稍后处理。

它常用于跨线程。

例如:

1
2
3
4
5
工作线程下载完成

发出 downloadFinished()

主线程稍后更新 UI

注意:GUI 控件只能在主线程更新,所以跨线程通知 UI 时,队列连接非常重要。

2.8.3.4 阻塞队列关联
1
connect(sender, &Sender::signalName, receiver, &Receiver::slotName, Qt::BlockingQueuedConnection);

它和 QueuedConnection 类似,都是把槽函数投递到接收者线程执行。

但区别是:

1
2
发送信号的线程会阻塞等待,
直到接收者线程把槽函数执行完。

特别注意:

1
不要在同一个线程里使用 BlockingQueuedConnection,否则很容易死锁。

因为同一个线程里:

1
2
3
4
发送者等槽函数执行完
但槽函数又需要这个线程的事件循环来执行
结果互相等
卡死
2.8.3.5 唯一关联
1
connect(button, &QPushButton::clicked, this, &MyWidget::save, Qt::UniqueConnection);

意思是:

1
如果同样的连接已经存在,就不要重复连接。

比如你不小心写了两次:

1
2
connect(button, &QPushButton::clicked, this, &MyWidget::save);
connect(button, &QPushButton::clicked, this, &MyWidget::save);

那么按钮点击一次,save() 可能会执行两次。

如果用:

1
2
connect(button, &QPushButton::clicked, this, &MyWidget::save, Qt::UniqueConnection);
connect(button, &QPushButton::clicked, this, &MyWidget::save, Qt::UniqueConnection);

Qt 会避免重复连接。

但是注意,UniqueConnection 不是说这个信号只能连接一个槽。它只是防止完全相同的一组连接重复出现。

2.8.3.6 单次关联
1
Qt::SingleShotConnection

意思是:

1
2
这个连接只触发一次。
触发一次后,Qt 自动断开这个连接。

例如:

1
connect(button, &QPushButton::clicked, this, &MyWidget::save, Qt::SingleShotConnection);

效果是:

1
2
第一次点击按钮:调用 save()
之后再点击:不再调用 save()

它适合:

1
2
3
只允许初始化一次
只处理第一次完成信号
只响应第一次点击
2.8.3.7 组合关联

比如想要队列连接 + 防止重复连接

可以写:

1
2
connect(sender, &Sender::signalName, receiver, &Receiver::slotName,
Qt::QueuedConnection | Qt::UniqueConnection);

如果想要直接连接 + 只触发一次

可以写:

1
2
connect(sender, &Sender::signalName, receiver, &Receiver::slotName,
Qt::DirectConnection | Qt::SingleShotConnection);

这里又是 flags,用 | 组合。

3 对象树与所有权

Qt中使用对象树object tree来组织和管理所有的QObject类及其子类的对象。

当创建一个QObject对象时,如果使用了其它的对象作为父对象,那么这个对象就会被添加到父对象的children()列表中,这样当父对象被销毁时,这个对象也会被销毁。

例如:

QPushButton button = new QPushButton(“确定”, this);
QLineEdit
edit = new QLineEdit(this);
QTimer *timer = new QTimer(this);

1
2
3
4
5
6
7
8
9
10
11
12
这些对象都挂在 `this` 下面。

关于this是什么,参考《Qt 窗口部件》1.1节。

> Q: 父对象和子对象到底是什么关系?
>
> A: 父子对象关系不是 C++ 继承关系!
>
> 比如:
>
> ```c++
> QPushButton *button = new QPushButton("保存", this);

>

这里 button 的父对象是 this

但是这不表示button 继承自 this。

它只是运行时的对象管理关系。

C++ 继承关系是:

1
2
3
QObject
└─ QWidget
└─ QPushButton

Qt 父子对象关系可能是:

1
2
3
4
SettingsWindow
├─ saveButton
├─ cancelButton
└─ apiKeyEdit

所以不要被命名困扰了,

1
2
继承关系:类和类之间的关系
父子对象关系:对象和对象之间的关系

3.1 对象树

假设有一个窗口:

1
2
3
4
QWidget *window = new QWidget;
QPushButton *button = new QPushButton("保存", window);
QLineEdit *edit = new QLineEdit(window);
QTimer *timer = new QTimer(window);

它的对象树大概是:

1
2
3
4
window
├─ button
├─ edit
└─ timer

当你执行:

1
delete window;

Qt 会自动删除它的子对象:

1
2
3
4
5
delete window

自动 delete button
自动 delete edit
自动 delete timer

3.2 parent()children()

可以获取一个 QObject 的父对象:

1
QObject *p = button->parent();

也可以获取子对象列表:

1
const QObjectList &list = window->children();

常用函数:

1
2
3
QObject *parent() const;
const QObjectList &children() const;
void setParent(QObject *parent);

例如:

1
button->setParent(this);

意思是:

1
把 button 挂到 this 下面。

不过实际开发中,通常直接在构造时传 parent:

1
QPushButton *button = new QPushButton("保存", this);

比后面再 setParent() 更清楚。

3.3 栈对象与堆对象

  • 例如:
1
2
QTimer *timer = new QTimer;
timer->start(1000);

这个 timer 没有父对象。

如果忘记:

1
delete timer;

它就会内存泄漏。

所以 Qt 里更常写:

1
QTimer *timer = new QTimer(this);

这样它跟着当前对象一起释放。

  • 栈对象不要乱传 parent

例如:

1
2
3
4
void MyWidget::createButton()
{
QPushButton button("保存", this);
}

这里 button 是局部变量,存在栈上。函数结束后它会自动销毁。

这种写法通常没意义,因为函数结束按钮就没了。

更危险的是,如果栈对象和父对象销毁顺序处理不好,可能造成重复析构问题

所以 GUI 控件一般写成:

1
QPushButton *button = new QPushButton("保存", this);

或者作为成员变量:

1
2
private:
QPushButton *button = nullptr;

然后构造函数里:

1
button = new QPushButton("保存", this);

3.4 布局和 parent 的关系

比如:

1
2
3
QVBoxLayout *layout = new QVBoxLayout(this);
QPushButton *button = new QPushButton("保存", this);
layout->addWidget(button);

这里有两个概念:

1
2
parent:负责对象生命周期和 QWidget 显示父子关系
layout:负责控件位置和大小

也就是说:

1
2
parent 管“谁释放谁”
layout 管“摆在哪里、多大”

layout->addWidget(button) 不是说 layout 拥有 button。它主要是把 button 加入布局管理,让布局负责排列它。

实际写法常见:

1
2
3
QVBoxLayout *layout = new QVBoxLayout(this);
QPushButton *button = new QPushButton("保存");
layout->addWidget(button);

这种情况下,Qt 通常会把 button 的父 QWidget 设置为布局所在的窗口。也就是说,按钮最终还是会归窗口管理。

3.5 对象与信号槽

  • Qt 很贴心:当 QObject 被销毁时,和它相关的信号槽连接通常会自动断开。

例如:

1
connect(timer, &QTimer::timeout, this, &MyWidget::update);

如果 timer 被销毁,连接会失效。

如果 this 被销毁,连接也会失效。

所以一般不需要在析构函数里手动:

1
disconnect(timer, &QTimer::timeout, this, &MyWidget::update);

Qt 会处理。

  • 有时候你不能马上 delete 一个 QObject,尤其是在事件处理或信号槽调用过程中。

Qt 提供:

1
void deleteLater();

例如:

1
worker->deleteLater();

意思是:

1
2
不要现在立刻 delete;
等回到事件循环后,Qt 再安全删除它。

这个在多线程、网络请求、异步对象里很常见。

4 元对象系统

4.1 元对象系统概述

普通 C++ 对象本身有成员变量、成员函数。

但 Qt 还想知道更多信息,比如:

  1. 这个对象的类名是什么?
  2. 这个类有哪些信号?
  3. 这个类有哪些槽函数?
  4. 这个类有哪些属性?
  5. 这个对象能不能安全转换成某个类型?
  6. 能不能通过字符串调用某个函数?

这些信息普通 C++ 本身不方便直接提供,所以 Qt 做了一套额外机制,叫元对象系统Meta-Object System.

  • 条件:

元对象系统主要依赖三样东西。

QObject、Q_OBJECT 宏、moc 元对象编译器。

关系大概是:

1
2
3
4
5
6
7
8
9
你写的 QObject 子类

类里写 Q_OBJECT

moc 扫描这个头文件

生成额外 C++ 代码

信号槽、属性、类名、动态调用等功能才能工作

4.2 Q_OBJECT 宏

1
2
3
4
5
6
7
class MyWidget : public QWidget
{
Q_OBJECT

public:
explicit MyWidget(QWidget *parent = nullptr);
};

Q_OBJECT 是一个宏,它会往类里插入一些 Qt 元对象系统需要的声明。

相当于告诉 Qt:这个类要加入元对象系统。

只要类需要下面这些能力,就应该写 Q_OBJECT

1
2
3
4
5
6
自定义信号
Q_PROPERTY 属性
qobject_cast
tr() 翻译
元对象信息
需要被元对象系统识别的 slots / invokable 方法

最典型的是自定义信号:

1
2
3
4
5
6
7
class ChatInput : public QWidget
{
Q_OBJECT

signals:
void messageSubmitted(const QString &text);
};

如果没有 Q_OBJECT,自定义信号、Q_PROPERTYqobject_cast 等元对象相关功能通常会出问题。

4.3 moc

moc即Meta-Object Compiler元对象编译器

它是 Qt 自带的一个代码生成工具。

仅靠普通 C++ 编译器不会生成这些机制所需的元信息:

1
2
3
4
5
Q_OBJECT
signals:
slots:
emit
Q_PROPERTY

所以 Qt 会先用 moc 扫描你的类,然后生成额外的 .cpp 代码。

比如你的头文件里有:

1
2
3
4
5
6
7
class ChatInput : public QWidget
{
Q_OBJECT

signals:
void messageSubmitted(const QString &text);
};

moc 会生成一些底层代码,让 Qt 知道:

1
2
ChatInput 这个类有一个信号:
messageSubmitted(QString)

所以你才能写:

1
emit messageSubmitted(text);

也才能写:

1
connect(chatInput, &ChatInput::messageSubmitted, this, &ChatWindow::sendMessage);

这也就解释了一个问题:signals 为什么只声明,不写函数体。

因为信号的实现不是手写的,而是 moc 生成的。

例如:

1
2
signals:
void messageSubmitted(const QString &text);

不要写:

1
2
3
void ChatInput::messageSubmitted(const QString &text)
{
}

因为信号不是普通业务函数。它的底层发射逻辑由 Qt 生成。

4.4 元对象系统的方法

  1. 获取类名:metaObject()

每个带有元对象能力的 QObject 都可以获取自己的元对象信息:

1
const QMetaObject *meta = object->metaObject();

然后可以获取类名:

1
QString className = object->metaObject()->className();

例子:

1
qDebug() << button->metaObject()->className();

这个功能调试时挺有用。

  1. 判断是不是某个类:inherits()
1
bool inherits(const char *className) const;

例如:

1
2
3
if (object->inherits("QPushButton")) {
qDebug() << "这是 QPushButton 或它的子类";
}

意思是:

这个对象是不是 QPushButton,或者是不是 QPushButton 的子类对象。

不过日常写代码更常用 qobject_cast。

  1. qobject_cast

qobject_cast 是 Qt 提供的类型转换工具。

例如:

1
2
QObject *obj = button;
QPushButton *btn = qobject_cast<QPushButton *>(obj);
  • 如果 obj 真的是 QPushButton 或它的子类,转换成功。

  • 如果不是,返回nullptr.

所以常见写法:

1
2
3
4
5
6
QObject *obj = sender();
QPushButton *button = qobject_cast<QPushButton *>(obj);

if (button) {
qDebug() << "发送信号的是按钮:" << button->text();
}

Q: qobject_cast 和 static_cast 的区别

A:

1
QPushButton *button = static_cast<QPushButton *>(obj);

static_cast 更相信程序员,它不会在运行时帮你认真检查对象到底是不是 QPushButton。

如果你转错了,可能出问题。

1
QPushButton *button = qobject_cast<QPushButton *>(obj);

qobject_cast 会通过 Qt 元对象系统检查,失败就返回 nullptr。

所以:

static_cast:我相信你,错了后果自负。
qobject_cast:我帮你查一下,不对就返回 nullptr。

  1. sender()

在槽函数里,有时可以知道是谁发出的信号:

1
QObject *sender() const;

例子:

1
2
3
4
5
6
7
8
9
10
void MyWidget::onButtonClicked()
{
QObject *obj = sender();
QPushButton *button = qobject_cast<QPushButton *>(obj);

if (button) {
qDebug() << "点击了按钮:" << button->text();
}

}

如果多个按钮连接到同一个槽:

1
2
connect(okButton, &QPushButton::clicked, this, &MyWidget::onButtonClicked);
connect(cancelButton, &QPushButton::clicked, this, &MyWidget::onButtonClicked);

那么槽函数里可以用 sender() 判断是哪一个按钮。

不过注意:sender() 不要滥用。项目里更清楚的写法通常是用 lambda:

1
2
3
4
5
6
7
connect(okButton, &QPushButton::clicked, this, [=]() {
handleButton("ok");
});

connect(cancelButton, &QPushButton::clicked, this, [=]() {
handleButton("cancel");
});

这样更直观。

  1. 动态属性 setProperty / property

QObject 可以通过字符串设置属性:

1
object->setProperty("role", "primary");

读取:

1
QVariant value = object->property("role");

例如:

1
2
saveButton->setProperty("role", "primary");
cancelButton->setProperty("role", "secondary");

之后可以读取:

1
QString role = saveButton->property("role").toString();
  1. 动态调用:QMetaObject::invokeMethod

Qt 可以通过元对象系统动态调用方法。

比如:

1
QMetaObject::invokeMethod(this, "saveSettings");

意思是:通过名字调用 this 对象上的 saveSettings 方法。

  • 常见用途:

跨线程调用某个对象的方法
延迟执行某个槽函数
框架层通过字符串调用方法

  • 例如投递到事件队列执行:
1
QMetaObject::invokeMethod(this, "saveSettings", Qt::QueuedConnection);
  1. tr() 翻译函数

你可能看到:

1
setWindowTitle(tr("Settings"));

tr() 是 Qt 国际化翻译用的函数,它和元对象系统也有关。

简单理解:tr(“Settings”) 表示这个字符串将来可以被翻译。

如果暂时不做多语言,写:

1
setWindowTitle("Settings");

也能运行。

但正式 Qt 项目里常见:

1
setWindowTitle(tr("Settings"));

以后可以配合 Qt 翻译工具生成多语言版本。

5 属性系统

5.1 初识属性

  1. 什么是属性

普通 C++ 里,你可能这样写:

1
2
3
4
5
class PetWidget : public QWidget
{
private:
int opacity = 100;
};

这里 opacity 是一个普通成员变量。

但 Qt 里经常说“属性”,比如:

1
2
3
button->setEnabled(false);
button->setVisible(true);
button->setObjectName("saveButton");

这些都可以理解成对象的状态:

1
2
3
4
enabled     是否可用
visible 是否可见
objectName 对象名
text 按钮文字、输入框文字

Qt 的属性系统就是把这些状态统一管理起来。

  1. 属性不等于成员变量

Q: 属性是不是变量?

A:

属性 ≈ 对外暴露出来的状态
成员变量 = 类内部真正保存数据的变量

比如:

1
2
button->setText("保存");
QString text = button->text();

这里 text 是按钮的一个属性。

但按钮内部到底用什么成员变量保存文字,你不用管。

Qt 常见写法是:

1
2
setXxx(...)
xxx()

比如:

1
2
3
4
5
6
7
8
9
10
11
setText(...)
text()

setEnabled(...)
isEnabled()

setVisible(...)
isVisible()

setObjectName(...)
objectName()

这就是典型的属性读写风格。

Qt 里可以粗略分两类属性:

  • 静态属性:类定义时就声明好的属性

  • 动态属性:运行时临时添加的属性

5.2 动态属性

5.2.1 setProperty 和 property

所有 QObject 都可以使用:

1
2
bool setProperty(const char *name, const QVariant &value);
QVariant property(const char *name) const;

例如:

1
2
saveButton->setProperty("role", "primary");
cancelButton->setProperty("role", "secondary");

读取:

1
QString role = saveButton->property("role").toString();

这就是动态属性。

意思是:

1
给 saveButton 临时加一个叫 role 的属性,值是 primary。

注意,这个 role 不一定是 QPushButton 原本就有的属性,是你运行时加上去的。

Q: 为什么返回值不是 QString?

A: 因为属性值可能是各种类型:

1
2
3
4
5
6
7
8
QString
int
bool
double
QColor
QSize
QPoint
...

所以 Qt 用一个通用容器 QVariant 来装。

比如:

1
2
3
button->setProperty("level", 3);
button->setProperty("enabledByConfig", true);
button->setProperty("role", "primary");

读取时再转回具体类型:

1
2
3
int level = button->property("level").toInt();
bool enabled = button->property("enabledByConfig").toBool();
QString role = button->property("role").toString();

5.2.2 动态属性常用场景

  • 场景一:给控件打标签

比如设置页面有很多输入框,想标记哪些是敏感字段:

1
2
apiKeyEdit->setProperty("secret", true);
baseUrlEdit->setProperty("secret", false);

之后统一处理:

1
2
3
4
5
6
7
QList<QLineEdit *> edits = this->findChildren<QLineEdit *>();

for (QLineEdit *edit : edits) {
if (edit->property("secret").toBool()) {
edit->setEchoMode(QLineEdit::Password);
}
}
  • 场景二:配合样式表

Qt 样式表可以根据属性选择控件。

例如:

1
saveButton->setProperty("role", "primary");

样式表:

1
2
3
QPushButton[role="primary"] {
font-weight: bold;
}

这样就可以根据 role 设置样式。

不过注意,如果运行时改变属性,有时需要重新刷新样式:

1
2
3
4
saveButton->setProperty("role", "danger");
saveButton->style()->unpolish(saveButton);
saveButton->style()->polish(saveButton);
saveButton->update();
  • 场景三:多个控件共用一个槽

比如多个按钮都连接同一个槽:

1
2
3
connect(openButton, &QPushButton::clicked, this, &MyWidget::handleButton);
connect(saveButton, &QPushButton::clicked, this, &MyWidget::handleButton);
connect(deleteButton, &QPushButton::clicked, this, &MyWidget::handleButton);

给它们设置属性:

1
2
3
openButton->setProperty("action", "open");
saveButton->setProperty("action", "save");
deleteButton->setProperty("action", "delete");

槽函数里判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void MyWidget::handleButton()
{
QPushButton *button = qobject_cast<QPushButton *>(sender());

if (!button) {
return;
}

QString action = button->property("action").toString();

if (action == "open") {
openFile();
} else if (action == "save") {
saveFile();
} else if (action == "delete") {
deleteFile();
}
}

不过现代写法里,很多时候用 lambda 更清楚:

1
2
3
connect(openButton, &QPushButton::clicked, this, [=]() { openFile(); });
connect(saveButton, &QPushButton::clicked, this, [=]() { saveFile(); });
connect(deleteButton, &QPushButton::clicked, this, [=]() { deleteFile(); });

所以动态属性不是必须用,而是某些批量处理场景很方便。

5.3 静态属性

5.3.1 Q_PROPERTY

静态属性通常通过 Q_PROPERTY 声明。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PetWidget : public QWidget
{
Q_OBJECT

Q_PROPERTY(int petScale READ petScale WRITE setPetScale NOTIFY petScaleChanged)

public:
int petScale() const;
void setPetScale(int scale);

signals:
void petScaleChanged(int scale);

private:
int m_petScale = 100;
};

这句是重点:

1
Q_PROPERTY(int petScale READ petScale WRITE setPetScale NOTIFY petScaleChanged)

拆成几部分:

1
2
3
4
5
int              属性类型
petScale 属性名
READ petScale 读取属性时调用 petScale()
WRITE setPetScale 修改属性时调用 setPetScale(...)
NOTIFY petScaleChanged 属性变化时发出 petScaleChanged(...)

所以需要写:

1
2
3
4
int PetWidget::petScale() const
{
return m_petScale;
}

和:

1
2
3
4
5
6
7
8
9
void PetWidget::setPetScale(int scale)
{
if (m_petScale == scale) {
return;
}

m_petScale = scale;
emit petScaleChanged(m_petScale);
}

这就是标准写法。

Q: 为什么成员变量常写成 m_xxx?

比如:

1
2
private:
int m_petScale = 100;

A: 这个 m_ 只是命名习惯,意思是 member。

1
2
3
petScale      属性名 / getter 函数名
setPetScale setter 函数名
m_petScale 内部成员变量

它们的关系是:

1
2
外部读属性 → 调用 petScale() → 返回 m_petScale
外部写属性 → 调用 setPetScale(...) → 修改 m_petScale

5.3.2 静态属性常用场景

  • 用处一:可以通过字符串访问

有了 Q_PROPERTY 后,可以这样访问:

1
widget->setProperty("petScale", 120);

读取:

1
int scale = widget->property("petScale").toInt();

也就是说,外部不一定非要直接调用:

1
widget->setPetScale(120);

也可以通过属性名字符串访问。

这就是元对象系统的能力。

  • 用处二:可以配合动画

Qt 的属性动画 QPropertyAnimation 需要操作 Qt 属性。

例如:

1
2
3
4
5
QPropertyAnimation *animation = new QPropertyAnimation(widget, "windowOpacity", this);
animation->setDuration(300);
animation->setStartValue(0.0);
animation->setEndValue(1.0);
animation->start();

这里 "windowOpacity" 就是 QWidget 的一个属性。

意思是:

1
300 毫秒内,把窗口透明度从 0.0 变到 1.0。

如果你自定义了 petScale,理论上也可以动画它:

1
2
3
4
5
QPropertyAnimation *animation = new QPropertyAnimation(petWidget, "petScale", this);
animation->setDuration(300);
animation->setStartValue(80);
animation->setEndValue(120);
animation->start();

前提是这个属性有 WRITE,也就是能被修改。

5.4 NOTIFY 信号

比如:

1
Q_PROPERTY(int petScale READ petScale WRITE setPetScale NOTIFY petScaleChanged)

这里的:

1
NOTIFY petScaleChanged

表示属性改变时会发信号。

标准 setter 通常这样写:

1
2
3
4
5
6
7
8
9
void PetWidget::setPetScale(int scale)
{
if (m_petScale == scale) {
return;
}

m_petScale = scale;
emit petScaleChanged(m_petScale);
}

为什么先判断?

1
2
3
if (m_petScale == scale) {
return;
}

因为如果值没变,就没必要发 petScaleChanged

否则可能导致:

1
2
3
明明值没变,却反复通知外部
界面重复刷新
信号槽循环调用

5.5 属性的限定

有些属性可以只读:

1
Q_PROPERTY(QString version READ version CONSTANT)

意思是:version 只能读取,不能修改。(根本原因:只有 READ,没有 WRITE。)

只读属性一般只有 READ,没有 WRITE

常见选项:

1
2
3
4
5
READ       指定 getter
WRITE 指定 setter
NOTIFY 指定变化通知信号
CONSTANT 表示属性值构造后不变
FINAL 表示子类不应该重写这个属性

6 容器类

Qt 库提供了一组通用的、基于模板的容器类(container classes)。这些容器类可以用来存储指定类型的项目(items),例如:

如果需要一个大小可变的QString数组,那么可以使用QList< QString >。

Q: 为什么 Qt 有自己的容器类?

C++ 标准库本来就有:

1
2
3
4
5
6
std::vector
std::list
std::map
std::unordered_map
std::set
std::string

但是 Qt 也有自己的:

1
2
3
4
5
QList
QMap
QHash
QSet
QString

A: 原因主要是:

1
2
3
4
1. Qt API 很多地方直接使用 Qt 容器。
2. Qt 容器和 QObject、QVariant、QString 等体系配合更自然。
3. Qt 容器支持隐式共享,复制成本通常比较低。
4. Qt 早期为了跨平台和统一接口,提供了自己的容器体系。

6.1 QList< T >

QList<T> 是最常见的顺序容器。

可以理解成一串按顺序排列的数据

例如保存多个动作名:

1
2
3
4
QList<QString> actions;
actions.append("idle");
actions.append("blink");
actions.append("wave");

也可以这样初始化:

1
QList<QString> actions = {"idle", "blink", "wave"};

访问:

1
2
QString firstAction = actions[0];
QString secondAction = actions.at(1);

常用函数:

1
2
3
4
5
6
7
8
9
10
11
void append(const T &value);        // 追加元素
void prepend(const T &value); // 插到开头
void insert(qsizetype i, const T &value); // 插入到指定位置
void removeAt(qsizetype i); // 删除指定位置的元素
bool removeOne(const T &value); // 删除第一个匹配元素
void clear(); // 清空
qsizetype size() const; // 元素个数
bool isEmpty() const; // 是否为空
T &operator[](qsizetype i); // 下标访问
const T &at(qsizetype i) const; // 只读访问
bool contains(const T &value) const; // 是否包含

例子:

1
2
3
4
5
6
7
QList<int> nums = {1, 2, 3};
nums.append(4);
nums.removeOne(2);

for (int n : nums) {
qDebug() << n;
}

输出:

1
2
3
1
3
4

6.2 QStringList

QStringList 可以理解成字符串列表,本质上就是一组 QString

例如:

1
2
QStringList actions;
actions << "idle" << "blink" << "wave";

常用场景:

1
2
3
4
动作名列表
资源文件后缀列表
搜索关键词列表
配置项名称列表

常用函数:

1
2
3
4
5
6
QStringList split(const QString &sep) const; // 按分隔符切分字符串,返回字符串列表
QString join(const QString &separator) const; // 把字符串列表用 separator 拼接成一个字符串
bool contains(const QString &str) const; // 判断列表中是否包含某个字符串
void append(const QString &str); // 追加字符串
qsizetype size() const; // 返回字符串数量
bool isEmpty() const; // 判断列表是否为空

例子:

1
2
3
QString text = "idle,blink,wave";
QStringList parts = text.split(",");
QString joined = parts.join(" | ");

结果:

1
idle | blink | wave

6.3 QMap

QMap 是键值对容器。

可以理解成:通过 key 找 value

例如:

1
2
3
4
QMap<QString, QString> config;
config["provider"] = "tavily";
config["model"] = "gpt-4.1";
config["baseUrl"] = "https://example.com";

读取:

1
QString provider = config["provider"];

更安全一点:

1
QString provider = config.value("provider", "default");

这里:

1
config.value("provider", "default");

意思是:

1
2
如果存在 provider,就返回它;
如果不存在,就返回 default。

常用函数:

1
2
3
4
5
6
7
8
9
10
iterator insert(const Key &key, const T &value); // 插入一组键值对;如果 key 已存在,通常会覆盖原来的 value;返回插入位置的迭代器
T value(const Key &key) const; // 根据 key 获取对应的 value;如果 key 不存在,返回 T 类型的默认值
T value(const Key &key, const T &defaultValue) const; // 根据 key 获取 value;如果 key 不存在,返回指定的 defaultValue
bool contains(const Key &key) const; // 判断容器中是否存在这个 key
qsizetype remove(const Key &key); // 删除指定 key 对应的键值对;返回删除的数量
QList<Key> keys() const; // 返回所有 key 组成的列表
QList<T> values() const; // 返回所有 value 组成的列表
void clear(); // 清空容器中的所有键值对
qsizetype size() const; // 返回键值对数量
bool isEmpty() const; // 判断容器是否为空

例子:

1
2
3
4
5
6
7
QMap<QString, int> scores;
scores.insert("Alice", 90);
scores.insert("Bob", 85);

if (scores.contains("Alice")) {
qDebug() << scores.value("Alice");
}

QMap 的 key 是有序的。比如 key 是字符串时,它会按 key 的顺序排列。

6.4 QHash

QHash 也是键值对容器。

用法和 QMap 很像:

1
2
3
QHash<QString, int> scores;
scores.insert("Alice", 90);
scores.insert("Bob", 85);

区别是:

1
2
QMap:按 key 排序,内部有序。
QHash:不保证顺序,通常查找更快。

例如配置项如果要稳定顺序显示,可以用 QMap
如果只是快速根据 ID 找对象,可以用 QHash

6.5 QSet< T >

QSet 是集合。

特点是:元素不能重复。

例如保存已经启用的功能名:

1
2
3
4
QSet<QString> enabledFeatures;
enabledFeatures.insert("search");
enabledFeatures.insert("llm");
enabledFeatures.insert("search");

虽然插入了两次 "search",但集合里只有一个。

判断是否存在:

1
2
3
if (enabledFeatures.contains("search")) {
qDebug() << "搜索已启用";
}

常用函数:

1
2
3
4
5
6
iterator insert(const T &value); // 插入一个元素;如果元素已经存在,不会重复插入;返回元素位置的迭代器
bool contains(const T &value) const; // 判断集合中是否包含某个元素
bool remove(const T &value); // 删除指定元素;成功删除返回 true,否则返回 false
void clear(); // 清空集合中的所有元素
qsizetype size() const; // 返回集合中的元素数量
bool isEmpty() const; // 判断集合是否为空

适合保存:

1
2
3
4
去重后的文件名
启用的标签
已经处理过的 ID
用户选择的功能集合

6.6 遍历容器

6.6.1 遍历 QList

1
2
3
4
5
QList<QString> actions = {"idle", "blink", "wave"};

for (const QString &action : actions) {
qDebug() << action;
}

这里建议用:

1
const QString &action

因为这样不会复制字符串。

6.6.2 遍历 QMap / QHash

简单遍历 key:

1
2
3
4
5
6
7
QMap<QString, QString> config;
config["provider"] = "tavily";
config["model"] = "gpt-4.1";

for (const QString &key : config.keys()) {
qDebug() << key << config.value(key);
}

不过 keys() 会生成一个 key 列表。数据很少时无所谓;数据很多时,可以用迭代器。

迭代器写法:

1
2
3
for (auto it = config.constBegin(); it != config.constEnd(); ++it) {
qDebug() << it.key() << it.value();
}

这个读法是:

1
2
3
4
从 config 的开头开始;
只要没到结尾;
每次向后移动一个元素;
输出 key 和 value。

6.7 常用 STL 算法

Qt 容器也可以配合很多标准库算法使用。

比如排序:

1
2
3
#include <algorithm>
QList<int> nums = {3, 1, 4, 2};
std::sort(nums.begin(), nums.end());

结果:

1
1 2 3 4

自定义排序:

1
2
3
4
5
QList<QString> names = {"cat", "elephant", "dog"};

std::sort(names.begin(), names.end(), [](const QString &a, const QString &b) {
return a.length() < b.length();
});

按照字符串长度排序。

查找:

1
2
3
4
5
auto it = std::find(names.begin(), names.end(), "dog");

if (it != names.end()) {
qDebug() << "找到了";
}

统计:

1
int count = std::count(nums.begin(), nums.end(), 2);

条件判断:

1
2
3
bool hasLongName = std::any_of(names.begin(), names.end(), [](const QString &name) {
return name.length() > 5;
});

常用的:

1
2
3
4
5
6
std::sort      排序
std::find 查找
std::count 计数
std::any_of 是否有一个满足条件
std::all_of 是否全部满足条件
std::remove_if 按条件移除,常和 erase 配合

6.8 QString

QString 是 Qt 的字符串类,非常重要。

普通 C++ 有:std::string

Qt 里主要用:QString

Qt API 大量使用 QString,比如:

1
2
button->setText("保存");
QString text = lineEdit->text();

6.8.1 创建 QString

1
2
QString name = "DesktopPet";
QString title("设置");

6.8.2 拼接

1
QString text = QString("Hello, ") + name;

更推荐复杂拼接用 arg():

1
QString message = QString("当前模型:%1,温度:%2").arg(modelName).arg(temperature);

6.8.3 常用函数

1
2
3
4
5
6
7
8
9
10
11
12
bool isEmpty() const;                    // 是否为空
qsizetype length() const; // 字符数
bool contains(const QString &str) const; // 是否包含
bool startsWith(const QString &s) const; // 是否以某字符串开头
bool endsWith(const QString &s) const; // 是否以某字符串结尾
QString trimmed() const; // 去掉首尾空白
QString toLower() const; // 转小写
QString toUpper() const; // 转大写
QStringList split(const QString &sep) const; // 分割
QString &replace(const QString &before, const QString &after); // 替换,并返回自身引用
int toInt(bool *ok = nullptr) const; // 转 int
double toDouble(bool *ok = nullptr) const; // 转 double

例子:

1
2
3
QString input = "  hello qt  ";
QString clean = input.trimmed();
qDebug() << clean;

输出:

1
hello qt

6.8.4 字符串转数字

1
2
3
4
5
6
7
8
9
QString text = "123";
bool ok = false;
int value = text.toInt(&ok);

if (ok) {
qDebug() << "转换成功:" << value;
} else {
qDebug() << "转换失败";
}

这个 ok 很重要。

如果QString text = "abc";

那么text.toInt(&ok);会失败,ok == false。

6.8.5 数字转 QString

整数:

1
2
int value = 123;
QString text = QString::number(value);

浮点数:

1
2
double x = 3.14159;
QString text = QString::number(x, 'f', 2);

结果:

1
3.14

6.8.6 QString 和 std::string 转换

  • Qt 字符串转标准字符串:
1
2
QString qstr = "hello";
std::string str = qstr.toStdString();
  • 标准字符串转 Qt 字符串:
1
2
std::string str = "hello";
QString qstr = QString::fromStdString(str);

不过在 Qt 项目里,界面相关尽量用 QString,别来回转太多。

6.9 QByteArray 和 QVariant

6.9.1 QByteArray

QByteArray 表示一段字节数据。

它和 QString 的区别是:

1
2
QString:文本字符串,适合人读的字符。
QByteArray:原始字节,适合网络、文件、编码、加密、二进制数据。

例如网络请求返回的数据通常是:

1
QByteArray data = reply->readAll();

JSON 字符串转字节:

1
QByteArray jsonData = jsonString.toUtf8();

字节转 QString:

1
QString text = QString::fromUtf8(jsonData);

常用函数:

1
2
3
4
5
6
7
bool isEmpty() const; // 判断字节数组是否为空
qsizetype size() const; // 返回字节数量
QByteArray trimmed() const; // 返回去掉首尾空白字符后的新 QByteArray,不修改原对象
QByteArray toBase64() const; // 把当前字节数据编码成 Base64 格式
static QByteArray fromBase64(const QByteArray &base64); // 把 Base64 数据解码回普通字节数据
QByteArray toHex() const; // 把当前字节数据编码成十六进制字符串形式
static QByteArray fromHex(const QByteArray &hexEncoded); // 把十六进制字符串形式的数据解码回普通字节数据

例如:

1
2
3
QString text = "你好";
QByteArray bytes = text.toUtf8();
QString restored = QString::fromUtf8(bytes);

6.9.2 QVariant

QVariant 是一个可以保存多种类型值的容器。

比如:

1
2
3
4
QVariant v1 = 123;
QVariant v2 = QString("hello");
QVariant v3 = true;
QVariant v4 = 3.14;

读取时转回具体类型:

1
2
3
4
int n = v1.toInt();
QString s = v2.toString();
bool b = v3.toBool();
double d = v4.toDouble();

它经常出现在:

1
2
3
4
5
QObject 属性系统
QSettings 配置读写
模型视图系统
JSON/数据表
通用参数传递

例如:

1
2
button->setProperty("role", "primary");
QVariant role = button->property("role");

property() 返回的就是 QVariant

6.9.3 QVariant 和类型安全

QVariant 很方便,但不要滥用。

比如:

1
2
QVariant value = "123";
int n = value.toInt();

能转。

但如果:

1
2
QVariant value = "abc";
int n = value.toInt();

转换失败时可能得到 0,容易混淆。

更稳可以:

1
2
3
4
5
6
bool ok = false;
int n = value.toString().toInt(&ok);

if (!ok) {
qDebug() << "转换失败";
}

6.10 隐式共享

Qt 很多类支持隐式共享,比如:

1
2
3
4
5
6
QString
QByteArray
QList
QMap
QHash
QImage

隐式共享也叫写时复制copy-on-write

  • 例子:

假设:

1
2
QString a = "hello";
QString b = a;

你可能以为复制了一整份字符串。

但 Qt 通常不会立刻复制底层数据,而是让 ab 暂时共享同一份数据:

1
2
3
a ─┐
├── "hello"
b ─┘

如果你只是读取:

1
2
qDebug() << a;
qDebug() << b;

它们一直共享,效率很高。

如果修改 b

1
b += " qt";

Qt 才会给 b 复制一份新数据:

1
2
3
a ─── "hello"

b ─── "hello qt"

这就叫写时复制。

Q: 为什么这样设计

A: 因为 Qt 代码里经常会传递对象:

1
2
3
void setTitle(QString title);
QString getName();
QList<QString> actions = otherActions;

如果每次都深拷贝,成本很高。

隐式共享让复制看起来简单,但底层很省:

1
2
复制时先共享。
真正修改时才复制。

所以 QStringQByteArrayQList 这些按值返回、按值传递在 Qt 里比较常见。