Qt 常用数据类型与容器:QString、QList、QVector、QMap 的性能与实现分析
引言
第五篇中,我们把 Qt Core 理解为 Qt 的基础能力层。本篇进入其中最常见、也最容易被“凭印象选错”的一组类:QString、QList、QVector、QMap。
它们看起来像是标准库 std::string、std::vector、std::map 的另一套写法,但 Qt 6 容器有自己的设计重点:Unicode 文本、与 Qt API 的天然协作,以及隐式共享(copy-on-write,写时复制)。这带来了轻量值传递的便利,也带来了第一次写入可能发生整块复制的成本。
本篇按下面的顺序展开:先会用,再与标准库对照,最后通过 Qt 6.8.3 源码理解内存和性能。读完后,至少应能回答这几个问题:
- 中文、文件名、界面文本为什么优先用 QString?
- Qt 6 中还要不要纠结 QList 与 QVector 的性能差异?
- QMap 为什么能按键排序,它什么时候不应该代替 QHash?
- 一个看似简单的赋值,为什么有时几乎不分配内存,有时第一次修改会变慢?
本文讨论的源码版本为 Qt 6.8.3。 Qt 6 背景下,文中关于 QList / QVector 的结论不适用于 Qt 5。
一、先建立容器地图:先看数据关系,再选类型
QString 不是“字符数组”,QMap 也不是“任何键值对都适用的容器”。先从数据关系入手,会比背类名可靠得多。

