06_Qt 常用数据类型与容器:QString、QList、QVector、QMap 的性能与实现分析

Qt 常用数据类型与容器:QString、QList、QVector、QMap 的性能与实现分析

引言

第五篇中,我们把 Qt Core 理解为 Qt 的基础能力层。本篇进入其中最常见、也最容易被"凭印象选错"的一组类:QStringQListQVectorQMap

它们看起来像是标准库 std::stringstd::vectorstd::map 的另一套写法,但 Qt 6 容器有自己的设计重点:Unicode 文本、与 Qt API 的天然协作,以及隐式共享(copy-on-write,写时复制)。这带来了轻量值传递的便利,也带来了第一次写入可能发生整块复制的成本。

本篇按下面的顺序展开:先会用,再与标准库对照,最后通过 Qt 6.8.3 源码理解内存和性能。读完后,至少应能回答这几个问题:

  • 中文、文件名、界面文本为什么优先用 QString
  • Qt 6 中还要不要纠结 QListQVector 的性能差异?
  • QMap 为什么能按键排序,它什么时候不应该代替 QHash
  • 一个看似简单的赋值,为什么有时几乎不分配内存,有时第一次修改会变慢?

本文讨论的源码版本为 Qt 6.8.3 。 Qt 6 背景下,文中关于 QList / QVector 的结论不适用于 Qt 5。

一、先建立容器地图:先看数据关系,再选类型

QString 不是"字符数组",QMap 也不是"任何键值对都适用的容器"。先从数据关系入手,会比背类名可靠得多。

图 1:四个主角都来自 Qt Core。注意:Qt 6 中 QVector<T>QList<T> 的类型别名。

需求 Qt 类型 对应标准库 关键特点
保存、拼接、搜索 Unicode 文本 QString std::stringstd::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> 有序键值对、隐式共享

本篇先只比较这四类。若需求是"海量键值查找但不关心排序",请优先想到 QHashstd::unordered_map;它们会在关联容器章节单独展开。

最小 CMake 依赖

这四个类都属于 Qt Core:

cmake 复制代码
find_package(Qt6 REQUIRED COMPONENTS Core)

target_link_libraries(MyApp PRIVATE Qt6::Core)

使用时按需包含头文件,而不是依赖某个"大而全"的头文件:

cpp 复制代码
#include <QString>
#include <QList>
#include <QMap>

QVector 在 Qt 6 中由容器前置声明头间接定义为 QList 的别名;在实际代码中仍可显式 #include <QVector>,这样意图最清楚。

二、QString:先把"文本"与"字节"分开

2.1 最常用的写法

界面显示文字、用户输入、路径名、JSON 文本字段等,优先使用 QString。下面的例子包含创建、拼接、格式化、搜索和 UTF-8 边界转换:

cpp 复制代码
#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

cpp 复制代码
const QString title = QStringLiteral("设备状态");

它可以让字符串字面量直接以 Qt 所需的字符形式构造 QString,避免先经过普通窄字符串再转换的路径。对于源码中的固定文本,这种写法也能明确表达其 Qt 字符串语义。

但不要为了形式统一而把所有内容都写成 QStringLiteral。来自网络、文件、C API 的 UTF-8 数据,应明确写出编码转换:

cpp 复制代码
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 之上。

维度 QString std::string
主要定位 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::stringQByteArray 往往更直接。提升不在于"类更多",而在于在 Qt 程序中少写不必要的编码转换,并获得一致的 Unicode 文本语义。

2.4 一个 Unicode 容易踩的坑:size() 不是"用户看到的字符数"

QString::size()length() 返回的是 UTF-16 代码单元 数量。大多数常见中文字符通常占一个 QChar,但某些 Emoji 和扩展字符需要代理对,会占两个 QChar;带组合附加符的字符也可能由多个代码点组成。

cpp 复制代码
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

cpp 复制代码
#include <QStringView>

bool isConfigFile(QStringView fileName)
{
    return fileName.endsWith(u".ini", Qt::CaseInsensitive);
}

QString name = QStringLiteral("settings.ini");
bool matched = isConfigFile(name);

QStringView 不拥有数据,不能保存到原始字符串生命周期之外。它是"只读借用",不是 QString 的更快替代品。

