这段内容讲的是 Qt 容器(Qt Containers)和标准库容器(STL Containers)之间的选择和背景:
主要观点:
- Qt 容器的历史背景
- Qt 自身带有一套容器类(如
QList
,QVector
,QMap
等),主要是因为历史原因:- 早期Qt需要支持没有标准库(STL)支持的平台。
- 避免将标准库的符号暴露在Qt的ABI(应用二进制接口)中,保证二进制兼容性。
- Qt 自身带有一套容器类(如
- 现在的情况(Qt 5及以后)
- Qt 5以后,已经假定目标平台有“可用的、可用的STL实现”。
- 这意味着Qt本身内部开始依赖标准库,标准库的可用性已成为前提。
- Qt容器在API中的角色
- Qt依然在API中使用Qt容器,且这些容器对应用程序开发者开放。
- 但对于新项目和业务代码,建议优先使用标准库容器,只有在必要时才用Qt容器。
总结
- Qt容器是为了历史兼容和API稳定性存在。
- 对于大多数现代C++项目,尤其是跨平台和与第三方库交互,推荐使用标准库容器(如
std::vector
,std::map
等)。 - 仅在和Qt框架接口交互时,或者特殊性能需求时考虑Qt容器。
这部分内容详细对比了 Qt 容器和 C++ 标准库容器的设计哲学及对应关系,并介绍了一个特殊的 Qt 容器——QVarLengthArray。以下是总结:
Qt 容器设计哲学 vs 标准库设计哲学
特点 | Qt容器 | 标准库容器 |
---|---|---|
目标 | 足够好,主要用于构建 GUI | 真正通用,适合各种场景 |
API 风格 | 使用 camelCase(例如 isEmpty() ) | 使用 snake_case(例如 empty() ) |
设计重点 | 易用性和API易发现性 | 高效和正确性 |
典型用法示例 | QVector<int> v; v << 1 << 2 << 3; | std::vector<int> v; v.push_back(1); |
拷贝行为 | 拷贝可能较“便宜”(浅拷贝或引用计数机制) | 拷贝通常是深拷贝 |
算法实现 | 作为成员函数(如 contains() ) | 通过标准算法(如 std::find ) |
Qt与标准库对应容器对照表
Qt 容器 | 标准库容器 |
---|---|
QVector | std::vector |
QList | — |
QLinkedList | std::list |
QVarLengthArray | — |
QMap | std::map |
QMultiMap | std::multimap |
QHash | std::unordered_map |
QMultiHash | std::unordered_multimap |
QSet | std::unordered_set |
QVarLengthArray 介绍
- QVarLengthArray 是 Qt 中一个特殊的容器,类似于
std::vector
,但它预先在栈上分配一定的空间,避免频繁的堆分配。 - 类似于 Boost 的
small_vector
,支持所谓的“短小优化”(SSO,small string optimization思想),提高小规模数据的性能。 - 适用于大多数情况下容器元素数量不会超过某个固定值的场景。
- 示例声明:
QVarLengthArray<Obj, 32> vector;
表示预分配32个对象的空间。
这段内容进一步强调了 QList 以及整体 Qt容器 的局限性,并建议更倾向使用标准库容器。总结如下:
QList 的特性和问题
- 不是链表,而是基于数组实现的列表。
- 对于存储大于指针大小的对象非常低效,因为它会为每个对象单独分配堆内存。
- 建议避免使用 QList,除非别无选择。
- 自己写代码时优先使用 QVector。
不建议使用 Qt 容器的理由
- Qt 容器维护和更新不活跃,缺乏新特性。
- STL 容器更快,生成的代码更小且经过更多测试。
- Qt 容器功能远不及 STL,例如:
- 存放的类型必须可默认构造和可复制。
- 没有异常安全保证。
- 缺少许多 C++98/C++11/C++17 新增的API,如范围构造、插入、就地构造(emplace)、基于节点的操作等。
- 不支持灵活的分配器、比较器、哈希函数等自定义操作。
- Qt 容器的API不一致,比如
QVector<T>
支持append(T&&)
,但QList<T>
不支持。 - 还有在 resize、capacity、shrink 等行为上的差异。
- Qt 容器API 与 STL 容器存在微妙差异,可能带来使用上的困扰。
建议
- 优先使用 STL 容器(如
std::vector
,std::map
,std::unordered_map
等)。 - 只有在必须与 Qt API 交互时,才考虑使用 Qt 容器。
这段主要给出了在实际项目中选择容器的建议,核心点总结如下:
选择哪个容器?
- STL 容器大多数情况下性能和特性都优于 Qt 容器。
- Qt 自身实现内部也开始采用 STL 容器,说明它们的优势。
- Qt 的 API 仍然暴露 Qt 容器,无法轻易替换,因为 Qt 有强 API 和 ABI 兼容性承诺。
应用程序的推荐策略:
- 优先使用 STL 容器。
- 仅在以下情况考虑使用 Qt 容器:
- 没有对应的 STL 或 Boost 容器(这几乎不存在)。
- 与 Qt 或基于 Qt 的库接口交互时。
- 如果用到了 Qt 容器,尽量避免来回转换 STL 和 Qt 容器。
保持使用 Qt 容器,减少性能开销和复杂度。
简单来说,除非为了兼容 Qt 接口,推荐用 STL 容器,既现代又高效。
关于 resize、capacity、shrink 这几个行为,Qt 容器和 STL 容器确实存在一些细节差异:
1. resize()
- STL 容器
resize(n)
会调整容器大小到n
,如果变大,会用默认值或指定值填充新增元素。- 对于
std::vector
,新增元素构造且初始化。 - 可以保证元素连续且大小准确。
- Qt 容器(比如
QVector
)resize(n)
也会调整大小,但内部实现可能采用引用计数共享数据。- 新增元素初始化行为和 STL 类似,但某些情况下效率可能略差。
QList
的行为因内部结构不同,resize()
可能导致额外的内存分配,效率不佳。
2. capacity()
- STL 容器
capacity()
返回当前已分配但未使用的内存空间大小。std::vector
会预先分配一定空间,减少扩容次数。- 可以通过
reserve()
来预先分配容量,避免多次重新分配。
- Qt 容器
capacity()
也返回预分配空间大小。- 但 Qt 容器(尤其是老版本)对容量管理不如 STL 灵活,扩容策略可能不同。
- 不能像 STL 一样明确调用
reserve()
保证容量。
3. shrink_to_fit()
- STL 容器
- C++11 引入的函数,
shrink_to_fit()
用于请求减少容量以匹配当前大小。 - 实现是非强制的,但多数现代库会释放多余内存。
- 提高内存利用率,避免浪费。
- C++11 引入的函数,
- Qt 容器
- 大多数 Qt 容器没有
shrink_to_fit()
接口。 - 只能通过拷贝或交换技巧手动释放多余容量,比如重新构造一个容器拷贝数据。
- 缺乏直接控制容量的函数,不够灵活。
- 大多数 Qt 容器没有
额外说明
- Qt 容器内部通常使用引用计数和共享数据的技术,这导致某些操作(比如 resize)会更复杂,可能出现延迟复制(copy-on-write)。
- STL 容器行为更加透明直接,便于性能优化和行为预测。
总结
操作 | STL 容器 | Qt 容器 |
---|---|---|
resize() | 直接调整大小,初始化新增元素 | 类似,但可能因共享数据延迟复制 |
capacity() | 返回预分配容量,可用 reserve 控制 | 返回预分配容量,容量管理不够灵活 |
shrink_to_fit() | 标准接口,尝试释放多余内存 | 无对应接口,需手动技巧释放多余容量 |
总结来说,针对 Qt 6:
- Qt 容器必须继续保留,确保兼容性和稳定的 API/ABI,不会做大破坏性改动。
- QList 在 Qt 中使用非常广泛,但它其实并不是一个理想的线性容器。
- 未来有可能让 QList 直接成为 QVector 的别名(typedef),简化内部实现。
- 同时,Qt 可能会推出一个新的容器类型(比如 QArrayList)来替代 QList 的部分功能,提供更好的性能和设计。
- Qt 容器通过类型特征(type traits)来优化性能,尤其是判断一个类型是否支持relocatable(可重定位)。
- 如果类型是可重定位的,容器扩容时可以直接用
realloc
这样高效的内存操作,而不需要一个个移动元素,性能大幅提升。 - 使用 Qt 容器时,建议用
Q_DECLARE_TYPEINFO
宏来告诉 Qt该类型是否可重定位,从而启用优化。 - 一些典型例子:
- 简单结构体(如
IntVector
)通常是可重定位的。 - 有指针指向自己或有内部联系的结构(如
TreeNode
)通常不可重定位,因为移动内存会破坏指针。 - 有短字符串优化(SSO)的字符串类型,如果内部指针指向内部缓冲区,也不可重定位。
这个机制可以显著提高 Qt 容器的性能,前提是正确声明类型信息。
- 简单结构体(如
编译器不能自动判断类型是否可重定位(relocatable),需要开发者手动标注。
- Qt 通过宏
Q_DECLARE_TYPEINFO(Type, Kind)
来告诉容器该类型的“性质”,Kind 可以是:- Q_PRIMITIVE_TYPE
- 类型非常简单,比如
int
,任何位模式都是有效的 - 构造和析构可以跳过,直接内存拷贝即可
- 类型非常简单,比如
- Q_MOVABLE_TYPE
- 类型可被内存移动(如用
memmove
或realloc
) - 但仍然调用构造和析构函数
- 类型可被内存移动(如用
- Q_COMPLEX_TYPE(默认)
- 普通复杂类型,需要正常调用构造、复制、析构
- Q_PRIMITIVE_TYPE
- EASTL 有类似机制(
EASTL_DECLARE_TRIVIAL_RELOCATE
),而 STL 标准库本身没有明确这个特性。
这让 Qt 容器能根据类型特性选择最优内存操作,提高性能。
每次定义可能会被放入 Qt 容器的自定义类型时,都应该用 Q_DECLARE_TYPEINFO
显式声明其类型信息。
- 例如:
struct IntVector {int size, capacity;int *data; }; Q_DECLARE_TYPEINFO(IntVector, Q_MOVABLE_TYPE);
- 如果之后再加这个 trait,有可能会导致 ABI 兼容性问题,影响程序稳定性和升级安全。
所以,建议一开始就定义好,避免后期修改带来的麻烦。
Qt 的**隐式共享(Implicit Sharing)**核心思想是:
- 对象内部包含一个指向实际数据(pimpl)的指针,这个数据块有一个引用计数器(refcount)。
- 创建对象时,refcount = 1。
- 拷贝对象时,只拷贝指针,refcount +1。
- 调用 const 方法不改数据,refcount 不变。
- 调用非 const 方法时,如果 refcount > 1,说明数据被共享,必须先detach(深拷贝数据),保证修改不会影响其他对象(写时拷贝,Copy-On-Write)。
这样设计的好处是: - 拷贝操作很轻量(只增引用计数),节省性能。
- 保证数据修改时不会影响到其他对象,实现值语义。
- 但需要注意调用非 const 方法会触发隐式深拷贝,可能会有性能开销。
这个机制常见于 Qt 的字符串(QString)、容器等类。
要小心“隐藏的 detach”,即你可能没有意识到调用了非 const 方法,导致了拷贝开销。
这里演示了隐式共享在 QVector 中的实际效果。
示意过程是这样的:
QVector<int> v {10, 20, 30};
QVector<int> v2 = v; // 复制v,不会马上拷贝数据,而是共享内部数据
v
和v2
共享同一块内存(payload),里面存着 {10, 20, 30}。- 引用计数(refcount)为 2,表示两个 QVector 对象共享同一数据。
- 这时,内存只保存了一份数据,拷贝成本很低。
只有当你对v
或v2
调用非 const 方法(修改操作)时,如果 refcount > 1,就会触发 detach,深拷贝数据,分配独立内存,避免数据冲突。
总结: - 复制 QVector 很轻量(共享数据 + refcount++)
- 修改共享数据前会触发深拷贝(detach)
这个例子具体展示了隐式共享(copy-on-write)机制在 QVector 修改时的行为:
QVector<int> v {10, 20, 30};
QVector<int> v2 = v; // v2 共享 v 的数据,refcount = 2,payload = {10, 20, 30}
v2[0] = 99; // 修改 v2,第一个元素变成 99
过程分析:
- 初始时,
v
和v2
共享同一份数据(payload),内容是{10, 20, 30}
,引用计数是 2。 - 当执行
v2[0] = 99
这个写操作时,v2
检测到引用计数大于 1(表示数据被共享),触发detach。 - detach 意味着
v2
会进行一次深拷贝,分配自己的内存来存储数据。 - 修改只会影响
v2
,v
依旧保持原数据{10, 20, 30}
。 - 结果是:
v
仍然是{10, 20, 30}
,v2
变成了{99, 20, 30}
,- 两者的数据不再共享,引用计数分别为 1。
这个机制保证了:
- 复制对象时开销很小,都是共享数据。
- 只有写操作时才真正做深拷贝,保证数据安全。
这是 Qt 容器里隐式共享的核心思想,也是性能优化的关键点。
Qt 的 Implicit Sharing(隐式共享) 的总结。以下是对这段内容的详细理解解释:
什么是 Implicit Sharing?
隐式共享是一种 “写时拷贝”(Copy-On-Write, COW) 机制,结合了引用计数和延迟深拷贝的技术,核心目的是:
- 节省内存
- 提高性能
- 简化代码书写
为什么有用?
“This mechanism makes writing code a lot simpler”
这句话的意思是:你可以像写普通值一样写 Qt 的类(例如 QString
, QByteArray
, QVector
等),而不用担心性能问题。
比如:
QString a = "hello";
QString b = a; // 不会拷贝数据,只是增加引用计数
b[0] = 'H'; // 触发深拷贝(detach),a 仍然是 "hello",b 变成 "Hello"
你可以放心用:
- 值返回(
return QString
) - 拷贝赋值(
QString b = a
) - 不需要频繁考虑性能陷阱
哪些 Qt 类支持 Implicit Sharing?
“The great majority of Qt value classes are implicitly shared”
常见支持隐式共享的类包括:
类型 | 是否支持隐式共享 |
---|---|
QString | 是 |
QByteArray | 是 |
QVariant | 是 |
QImage / QPixmap | 是 |
QVector | 是 |
QList | 是 |
QMap / QHash | 是 |
QVarLengthArray | 否 |
例外:
QVarLengthArray
是值语义容器,不走引用计数,它在栈上直接分配数据,所以不隐式共享。
工作原理简述:
- 拷贝对象时,不复制数据,只是引用计数 +1。
- 写入对象时,如果引用计数 > 1,就自动复制一份(detach),以避免影响其他对象。
- 析构对象时,引用计数 -1,当为 0 时释放资源。
总结:
隐式共享是 Qt 的一大特色。它允许你:
- 写出高效、简洁的值语义代码;
- 不担心性能陷阱;
- 但也要注意:写操作(即使看起来是 const)可能隐式触发深拷贝。
“Implicit sharing and containers: where’s the catch?”指出了 Qt 隐式共享容器的一些隐藏陷阱和误区。以下是逐句解析与理解:
隐式共享和容器:问题出在哪?
• Handing out references to data inside a container does not make the container unshareable
意思:
即使你将容器中的元素通过引用的方式返回或传递出去,比如:
QVector<int> v1 = {1, 2, 3};
QVector<int> v2 = v1; // 现在引用计数是 2
int &ref = v1[0]; // 获取引用
此时 v1
仍然是可共享的,Qt 不会自动触发 detach。
也就是说:只是访问引用,不会破坏共享关系。
• It's easy to accidentally detach a container
意思:
一旦你对容器做了写操作(哪怕是间接的),就会触发 detach(深拷贝)。比如:
v2[0] = 100; // 一写就会 detach,变成独立的副本
这种操作很容易发生在你没意识到的地方,从而悄悄改变了对象的共享状态。
• Accidental detaching can hide bugs
意思:
这种悄悄发生的 detach 行为可能导致 bug 被隐藏,因为:
- 你以为两个对象共享同一份数据(如
v1
,v2
),但其实不再共享; - 导致数据不同步、调试困难;
- 在多线程或资源受限环境中尤其危险。
例如:
if (v1 == v2) {doSomething(); // 你以为它们是同一份数据,但可能早就 detach 了
}
• IOW, it's not just about performance
IOW = In Other Words(换句话说)
不是只有性能问题,还是“正确性问题”!
- 深拷贝带来的性能开销固然重要;
- 代码逻辑混乱、共享状态错乱、数据不一致更加危险;
- 这些 bug 可能非常隐蔽,特别是当代码中混入了隐藏的 detach 操作。
• Code polluted by (out-of-line) detach/destructor calls
意思:
编译后的代码里会因为 Qt 的隐式共享机制,出现许多:
- 隐藏的拷贝构造函数调用
- 深拷贝(detach)操作
- 析构函数调用
这会让代码生成“变重”、函数调用栈变复杂,甚至会破坏 inlining,从而降低性能或调试可读性。
总结:使用 Qt 隐式共享容器的注意事项
项目 | 建议 |
---|---|
写入操作 | 明确知道何时触发了 detach |
多对象共享容器时 | 小心副作用、不可预期的独立副本 |
性能敏感代码 | 尽量使用 std::vector 等无隐式共享的 STL 容器 |
传引用/指针访问内部数据 | 知道不会破坏共享状态,但不要写入! |
这段内容解释了 Qt 隐式共享容器(如 QVector
)中一个容易被忽略的陷阱:引用(包括迭代器)不会阻止容器被拷贝(detach),可能导致代码行为与你预期的不同。
下面是逐句解释和深入理解:
Returning references to data inside a container(从容器中返回引用)
“Handing out references to data inside a container does not make the container unshareable”
意思是:
即使你取出了容器中某个元素的引用,这个容器仍然是“共享的”,Qt 不会因为你持有引用而主动拷贝(detach)数据。
也就是说,这不是 COW(Copy-On-Write)触发的条件。
“E.g. of such references: iterators”
像下面这样:
QVector<int> v = {10, 20, 30};
auto it = v.begin(); // 拿到迭代器
int &ref = v[0]; // 或直接取引用
这些引用/迭代器不会改变引用计数,也不会让 Qt 自动 detach。
例子分析:
QVector<int> v {10, 20, 30};
auto &r = v[0]; // 取引用
QVector<int> v2 = v; // 现在 v 和 v2 是共享的,refcount = 2
r = 99; // 修改通过 v 的引用,会影响共享数据!
图示如下(内存共享前):
v 和 v2 共用同一个 payload:
payload = [10, 20, 30]
refcount = 2
当执行 r = 99;
时,没有触发 detach,因为 r
是直接引用底层数据。
结果是:
v = [99, 20, 30]
v2 = [99, 20, 30] <-- 也被修改了!
你以为 v2[0]
还是 10,结果 断言失败:
assert(v2[0] == 10); // fails!
关键陷阱总结:
行为 | 是否触发 detach? | 说明 |
---|---|---|
赋值一个容器 | 不触发 | 引用计数增加 |
调用非 const 成员函数 | 可能触发 | 如 v[0] = 99 ,会自动复制(detach) |
手动获取元素引用然后修改 | 不自动 detach | 数据是共享的,两个容器都会变 |
使用迭代器修改内容 | 不自动 detach | 同样直接影响共享内存 |
正确做法建议:
- 避免持久使用引用或迭代器后再修改容器副本;
- 如果你想“安全修改一个副本”,请手动 detach:
QVector<int> v2 = v; v2.detach(); // 强制深拷贝 v2[0] = 99; // 不会影响原 v
- 使用 STL 容器(如
std::vector
)时,这种问题不会出现,因为没有隐式共享。
Qt 隐式共享容器中「意外的深拷贝(accidental detach)」问题。这是 Qt 使用者常常忽略的一大坑。以下是详细解释:
你需要理解的核心要点:
例子:
QVector<int> calculateSomething();
const int firstResult = calculateSomething().first();
问题分析:
QVector<T>::first(); // 是非 const 的,返回 T&
这就意味着:
calculateSomething()
生成了一个临时的QVector<int>
。.first()
是 非 const 成员函数,所以 Qt 会 触发 detach(即 deep copy 临时对象的数据)。
但其实你并不需要修改这个容器!你只是想拿第一个值!但 Qt 没法知道你的意图,调用了非 const 版本,就要执行 Copy-On-Write。
结果:
你只是想:
const int x = QVector<int>{10, 20, 30}.first();
但 Qt 背后悄悄:
- 创建 QVector 临时对象
- 进行一次深拷贝(detach)
- 然后返回引用(其实没用到)
这就引发了不必要的内存分配和复制——而你完全没有意识到!
正确的写法:
const int firstResult = calculateSomething().constFirst();
这样:
.constFirst()
是const
成员函数- 不会触发 detach
- 没有不必要的深拷贝
- 返回值仍然是你需要的第一个元素(但是只读的)
总结:Qt 中意外 detach 的教训
错误写法 | 原因 | 正确写法 |
---|---|---|
v.first() | 非 const 成员函数,可能 deep copy | v.constFirst() |
v.last() | 同上 | v.constLast() |
v[i] | 返回引用,非 const,可能 detach | v.at(i) 或 const auto val = v[i]; |
怎么发现这些问题?
- 它们在编译时不会报错;
- 但会在 heap profiler(如
massif
,heaptrack
)中看到内存突增; - 一旦你分析出代码里这些细节,会发现许多“不该拷贝的地方在偷偷拷贝”。
Qt 容器的隐式共享(implicit sharing)机制以及 accidental detach(意外拷贝) 带来的陷阱。现在我们来逐条解析你说的这个更严重的问题:
1. Accidental Detach 导致的 Bug(不是性能问题,而是逻辑错误)
场景代码:
QMap<int, int> map;
// ...
if (map.find(key) == map.cend()) {std::cout << "not found" << std::endl;
} else {std::cout << "found" << std::endl;
}
问题来了:
map.find(key)
是一个 非常量成员函数(non-const);- 它可能导致容器 detach,即做一次深拷贝(复制 pimpl)。
- 而你已经提前调用了
map.cend()
,它指向 原始容器的末尾; - 之后容器被 detach 成新副本,
map.find()
返回的是 新副本的迭代器; - 两个 end 迭代器 来自不同的容器副本,它们 不相等!
结果:
哪怕 key 根本就不在原始 map 中,也可能打印:
found
这就不是性能问题了,而是一个 逻辑 Bug,非常隐蔽、危险!
正确做法:使用 const 方法
if (map.constFind(key) == map.cend()) {std::cout << "not found" << std::endl;
}
或者,等价更安全:
if (!map.contains(key)) {std::cout << "not found" << std::endl;
}
总结建议:Qt 容器 + 隐式共享 + 非 const 方法 = 潜在 Bug
场景 | 错误方法 | 原因 | 正确做法 |
---|---|---|---|
查找元素 | map.find(key) | 可能触发 detach,破坏逻辑判断 | map.constFind(key) |
访问第一个元素 | v.first() | 非 const 方法可能导致 deep copy | v.constFirst() |
遍历时比较 | 混用 iterator 和 const_iterator | 来自不同容器副本,比较结果错误 | 统一用 const_iterator |
🗣 标准库的立场 vs Qt 的立场
C++ STL | Qt |
---|---|
避免隐式共享机制(copy-on-write) | 依赖 implicit sharing |
强调明确语义、值语义 | 更偏重方便与 API 一致性 |
更安全、更一致 | 更方便、更快捷,但埋雷多 |
如果你开发的是性能敏感或逻辑严谨的模块(比如底层库、工具链),建议: |
- 尽量使用 STL 容器;
- 只在与 Qt API 交互时使用 Qt 容器;
- 严格区分 const 和非 const 使用;
- 使用静态分析工具(如 Clazy)来检测 Qt-specific misuse。
千万不要再用 foreach
或 Q_FOREACH
!
原因总结:
foreach (var, container)
等价于以下代码:
{const auto _copy = container; // 拷贝了整个容器!auto it = _copy.begin(), end = _copy.end();for (; it != end; ++it) {var = *it;body;}
}
严重问题:
1. 容器整体拷贝一次:
即使你只想遍历,但 Qt 容器采用隐式共享机制(copy-on-write),会把整个容器 复制一份!这完全是你意料之外的。
2. 逻辑错误隐患:
容器变了,你拿到的是副本,里面元素可能不对,还以为遍历的是原始容器。
3. 性能问题非常严重:
在有大量数据或频繁迭代的场景下,每次循环都在悄悄 deep copy。
正确做法:使用 C++11 的 range-based for
for (const auto& value : container) {// Safe, efficient, no copy
}
- 不会触发隐式深拷贝
- 更现代,更清晰
- 完美支持 STL 和 Qt 容器(如
QVector
,QStringList
)
尤其小心 Qt 容器
由于 Qt 容器隐式共享(implicit sharing)+ Q_FOREACH 的复制行为,一起使用等于 踩雷必炸。
例如:
QStringList list = {"a", "b", "c"};
foreach (QString s, list) {// 修改 s 没问题,但 list 是被拷贝的副本
}
你以为你在修改 list
,其实根本没改到!
永久禁用建议(企业/团队级别)
在 .pro
或 CMakeLists.txt
中添加:
DEFINES += QT_NO_FOREACH
这会在你代码中使用 foreach
/ Q_FOREACH
时 编译失败。
总结
特性 | Q_FOREACH | C++11 range-for |
---|---|---|
是否复制容器 | 会复制 | 不会 |
支持 STL 容器 | 否 | 是 |
是否安全高效 | 否 | 是 |
是否推荐使用 | 完全不推荐 | 强烈推荐 |
foreach
/ Q_FOREACH
使用建议总结
“优点”(其实是个误导):
- “可以安全修改原容器”
其实是误导:因为循环中你操作的是拷贝副本,不是原容器。真正修改容器行为反而不可控、不清晰。
缺点(致命):
缺点 | 说明 |
---|---|
总是复制容器 | Q_FOREACH 会拷贝一份容器(无论你愿不愿意),即使你只读。对 STL 容器非常昂贵 |
拷贝的是 const 容器 | 无法修改容器元素(因为元素是 const) |
可读性差 | 隐式语义不清楚,容易出 bug |
与现代 C++ 不兼容 | 不支持 STL 容器,不支持迭代器,不支持 structured bindings 等 |
将在 Qt 6 中被移除 | 官方明确计划废弃此功能 |
最佳实践
- 使用 C++11 的 range-based for:
for (const auto& item : container) {// 安全、高效、清晰
}
- 禁用 Q_FOREACH / foreach:
在项目中定义:
DEFINES += QT_NO_FOREACH // for qmake
# 或
add_definitions(-DQT_NO_FOREACH) // for CMake
这将使编译器在你使用 foreach
时直接报错,强制你使用现代 C++。
额外建议
替换建议 | 替代语法 |
---|---|
foreach (auto x, list) | for (const auto& x : list) |
foreach (QString s, strings) | for (const QString& s : strings) |
foreach (int i, QVector<int>) | for (int i : QVector<int>) |
如果你愿意,我可以: |
- 扫描一个项目中的所有
foreach
并一键转换为 C++11 语法; - 或者写一个
clang-tidy
规则 / Python 脚本帮助自动替换。
这一节讲的是 range-based for 循环(基于范围的 for 循环) 在 Qt 和 STL 容器上的行为细节,尤其是它可能引发的 隐式分离(implicit detach) 问题。
Range-based for 的真实展开形式:
for (var : container) body;
等价于:
{auto &&c = (container);auto i = begin(c);auto e = end(c);for (; i != e; ++i) {var = *i;body;}
}
STL 容器(如 std::vector<T>
)的行为
i
和e
是std::vector<T>::iterator
- 如果你不在
body
中修改元素或容器,没有副作用。 - 这也是现代 C++ 推荐的方式。
Qt 容器(如 QVector<T>
)的行为
i
和e
是QVector<T>::iterator
,不是 const_iterator。- 即使你不在
body
中修改容器,也可能导致 隐式 detach:因为 Qt 容器在调用
non-const
成员函数(如begin()
和end()
)时,如果 refcount > 1,会触发深拷贝(detach)。
例如:
QVector<QString> v = ...;
for (const auto& s : v) {qDebug() << s;
}
乍一看没问题,但 v.begin()
和 v.end()
是 非 const 成员函数,可能导致:
- 性能开销:触发 deep copy
- 行为变化:影响共享数据的其它副本
正确做法(避免 detach)
推荐方式:明确使用 const&
或 const_iterator
:
// 使用 const 引用,避免 detach
for (const QString& s : v) {qDebug() << s;
}
或者,如果你写模板代码,优先使用 const QVector<T>&
参数,这样 begin()
会是 const_iterator
,避免 detach。
小结:如何安全使用 range-based for?
情况 | 建议 |
---|---|
使用 STL 容器 | 直接使用 range-based for,安全高效 |
使用 Qt 容器 | 容器变量加 const& ,元素加 const& |
不确定是否 detach | 用 const_iterator 避免陷阱 |
这一部分是对 Qt 的 Q_FOREACH
和 C++11 的 range-based for loop 在使用 Qt 容器和 STL 容器时的行为差异总结。下面帮你归纳一下关键点:
range-based for
vs Q_FOREACH
对比总结
容器类型 | Q_FOREACH | range-based for (auto & : c) | range-based for (const auto & : c) |
---|---|---|---|
Qt 非 const | OK(cheap) | 可能会 detach(非 const 迭代器) | 可能会 detach(begin() 不是 const) |
Qt const | OK(cheap) | 不会 detach(const 迭代器) | 不会 detach(const 迭代器) |
STL 非 const | 会 deep copy(复制一份) | OK | OK |
STL const | 会 deep copy | OK | OK |
为什么有这些区别?
Q_FOREACH
的问题:
- 始终复制容器(哪怕是 const 容器),对于 STL 容器来说是灾难性的(深拷贝)。
- 对 Qt 容器没什么问题,因为 Qt 使用了 implicit sharing,所以复制是廉价的。
- 缺点是代码难以推理,性能不透明,因此 Qt 6 已废弃
Q_FOREACH
。
range-based for
的细节:
for (auto &item : container) {// ...
}
- 如果
container
是 Qt 非 const 容器:- 调用的是
begin()
和end()
,它们是 非常量成员函数。 - 如果容器被共享(refcount > 1),会发生 detach(深拷贝)。
- 调用的是
- 如果
container
是const
,就会使用const_iterator
,不会触发 detach。
实践建议(写 Qt 代码时):
- 避免使用
Q_FOREACH
,在项目中定义:#define QT_NO_FOREACH
- 优先使用 range-based for,但要注意:
const QVector<int> vec = ...; for (const auto &x : vec) { // 安全,不会 detach... } QVector<int> vec = ...; for (auto &x : vec) { // 可能 detach(如果被共享)... }
- 如果一定需要修改容器或元素,考虑:
- 保证容器未共享;
- 或使用
detach()
手动控制。
- 非 const Qt 容器使用 range-based for 循环时,要小心可能触发隐式 detach。
- 如果不修改容器,尽量用 const 容器或者通过
qAsConst()
(Qt5.7起)或std::as_const()
(C++17起)将容器转换为 const。 - 不能对临时(rvalue)直接使用
qAsConst()
,这种情况下先用 const 引用绑定,再循环。
这样可以避免不必要的深拷贝,提升性能,且代码更安全。
Clazy,它是基于 Clang 的开源静态分析工具,专门针对 Qt 代码。总结一下:
- Clazy 类似于 clang-tidy,但聚焦于 Qt 风格和 Qt 特有的坑。
- 它自带 50+ 规则检查,比如:
- detaching-temporary(检测隐式 detach 相关问题)
- strict-iterators(检测迭代器使用)
- missing-typeinfo(缺少类型信息)
- foreach(检测不建议用的 Qt foreach)
- 它还能自动提供 fix-it,帮你自动改代码。
- 即使是 Qt 自己的代码库,也有不少问题被 Clazy 检测出来。
- 建议定期用 Clazy 扫描你的代码,修复警告,提升代码质量和性能。
这工具对保持 Qt 代码库的健康和现代化很重要,尤其是避免隐式 detach 等细节导致的性能问题。
Qt 字符串类创建方式
常见的创建字符串的方式很多:
- 直接字符串字面量
"string"
QByteArray("string")
—— 字节数组,无编码信息QByteArrayLiteral("string")
—— 编译时常量,不分配内存QString("string")
—— UTF-16编码字符串,分配内存QLatin1String("string")
—— 轻量视图,适合拉丁1编码QStringLiteral("string")
—— 编译时UTF-16常量,不分配内存(Qt 5.9+)QString::fromLatin1("string")
—— 从Latin1编码构造QString::fromUtf8("string")
—— 从UTF-8编码构造tr("string")
—— 用于国际化的字符串QStringView(u"string")
—— 轻量视图,不分配内存
QByteArray
- 表示字节序列,类似
std::string
,不包含编码信息 - 隐式共享(copy-on-write)
- 构造函数会分配内存
QByteArray::fromRawData()
可以避免部分分配QByteArrayLiteral()
不分配内存,存储于只读段,适合静态数据
QString
- 使用 UTF-16 编码,支持 Unicode 操作(优于
std::u16string
) - 隐式共享
- 构造函数会分配内存
QString::fromRawData()
可以避免分配(只读视图)- 推荐使用
QStringView
作为轻量字符串视图 QStringLiteral()
自 Qt 5.9 起不分配内存,数据存储在只读段,适合字符串常量
总结:- 用 Qt 来管理 Unicode 字符串,选
QString
;如果只读且想避免拷贝,选QStringView
或QStringLiteral
。 - 用
QByteArray
处理原始字节流或二进制数据。
“Latin1” 是“ISO 8859-1”编码的简称,全称是 ISO/IEC 8859-1: Latin Alphabet No. 1。
简单来说: - 它是一种单字节字符编码,使用 1 个字节(8 位)表示一个字符;
- 能表示西欧主要语言的字符集,比如英语、法语、德语、西班牙语等;
- 范围覆盖了 0x00 到 0xFF 共 256 个字符,其中前 128 个字符和 ASCII 码完全一样;
- 它不支持像中文、日文、韩文等复杂字符,只适合基本拉丁字母和西欧符号;
- 在 Qt 里,
QLatin1String
是对 Latin1 编码字符串的轻量包装,用来高效处理这类字符串,避免转码成本。
总结:Latin1 是一种旧式的、西欧字符编码,适合只包含拉丁字母的文本,不支持多语言 Unicode。
简单总结一下 QLatin1String
的作用和特点:
- 它是一个轻量的字符串包装类,只包含一个
const char*
指针和字符串长度,不做内存管理; - 主要用来表示 Latin1(ISO 8859-1)编码的字符串字面量,比如代码里的
"foo"
; - 用于
QString
相关函数的重载,避免不必要的临时QString
分配和转换,提高性能; - 例如:
QString::startsWith()
同时有两个版本,一个接受QString
,一个接受QLatin1String
,后者性能更好,因为不产生临时字符串。
你可以把它看成是 Qt 里对纯 ASCII 或 Latin1 字符串字面量的一个“快捷通道”,用来减少字符串转换和内存开销。
总结一下这段内容的重点:
- Qt 的主要字符串类是
QString
和QByteArray
。 - 这几年对它们的改进不多,保持了比较稳定的设计。
- 从 Qt 5.9 开始,
QStringLiteral
和QByteArrayLiteral
这两个宏的实现优化了——它们不会再动态分配内存,而是直接使用编译时生成的静态内存,这样可以显著提升性能。
简单总结一下 QStringView
:
- 从 Qt 5.10 引入的类型,是一个 非拥有(non-owning) 的字符串视图,类似于 C++17 标准的
std::u16string_view
。 - 它直接指向一段 UTF-16 编码的字符序列(比如
QString
内部存储格式),但不负责管理这段内存。 - 这样可以避免不必要的字符串拷贝,提升性能,特别适合只读访问场景。
- 它提供了和
QString
大部分相似的只读接口,方便使用。 - Qt 5.11 之后还会有更多的 API 和对
QStringBuilder
的支持,使用体验会更好。
这段讲的是用 QStringView
作为函数参数类型的理由和好处,重点如下:
- 主要用途:
QStringView
适合作为函数参数,尤其是函数需要读取字符串但不需要保留它时。 - 如果函数需要一个 Unicode 字符串参数,而且函数不会保存这个字符串,推荐用
QStringView
,避免无谓的拷贝和分配。 - 讨论了一个例子:
Document::find(StringType substring)
,StringType
用哪个类型好?QString
会强制调用者提供一个完整的QString
,可能要动态分配内存,或者使用QStringLiteral
编译期字符串。QByteArray
不是 Unicode 安全的,不适合处理 Unicode 文本。QLatin1String
虽然性能好(因为是 Latin-1 编码),但不 Unicode 安全,不过可以做为额外重载实现快速路径。
总结:QStringView
既支持 Unicode,也避免了字符串不必要的复制,非常适合作为 API 中接受字符串参数的接口类型。
这部分内容强调了 QStringView
作为接口类型的优势:
QStringView
是 Unicode 安全的(支持 UTF-16 编码)。- 它不会进行内存分配(alloc-free),性能好。
- 它可以从多种字符串源构造:
- 编译时的字符串字面量(
u"compile time"
) - 动态分配的
QString
对象 - 甚至是一个大字符串的子串(通过
QStringView(bigString, 40)
)
举例:
- 编译时的字符串字面量(
class Document {iterator find(QStringView substring);
};
Document d;
d.find(u"compile time"); // 传入编译时字符串字面量
QString allocatedString = "...";
d.find(allocatedString); // 传入动态分配的QString
d.find(QStringView(bigString, 40)); // 传入大字符串的子串视图
另外,QStringView
还能作为“零分配”切割字符串的工具,比如:
QString str = "...";
QRegularExpression re("reg(.*)ex");
QRegularExpressionMatch match = re.match(str);
if (match.hasMatch()) {QStringView cap = match.capturedView(1); // 直接获取子串视图,无需分配内存// ...
}
总结:QStringView
让字符串处理既高效又安全,非常适合作为函数参数和字符串子串的视图类型。
这一部分讲的是 QStringView
对 Qt API 的巨大影响:
- 许多 Qt 函数现在接受
QString
参数,但实际上并不需要持有字符串数据,只是读取它们。 - 在 Qt 6 中,应该将这些函数改为接受
QStringView
,这样避免不必要的内存分配和拷贝,提高性能。 - Qt 5 里还有一个类似的非拥有字符串视图类型叫
QStringRef
,但它设计有缺陷:- 它必须绑定到一个
QString
对象,而不能直接表示任何 UTF-16 字符序列。 - 因此灵活性较差。
- 它必须绑定到一个
QStringRef
只是权宜之计,建议如果迫切需要用字符串视图,可以暂时用它,但随着QStringView
在功能上达到 API 完整性,QStringRef
会被废弃。
总结:QStringView
是更现代、更高效的字符串视图接口,未来 Qt 版本将以它为标准,替代旧的字符串引用方式。
POD(Plain Old Data) 平凡数据类型 类型是否等同于“可搬移”(relocatable)?
- 答案是否定的:POD和可搬移是两个独立的概念。
- 一个类型是否可搬移(relocatable)与它是否是POD类型无关。
- 可搬移类型可能有非平凡的构造函数和析构函数,比如 Qt 里基于pimpl(指针实现)的值类。
- 反过来,即使是平凡(trivial)的类型,也未必可搬移——比如某些类型的对象地址本身代表身份(identity),搬移会破坏语义。
- 所有C数据类型都是trivial,但不一定是relocatable。
另外,关于Qt中废弃的API(deprecated APIs): - Qt会标记旧的API为废弃,虽然它们仍然可用且通过测试,但Qt 6版本会移除大部分废弃API。
- Qt源码中通过宏
QT_DEPRECATED_SINCE(major, minor)
来标记,并用QT_DEPRECATED_X("建议替代方法")
生成编译警告。 - 你可以通过定义宏来控制废弃API的使用:
- 定义
QT_DEPRECATED_WARNINGS
来开启废弃API的警告。 - 定义
QT_DISABLE_DEPRECATED_BEFORE=版本号
来将早于某版本的废弃API使用视为错误。
例如:
- 定义
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x050900
这样就强制不允许使用 Qt 5.9.0 及更早版本中废弃的API。
总结:
- POD ≠ relocatable,二者语义不同。
- Qt鼓励逐步迁移,避免使用废弃API,尤其是升级到Qt 6时。
QList 的核心问题是:
- 它是一个“基于数组”的容器,但内部实现是一个
void*
指针数组。 - 根据存储的类型不同,QList 可能存放指向元素的指针(每个元素单独堆分配),也可能直接存放元素本身。
- 这种设计导致:
- 每个元素单独堆分配时性能和内存效率都很差。
- 元素存储方式依赖于平台(32位 vs 64位)和元素类型,行为不稳定难以预测。
- 对于小且可搬移的数据类型(如 int 在64位平台)非常浪费空间。
- QList 优化了前置插入操作(prepend),但代价较大。
- 尽管有这些问题,QList仍然是Qt API中最常暴露的容器之一。
总结:
不要在自己的代码中使用 QList,推荐使用 QVector 或 STL 容器,除非必须和 Qt API 交互。
总结下 QList 和 QVector 的区别和使用建议:
- 推荐使用 QVector,除非你必须调用需要 QList 的 Qt API。
- QVector 通常生成更少的代码,性能也更好。
- QVector 在大多数操作上比 QList 快,唯一例外是:
- 经常在前面插入元素时,QList表现可能更好。
- 对非常大的对象进行重新分配时,QList可能更合适。
- QVector 重新分配时,不保证引用或指针的有效性(会失效)。
- 如果需要引用或指针保持有效,建议用指针的容器,比如
QVector<T*>
。
简单说就是:
绝大多数情况下用 QVector,只有少数场景考虑 QList。