图 1:四个主角都来自 Qt Core。注意:Qt 6 中 QVector<T> 是 QList<T> 的类型别名。
| 保存、拼接、搜索 Unicode 文本 | QString | std::string、std::u16string | UTF-16 代码单元,Qt 文本 API,隐式共享 |
| 按下标保存一串同类型元素 | QList<T> | std::vector<T> | 连续存储、随机访问、隐式共享 |
| 旧代码中写了“向量”语义 | QVector<T> | std::vector<T> | Qt 6 中就是 QList<T> |
| 按键查值,且需要按键有序遍历/范围查询 | QMap<Key, T> | std::map<Key, T> | 有序键值对、隐式共享 |
本篇先只比较这四类。若需求是“海量键值查找但不关心排序”,请优先想到 QHash 或 std::unordered_map;它们会在关联容器章节单独展开。
最小 CMake 依赖
这四个类都属于 Qt Core:
find_package(Qt6 REQUIRED COMPONENTS Core)
target_link_libraries(MyApp PRIVATE Qt6::Core)
使用时按需包含头文件,而不是依赖某个“大而全”的头文件:
#include <QString>
#include <QList>
#include <QMap>
QVector 在 Qt 6 中由容器前置声明头间接定义为 QList 的别名;在实际代码中仍可显式 #include <QVector>,这样意图最清楚。
二、QString:先把“文本”与“字节”分开
2.1 最常用的写法
界面显示文字、用户输入、路径名、JSON 文本字段等,优先使用 QString。下面的例子包含创建、拼接、格式化、搜索和 UTF-8 边界转换:
#include <QString>
#include <QDebug>
QString userName = QStringLiteral("程与留");
int score = 95;
// arg() 按占位符替换,比手工拼接数字和多语言文本更清晰。
QString message = QStringLiteral("用户 %1 的得分是 %2")
.arg(userName)
.arg(score);
if (message.contains(QStringLiteral("得分"))) {
qDebug() << message;
}
// 外部协议、文件或 C API 常以 UTF-8 字节交互。
QByteArray utf8 = message.toUtf8();
QString restored = QString::fromUtf8(utf8);
这里的边界很重要:QString 表示文本,QByteArray 表示字节序列。网络报文、哈希结果、压缩数据不应该因为“也能放进字符数组”就塞进 QString。
2.2 为什么使用 QStringLiteral
对于代码中的固定文本,推荐使用 QStringLiteral:
const QString title = QStringLiteral("设备状态");
它可以让字符串字面量直接以 Qt 所需的字符形式构造 QString,避免先经过普通窄字符串再转换的路径。对于源码中的固定文本,这种写法也能明确表达其 Qt 字符串语义。
但不要为了形式统一而把所有内容都写成 QStringLiteral。来自网络、文件、C API 的 UTF-8 数据,应明确写出编码转换:
const char *payload = "{\\"name\\":\\"Qt\\"}";
QString text = QString::fromUtf8(payload);
2.3 与 std::string 的第一轮对比
std::string 是字节字符串。它可以保存 UTF-8,但类型本身并不知道这些字节是否是 UTF-8,也不会替你按 Unicode 语义处理它们。QString 则把 Qt 的文本 API 统一建立在 Unicode 之上。
| 主要定位 | Qt Unicode 文本 | 一串 char 字节 |
| 内部编码 | UTF-16 代码单元 | 未规定,常用约定是 UTF-8 |
| Qt API 协作 | 直接传给控件、文件、JSON 等 Qt API | 经常需要转换 |
| 常见格式化 | arg() | std::format(C++20)或流/第三方库 |
| 复制语义 | 隐式共享,写入时分离 | 常规值复制,具体实现可有 SSO |
| 适合场景 | UI、路径、翻译、Qt 业务文本 | 协议字节、标准库优先的纯 C++ 库 |
这并不表示 QString 在任何场景都优于 std::string。如果一个跨平台算法库完全不依赖 Qt,接口使用 std::string 可以降低依赖;如果协议明确规定 UTF-8 字节,则保留 std::string 或 QByteArray 往往更直接。提升不在于“类更多”,而在于在 Qt 程序中少写不必要的编码转换,并获得一致的 Unicode 文本语义。
2.4 一个 Unicode 容易踩的坑:size() 不是“用户看到的字符数”
QString::size() 和 length() 返回的是 UTF-16 代码单元数量。大多数常见中文字符通常占一个 QChar,但某些 Emoji 和扩展字符需要代理对,会占两个 QChar;带组合附加符的字符也可能由多个代码点组成。
QString text = QString::fromUtf8("A😀");
qDebug() << text.size(); // 通常输出 3:A 为 1,😀 为 2 个 UTF-16 代码单元
因此不要用 size() 直接实现“限制用户输入 10 个可见字符”这类需求。需要按用户感知的字符(grapheme cluster)处理时,应使用 Qt 的文本边界分析能力,而不是把 QChar 数量当成人类字符数。
2.5 只读视图:QStringView
当函数只读取一段文本、不需要拥有它时,QStringView 是减少临时分配的工具,作用类似 std::string_view:
#include <QStringView>
bool isConfigFile(QStringView fileName)
{
return fileName.endsWith(u".ini", Qt::CaseInsensitive);
}
QString name = QStringLiteral("settings.ini");
bool matched = isConfigFile(name);
QStringView 不拥有数据,不能保存到原始字符串生命周期之外。它是“只读借用”,不是 QString 的更快替代品。
QStringView view;
{
QString text = QStringLiteral("Hello");
view = text;
}
// view 已经悬空,不能再使用
QStringView 本身不拥有字符串数据,因此它的生命周期不能超过被引用的 QString。
三、QList:Qt 6 的主力顺序容器
3.1 从增删改查开始
QList<T> 保存有顺序的一组 T。它支持下标随机访问、范围 for、尾部追加和中间插入。
#include <QList>
#include <QString>
#include <QDebug>
QList<QString> devices = {
QStringLiteral("温度传感器"),
QStringLiteral("压力传感器")
};
devices.append(QStringLiteral("流量传感器"));
devices.insert(1, QStringLiteral("湿度传感器"));
devices.removeAt(0);
for (const QString &device : devices) {
qDebug() << device;
}
if (!devices.isEmpty()) {
qDebug() << "第一项:" << devices.front();
}
初学阶段可以先记住:
- 能够合理预估最终数量、且容器会持续增长时,可以先 reserve();
- 尾部 append() / push_back() 是顺序容器的常见高效操作;
- 中间 insert()、removeAt() 需要移动后续元素,别在大循环中滥用;
- 只读遍历写成 const T &,避免元素逐个复制。
QList<int> samples;
samples.reserve(10'000);
for (int i = 0; i < 10'000; ++i) {
samples.append(i);
}
3.2 与 std::vector 对比
从“顺序、连续、可按下标访问”的能力上看,Qt 6 的 QList<T> 与 std::vector<T> 很接近:
| operator[] / at() | O(1) | O(1) | 连续内存上的随机访问 |
| 尾部追加 | 均摊 O(1) | 均摊 O(1) | 容量不足时需要扩容 |
| 中间插入、删除 | O(n) | O(n) | 需要移动一段元素 |
| 遍历 | O(n) | O(n) | 都适合缓存友好的线性遍历 |
| 预留容量 | reserve(n) | reserve(n) | 提前避免多次扩容 |
| 复制 | 通常先共享,写时复制 | 复制元素 | 这是最明显的语义差别 |
std::vector 的优势是标准、语义直接、没有隐式共享造成的“第一次写入”成本,也更自然地融入标准算法、std::span、Allocator 等生态。QList 的提升在于与 Qt 类型和 Qt API 的协作,并支持轻量的值传递。
例如下面两个函数都合理,关键是看模块边界:
// Qt UI / 应用层:容器会传入 Qt 模型、信号槽或 Qt API。
void updateDeviceNames(const QList<QString> &names);
// 与 Qt 无关的通用算法库:只暴露标准库类型。
double average(std::span<const double> values);
不要因为项目“使用 Qt”就把纯算法层的每一个 std::vector 都替换为 QList。类型应该服从依赖边界。
3.3 QList 的 at() 与 operator[]
两者都用于按下标取元素,但使用方式不同:
QList<int> values = {10, 20, 30};
int value = values.at(1); // 只读值
values[1] = 200; // 可写访问,可能触发 detach()
operator[] 返回可写引用;当容器与其他对象共享数据时,这个可写入口必须先保证“本对象独占数据”。这正是后文隐式共享的关键。
索引正确性仍然是调用者的责任。at() 可以在调试环境下通过断言帮助发现越界,但它并不是一种“越界后返回错误状态”的安全访问机制。索引越界本身仍然表示程序逻辑错误。
四、QVector:Qt 6 中不再是另一种实现
⚠️ Qt 5 与 Qt 6 的重要区别
这是从 Qt 5 学习资料过渡到 Qt 6 时最重要的一条结论:
// Qt 6.8.3,src/corelib/tools/qcontainerfwd.h 第 39 行
template<typename T> using QVector = QList<T>;
也就是说,在 Qt 6 中:
QList<int> list;
QVector<int> vector;
二者是同一个底层类型,不存在“QVector 连续存储、QList 链表存储,所以必须选前者”的 Qt 5 时代结论。性能、内存布局、成员接口都不该成为二选一的理由。
什么时候保留 QVector 这个名字?
- 老代码、公开接口或业务语义已经使用 QVector,保留它可减少无意义的改名;
- 你希望读者一眼看出“这是一组按下标访问的数值序列”;
- 新项目团队只想统一一种写法,则直接统一为 QList 也完全合理。
真正需要统一的是团队接口风格,不是性能。新旧资料若告诉你“Qt 6 必须选 QVector 才连续”,应先检查它讨论的是不是 Qt 5。
五、QMap:按键有序的键值容器
5.1 基本使用
QMap<Key, T> 将每个键映射到一个值,并且按键排序。下面用设备 ID 保存状态:
#include <QMap>
#include <QString>
#include <QDebug>
QMap<int, QString> states;
states.insert(1003, QStringLiteral("离线"));
states.insert(1001, QStringLiteral("运行中"));
states.insert(1002, QStringLiteral("告警"));
// value() 查询不到时返回默认值,不会修改容器。
QString state = states.value(1002, QStringLiteral("未知"));
// QMap 按 key 升序迭代:1001、1002、1003。
for (auto it = states.cbegin(); it != states.cend(); ++it) {
qDebug() << it.key() << it.value();
}
5.2 value() 与 operator[]:一个常见逻辑 bug
查询时优先使用 contains()、value() 或 constFind()。不要在只想读取时顺手写 operator[]:
QMap<QString, int> counters;
int readOnly = counters.value(QStringLiteral("ok"), 0); // 不插入
counters[QStringLiteral("ok")] += 1; // 不存在时插入值初始化的 int,再写入
非 const 的 operator[] 必须返回 T &,所以键不存在时会插入一个默认构造的值。它适合“取出并准备修改”,不适合“查一下有没有”。这不仅影响业务数据,也可能触发隐式共享分离。
5.3 与 std::map 的对比
QMap 和 std::map 都是有序关联容器,典型查找、插入、删除复杂度为 O(log n)。二者的主要差异如下:
| 键顺序 | 按 operator< / 比较器有序 | 按比较器有序 |
| 查找、插入、删除 | 典型 O(log n) | 典型 O(log n) |
| 底层实现(Qt 6.8.3) | 包装 std::map<Key, T> | 标准库平衡树实现 |
| 复制 | 隐式共享 | 复制各节点 |
| 与 Qt API 协作 | keys()、values()、Qt 元类型与序列化场景方便 | 标准算法和泛型生态自然 |
| 内存特点 | 一份共享控制块 + 树节点;写时可能整体复制 | 每个元素通常一个树节点 |
QMap 的“有序”不是附加装饰,而是选型理由。按时间区间、按 ID 排序显示、使用 lowerBound() / upperBound() 查询一个键区间时,它非常合适:
QMap<int, QString> tasks;
tasks.insert(10, QStringLiteral("采集"));
tasks.insert(20, QStringLiteral("校验"));
tasks.insert(30, QStringLiteral("入库"));
auto first = tasks.lowerBound(15);
auto last = tasks.upperBound(30);
for (auto it = first; it != last; ++it) {
qDebug() << it.key() << it.value(); // 20、30
}
若只是按键快速定位、完全不关心顺序和范围查询,应比较 QHash / std::unordered_map,而不是先用 QMap 再抱怨 O(log n)。
六、性能对比:先看操作复杂度,再看内存行为
“哪个容器快”没有脱离场景的答案。性能至少由四件事共同决定:数据规模、操作分布、元素类型、是否发生分离或扩容。
6.1 一张实用的复杂度表
| QString | O(1) 访问 UTF-16 代码单元 | 均摊 O(1) | O(n) | 文本搜索通常 O(n) | 按代码单元 |
| QList<T> / QVector<T> | O(1) | 均摊 O(1) | O(n) | 不适用 | 插入顺序 |
| std::vector<T> | O(1) | 均摊 O(1) | O(n) | 不适用 | 插入顺序 |
| QMap<K,V> | 不适用 | 不适用 | O(log n) | O(log n) | 按键有序 |
| std::map<K,V> | 不适用 | 不适用 | O(log n) | O(log n) | 按键有序 |
这里的“均摊 O(1)”有前提:扩容不是每一次 append() 都发生,但容量用尽的那一次会重新分配并迁移元素。可预估数量时,reserve() 是最直接、最可解释的优化。
6.2 内存开销:不要只看 sizeof(对象)
容器对象本身通常只保存少量状态或数据指针;真正的内存主要来自堆上的数据块、预留容量和元素自身。下面的判断比硬背“某个类占多少字节”更有用:
| QString | UTF-16 数据,约 capacity × 2 字节,再加结尾 NUL 和头部 | UTF-16 数据通常按每个代码单元 2 字节存储;BMP 范围内的常见中文通常占 1 个代码单元,而部分 Emoji 等字符需要 2 个 UTF-16 代码单元,即通常占 4 字节。 |
| QList<T> / QVector<T> | 连续的 T 数组,约 capacity × sizeof(T) | 数据块头部、对齐和可能的空闲容量;复杂 T 的内部内存另算 |
| QMap<K,V> | std::map 的一个树节点一组键值 | 节点指针、颜色/平衡信息、分配器对齐使每元素开销明显大于连续数组 |
| std::vector<T> | 连续的 T 数组 | 典型三指针状态 + 容量冗余;具体对象大小由实现决定 |
| std::map<K,V> | 每个元素的树节点 | 与 QMap 的树节点成本同类;大量小元素时局部性较差 |
因此,不要把 capacity() 与已使用元素数 size() 混为一谈:
QList<int> values;
values.reserve(1'000);
values.append(1);
qDebug() << values.size(); // 1
qDebug() << values.capacity(); // 至少可容纳 1,000 个元素
预留容量是在用可能的空闲内存换更少的重新分配。长生命周期容器如果后续不再增长、且内存紧张,可酌情 squeeze();不要在普通循环中频繁调用它,因为压缩自身也可能重新分配。
这里可能会有人问reserve在进行预留容量,那么resize是在做什么?
QList<int> values;
values.reserve(100);
// size() == 0
// capacity() >= 100
values.resize(100);
// size() == 100
reserve() 管的是“预留空间”,resize() 管的是“元素数量”。
6.3 隐式共享如何改变“复制”的成本
本文讨论的几个 Qt 类型都采用隐式共享机制。复制对象时,两个值先引用同一份只读数据;任一方要写入时,Qt 为写入方复制出独立数据。这既保留值语义,也避免只读传递时的深拷贝。

图 2:复制不等于立刻复制全部元素。第一次可写访问若发现引用计数大于 1,才会分离数据。
下面的代码能直观看到语义:
QString a = QStringLiteral("Qt");
QString b = a; // 通常只增加共享数据块的引用计数
b.append(u" 6"); // b 需要写入,先 detach,再追加
qDebug() << a; // "Qt"
qDebug() << b; // "Qt 6"
它带来的收益是:函数按值接收、返回 QString / QList / QMap 时,在只读路径上通常很轻量。但“按值传递便宜”并不意味着“按值传递永远更快”;如果函数内部一定会修改参数,而调用方又持有共享副本,则第一次修改可能产生深拷贝。
它的成本是:大对象被复制后,第一次非 const 修改可能执行 O(n) 的数据复制。因此,以下两种风格的性能特征完全不同:
// 适合:函数需要保存自己的值,或返回后继续以值对象使用。
QString normalized(QString text)
{
return text.trimmed().toLower();
}
// 适合:只读、且不需要拥有文本时,避免调用者误以为会修改它。
bool isValidName(const QString &text)
{
return !text.trimmed().isEmpty();
}
入门阶段的实用规则是:只读参数默认 const T & 或视图类型;确实需要副本并修改时,按值接收是清晰且常常高效的;不要为了“省复制”把所有对象都做成裸指针。
七、从 Qt 6.8.3 源码看实现
源码不是要求初学者逐行读完,而是用来验证几个会影响设计和性能的事实。
7.1 共享数组数据块:引用计数、容量与分离条件
文件 src/corelib/tools/qarraydata.h 定义了 QArrayData。其中最关键的成员是:
struct QArrayData
{
QBasicAtomicInt ref_;
ArrayOptions flags;
qsizetype alloc;
bool isShared() const noexcept
{ return ref_.loadRelaxed() != 1; }
bool needsDetach() noexcept
{ return ref_.loadRelaxed() > 1; }
};
对应源码位置:src/qtbase/src/corelib/tools/qarraydata.h 第 42、69-80 行。
可以把它理解成:ref_ 管理有多少个 Qt 值对象共享这一数据块,alloc 记录已分配容量;当 ref_ > 1 时,写入者不能直接修改共享数据,所以 needsDetach() 返回真。
QArrayDataPointer::detach() 的实现更直接:
void detach(QArrayDataPointer *old = nullptr)
{
if (needsDetach())
reallocateAndGrow(QArrayData::GrowsAtEnd, 0, old);
}
源码位置:src/corelib/tools/qarraydatapointer.h 第 142-146 行。名字里的 reallocateAndGrow 不代表每次都增长;这里传入 0,表示仅为了分离出独占的存储。
7.2 QString:char16_t 数组,而不是 UTF-8 字节数组
QString 在头文件中定义数据类型为:
class Q_CORE_EXPORT QString
{
typedef QTypedArrayData<char16_t> Data;
// …
};
源码位置:src/corelib/text/qstring.h 第 128-131 行。因此它的底层字符单元是 16 位 char16_t,公开 API 则用 QChar 表达。这就是前文“size() 数的是 UTF-16 代码单元”的实现依据。
再看几个可写入口的实现:
QChar *QString::data()
{
detach();
return reinterpret_cast<QChar *>(d.data());
}
void QString::detach()
{ if (d.needsDetach()) reallocData(d.size, QArrayData::KeepSize); }
QChar &QString::operator[](qsizetype i)
{ return data()[i]; }
源码位置:src/corelib/text/qstring.h 第 1247-1256、1352-1353 行。只要走到可写 data() 或可写 operator[],就会先 detach()。这也是为什么应把只读参数声明为 const QString &,只读遍历使用 cbegin() / cend() 或 const 对象。
7.3 QList:连续存储、容量控制、Qt 6 的 QVector 别名
QList<T> 在 src/corelib/tools/qlist.h 中持有:
using Data = QTypedArrayData<T>;
using DataPointer = QArrayDataPointer<T>;
DataPointer d;
这说明它复用了前面的共享数组数据块。迭代器还声明为 std::contiguous_iterator_tag(在可用的标准库环境下),证明 Qt 6 的 QList 是连续元素序列,而不是旧资料中常提到的指针间接层布局。
QList::resize_internal() 的关键分支如下:
if (d->needsDetach() || newSize > capacity() – d.freeSpaceAtBegin()) {
d.detachAndGrow(QArrayData::GrowsAtEnd, newSize – d.size, nullptr, nullptr);
}
源码位置:src/corelib/tools/qlist.h 第 746-755 行。它把两类代价分得很清楚:
而 src/corelib/tools/qcontainerfwd.h 第 39 行的 template<typename T> using QVector = QList<T>; 则是 Qt 6 中不要再单独比较二者性能的最终证据。
7.4 QMap:基于 std::map 实现的 Qt 有序关联容器
Qt 6.8.3 的 QMap 源码不是自己重新实现红黑树,而是明确使用标准库映射:
template <class Key, class T>
class QMap
{
using Map = std::map<Key, T>;
using MapData = QMapData<Map>;
QtPrivate::QExplicitlySharedDataPointerV2<MapData> d;
};
源码位置:src/corelib/tools/qmap.h 第 186-192 行。MapData 继承自 QSharedData,内部保存 std::map。所以 QMap 的有序查找复杂度与 std::map 同类,同时在它外层加上 Qt 的隐式共享值语义。
看非 const 的下标运算符:
T &operator[](const Key &key)
{
const auto copy = d.isShared() ? *this : QMap();
detach();
auto i = d->m.find(key);
if (i == d->m.end())
i = d->m.insert({key, T()}).first;
return i->second;
}
源码位置:src/corelib/tools/qmap.h 第 369-376 行。这里同时解释了两件事:非 const operator[] 会分离共享数据;键不存在时会插入默认值。对于大 QMap,复制后再修改一个键也可能触发整棵映射的复制,因此不要把它误当作“只复制一个节点”的数据结构。这是隐式共享带来的典型 trade-off:复制成本低,但共享状态下第一次写入的成本可能很高。
八、性能实践:如何避免无效优化和真实的性能坑
8.1 先 reserve(),再批量构建
下面是最值得养成的习惯之一:
QList<QString> names;
names.reserve(records.size());
for (const Record &record : records) {
names.append(record.name);
}
这并不改变最终元素数量,却显著减少容量不断翻倍或重新分配时的元素迁移。相同原则适用于 QString 的大量拼接:如果最终长度可预测,先 reserve()。
QString report;
report.reserve(lines.size() * 32);
for (const QString &line : lines) {
report += line;
report += u'\\n';
}
8.2 不要在热循环中反复触发“复制后写入”
// 容易隐藏分离成本:每次传值后都可能修改副本。
void appendSuffix(QString text)
{
text.append(u".log");
}
这段代码本身不一定有问题。短字符串、低频调用时根本不用优化;但若 text 很大、函数在热路径上调用上百万次,第一次写入会使每次调用都承担分离和复制的可能。此时应重新审视业务:是否应直接构造结果、是否能传 QStringView 只读、是否应在调用方积累并一次性处理。
性能优化的正确顺序是:先测量,再确认热点,再利用复杂度与内存模型修改。 不要因为看到“隐式共享”就假设所有复制都零成本,也不要因为看到“可能 detach”就提前把每个对象改为指针。
8.3 Qt 容器的迭代器失效与隐式共享
顺序容器扩容、插入或删除可能使原有指针、引用、迭代器失效,这与 std::vector 的常见规则相似。隐式共享还多了一层:对非 const 容器调用可能触发分离的 API 时,原来指向共享数据的迭代器也不应继续假定有效。
实用做法:
- 容器结构发生变化后,不继续使用旧迭代器;
- 只读遍历优先 cbegin() / cend(),或范围 for (const T &item : container);
- 不在遍历同一容器时随意 append()、insert()、removeAt();
- 需要删除元素时,按 API 文档使用返回的新迭代器,或采用更适合的收集/过滤策略。
8.4 QMap 不是“更快的字典”
很多初学者看到“键值对”就用 QMap。应先问一个更具体的问题:我需要排序吗?
| 固定数量、字段含义明确的数据 | struct / std::array | 数据结构比通用容器更明确 |
| 数量动态、需要顺序访问 | QList | 动态数组 |
| 需要按 key 查找且不关心顺序 | QHash | 哈希查找 |
| 需要按 key 有序遍历 | QMap | 有序关联 |
这比笼统地问“QMap 和 QHash 谁更快”更接近真实工程决策。
九、选型速查表
当你写新代码时,可以按下面的顺序判断:
| 需要显示、翻译、搜索或传给 Qt UI 的文本 | QString | Qt 的 Unicode 文本主类型 | 任意二进制字节缓冲区 |
| 外部 UTF-8 数据 | QByteArray / std::string,边界处 fromUtf8() | 编码与文本语义明确 | 未转换就当 QString |
| 可按下标读写的一串元素 | QList<T> | Qt 6 连续顺序容器 | 高频中间插删队列 |
| 已有的 Qt 6 QVector<T> 接口 | QVector<T> 或统一迁移到 QList<T> | 两者同一实现 | 一个独立性能优化选项 |
| 有序键值和范围查询 | QMap<K,V> | 按键排序、lowerBound() | 无序高速哈希表 |
| 纯 C++ 核心库 | 标准库容器 | 降低 Qt 依赖,融入标准生态 | 为一致性强行改 Qt 容器 |
最后再强调一次:容器选择首先是数据关系与接口边界的问题,其次才是微基准测试的问题。若没有确定的数据规模和操作比例,一句“某容器更快”通常没有工程价值。
十、总结
本篇从用法走到源码,可以收束为以下结论:
- QString 是 Qt 的 Unicode 文本类型,内部以 UTF-16 代码单元存储;它适合 Qt UI、路径和业务文本,但不应代替二进制字节缓冲区。
- Qt 6 的 QList<T> 是连续的顺序容器,与 std::vector<T> 在基本复杂度上相近;预知规模时先 reserve()。
- Qt 6 中 QVector<T> 是 QList<T> 的别名,选型不再需要基于二者性能或内存布局。
- QMap<K,V> 是带隐式共享值语义的有序 std::map 包装;它适合排序和范围查询,非 const operator[] 会在缺键时插入值。
- QString、QList / QVector 和 QMap 都利用隐式共享:复制常常轻量,第一次写入可能分离并复制数据。只读用 const,批量构建先 reserve(),才是更稳妥的性能习惯。
下一篇将继续进入 Qt Core 的对象模型,理解 QObject、父子对象树和内存管理。届时会看到:容器管理的是值数据,而 QObject 的生命周期管理解决的是另一类问题,二者不能混为一谈。
下一篇预告:《Qt 对象模型与内存管理——QObject、对象树、父子关系》