cpp 复制代码
QStringView view;
{
    QString text = QStringLiteral("Hello");
    view = text;
}

// view 已经悬空,不能再使用

QStringView 本身不拥有字符串数据,因此它的生命周期不能超过被引用的 QString

三、QList:Qt 6 的主力顺序容器

3.1 从增删改查开始

QList<T> 保存有顺序的一组 T。它支持下标随机访问、范围 for、尾部追加和中间插入。

cpp 复制代码
#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 &,避免元素逐个复制。
cpp 复制代码
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> 很接近:

操作 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 的协作,并支持轻量的值传递。

例如下面两个函数都合理,关键是看模块边界:

cpp 复制代码
// 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 QListat()operator[]

两者都用于按下标取元素,但使用方式不同:

cpp 复制代码
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 时最重要的一条结论:

cpp 复制代码
// Qt 6.8.3,src/corelib/tools/qcontainerfwd.h 第 39 行
template<typename T> using QVector = QList<T>;

也就是说,在 Qt 6 中:

cpp 复制代码
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 保存状态:

cpp 复制代码
#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[]

cpp 复制代码
QMap<QString, int> counters;

int readOnly = counters.value(QStringLiteral("ok"), 0); // 不插入

counters[QStringLiteral("ok")] += 1; // 不存在时插入值初始化的 int,再写入

constoperator[] 必须返回 T &,所以键不存在时会插入一个默认构造的值。它适合"取出并准备修改",不适合"查一下有没有"。这不仅影响业务数据,也可能触发隐式共享分离。

5.3 与 std::map 的对比

QMapstd::map 都是有序关联容器,典型查找、插入、删除复杂度为 O(log n)。二者的主要差异如下:

维度 QMap<Key, T> std::map<Key, T>
键顺序 operator< / 比较器有序 按比较器有序
查找、插入、删除 典型 O(log n) 典型 O(log n)
底层实现(Qt 6.8.3) 包装 std::map<Key, T> 标准库平衡树实现
复制 隐式共享 复制各节点
与 Qt API 协作 keys()values()、Qt 元类型与序列化场景方便 标准算法和泛型生态自然
内存特点 一份共享控制块 + 树节点;写时可能整体复制 每个元素通常一个树节点

QMap 的"有序"不是附加装饰,而是选型理由。按时间区间、按 ID 排序显示、使用 lowerBound() / upperBound() 查询一个键区间时,它非常合适:

cpp 复制代码
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() 混为一谈:

cpp 复制代码
QList<int> values;
values.reserve(1'000);
values.append(1);

qDebug() << values.size();     // 1
qDebug() << values.capacity(); // 至少可容纳 1,000 个元素

预留容量是在用可能的空闲内存换更少的重新分配。长生命周期容器如果后续不再增长、且内存紧张,可酌情 squeeze();不要在普通循环中频繁调用它,因为压缩自身也可能重新分配。

这里可能会有人问reserve在进行预留容量,那么resize是在做什么?

cpp 复制代码
QList<int> values;

values.reserve(100);
// size() == 0
// capacity() >= 100

values.resize(100);
// size() == 100

reserve() 管的是"预留空间",resize() 管的是"元素数量"。

6.3 隐式共享如何改变"复制"的成本

本文讨论的几个 Qt 类型都采用隐式共享机制。复制对象时,两个值先引用同一份只读数据;任一方要写入时,Qt 为写入方复制出独立数据。这既保留值语义,也避免只读传递时的深拷贝。

图 2:复制不等于立刻复制全部元素。第一次可写访问若发现引用计数大于 1,才会分离数据。

下面的代码能直观看到语义:

cpp 复制代码
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) 的数据复制。因此,以下两种风格的性能特征完全不同:

cpp 复制代码
// 适合:函数需要保存自己的值,或返回后继续以值对象使用。
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。其中最关键的成员是:

cpp 复制代码
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() 的实现更直接:

cpp 复制代码
void detach(QArrayDataPointer *old = nullptr)
{
    if (needsDetach())
        reallocateAndGrow(QArrayData::GrowsAtEnd, 0, old);
}

源码位置:src/corelib/tools/qarraydatapointer.h 第 142-146 行。名字里的 reallocateAndGrow 不代表每次都增长;这里传入 0,表示仅为了分离出独占的存储。

7.2 QStringchar16_t 数组,而不是 UTF-8 字节数组

QString 在头文件中定义数据类型为:

cpp 复制代码
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 代码单元"的实现依据。

再看几个可写入口的实现:

cpp 复制代码
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 中持有:

cpp 复制代码
using Data = QTypedArrayData<T>;
using DataPointer = QArrayDataPointer<T>;
DataPointer d;

这说明它复用了前面的共享数组数据块。迭代器还声明为 std::contiguous_iterator_tag(在可用的标准库环境下),证明 Qt 6 的 QList 是连续元素序列,而不是旧资料中常提到的指针间接层布局。

QList::resize_internal() 的关键分支如下:

cpp 复制代码
if (d->needsDetach() || newSize > capacity() - d.freeSpaceAtBegin()) {
    d.detachAndGrow(QArrayData::GrowsAtEnd, newSize - d.size, nullptr, nullptr);
}

源码位置:src/corelib/tools/qlist.h 第 746-755 行。它把两类代价分得很清楚:

  1. 数据仍被共享,写入前必须分离;
  2. 现有容量不足,必须扩容。

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 源码不是自己重新实现红黑树,而是明确使用标准库映射:

cpp 复制代码
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 的下标运算符:

cpp 复制代码
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(),再批量构建

下面是最值得养成的习惯之一:

cpp 复制代码
QList<QString> names;
names.reserve(records.size());

for (const Record &record : records) {
    names.append(record.name);
}

这并不改变最终元素数量,却显著减少容量不断翻倍或重新分配时的元素迁移。相同原则适用于 QString 的大量拼接:如果最终长度可预测,先 reserve()

cpp 复制代码
QString report;
report.reserve(lines.size() * 32);
for (const QString &line : lines) {
    report += line;
    report += u'\n';
}

8.2 不要在热循环中反复触发"复制后写入"

cpp 复制代码
// 容易隐藏分离成本:每次传值后都可能修改副本。
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 有序关联

这比笼统地问"QMapQHash 谁更快"更接近真实工程决策。

九、选型速查表

当你写新代码时,可以按下面的顺序判断:

你手上的数据 首选 原因 不要误用为
需要显示、翻译、搜索或传给 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[] 会在缺键时插入值。
  • QStringQList / QVectorQMap 都利用隐式共享:复制常常轻量,第一次写入可能分离并复制数据。只读用 const,批量构建先 reserve(),才是更稳妥的性能习惯。

下一篇将继续进入 Qt Core 的对象模型,理解 QObject、父子对象树和内存管理。届时会看到:容器管理的是值数据,而 QObject 的生命周期管理解决的是另一类问题,二者不能混为一谈。


下一篇预告:《Qt 对象模型与内存管理------QObject、对象树、父子关系》

相关推荐
林森lsjs1 小时前
队列 FIFO 原理 & 手撕两种队列实现(链表 + 循环数组)—数据结构陆
java·开发语言·数据结构·队列
H_oRIZoN_2 小时前
Linux入门DAY23(C语言树形结构)
linux·c语言·数据结构
间歇性努力持续性发呆的野生快乐选手2 小时前
stl容器使用和算法简介
数据结构·c++
重生之后端学习6 小时前
15. 三数之和[中等]✅
java·数据结构·算法·leetcode·职场和发展
疯狂打码的少年6 小时前
【数据结构】交换类排序:冒泡与快速排序
数据结构·笔记·算法·排序算法
zlinear数据采集卡7 小时前
数据采集卡从入门到精通(38):上位机开发实战——Python/QT/LabVIEW的技术选型与分层架构
python·单片机·嵌入式硬件·qt·fpga开发·开源·labview
Nil2087 小时前
leetcode 108有序数组转换为二叉搜索树
数据结构·算法·leetcode
hn小菜鸡7 小时前
LeetCode 763、划分字母区间
数据结构·算法·leetcode
疯狂打码的少年8 小时前
【数据结构】哈希表:构造与冲突处理
数据结构·笔记·哈希算法·散列表