现代Qt开发教程(新手篇)1.4——容器

现代Qt开发教程(新手篇)1.4------容器

相关仓库仍然已经开源,正在积极火热的建设之中,欢迎各位大佬提Issue和PR!
链接地址:github.com/Awesome-Emb...

1. 前言

说实话,我刚从 STL 转过来的时候也有个疑问:为什么不用 std::vector 和 std::map?C++ 标准库不是挺成熟的吗?

后来实际项目跑起来才明白,Qt 容器确实有一套自己的哲学。首先它们是隐式共享的,传值不拷贝,这在信号槽这种到处都是参数传递的场景下简直是神器。其次它们和 Qt 其他类型配合得天衣无缝,QString、QDateTime 这些往容器里一塞,什么都不用管。还有 Qt6 之后 QList 统一成了连续数组模型,性能表现和 std::vector 一样优秀。

但我们也要承认,如果你的项目已经大量用了 STL,没有特别必要全部换成 Qt 容器。但至少你要了解它们,因为 Qt API 里到处都是 QList、QStringList、QMap 这种返回类型,你不可能躲得掉。

2. 环境说明

本文基于 Qt 6.10+,所有示例代码都经过 CMake 3.26+ 环境验证。如果你还在用 qmake 时代,建议尽快迁移,CMake 对 Qt6 的支持比 qmake 顺畅太多。

3. 核心概念

3.1 QList:万能的连续数组

先说清楚一个历史包袱:Qt5 时代的 QList 是个指针数组的混合体,中间层间接访问,性能不是最优。Qt6 把 QList 和 QVector 统一了,现在就是纯粹的连续内存数组,和 std::vector 一样是连续存储。

cpp 复制代码
#include <QList>

QList<int> numbers = {1, 2, 3, 4, 5};
numbers.append(6);              // 尾部追加,均摊 O(1)
numbers.prepend(0);             // 头部插入,O(n) 需要移动所有元素
numbers.insert(2, 99);          // 位置 2 插入,同样 O(n)
int value = numbers.at(3);      // 安全访问,越界会报错
int fast = numbers[3];          // 不检查越界,性能更好

你会发现我特别强调 "连续内存" 这个特性。这意味着什么?意味着你可以用指针直接遍历,意味着 CPU 缓存命中率高,意味着和 C 数组互操作很方便。

cpp 复制代码
int* rawPtr = numbers.data();                      // 获取底层 C 数组指针
const int* constRawPtr = std::as_const(numbers).data();

📝 口述回答:用自己的话说说,QList 和 std::vector 有什么本质区别?什么场景下你会优先选择 QList?

3.2 QMap vs QHash:有序还是快速

这个问题我面试时经常问。QMap 是红黑树实现,键值对按 key 排序存储,查找是 O(log n)。QHash 是哈希表实现,查找均摊 O(1),但迭代顺序是乱的。

cpp 复制代码
#include <QMap>
#include <QHash>

QMap<QString, int> scores;
scores["Alice"] = 95;
scores["Bob"] = 87;
// 迭代时按 key 排序:Alice, Bob
for (auto it = scores.cbegin(); it != scores.cend(); ++it) {
    qDebug() << it.key() << ":" << it.value();
}

QHash<QString, int> hashScores;
hashScores["Alice"] = 95;
hashScores["Bob"] = 87;
// 迭代顺序不确定

这里有个很实际的抉择:如果你需要有序遍历,或者需要范围查询(比如查找所有以 "A" 开头的 key),用 QMap。如果纯粹是 key-value 查找,QHash 是性能之王。

cpp 复制代码
// QHash 查找示例
if (hashScores.contains("Alice")) {
    int score = hashScores.value("Alice");      // 找不到返回默认构造值 0
    int score2 = hashScores["Alice"];           // 找不到会自动插入默认值!
}

注意上面那个坑:operator[] 找不到 key 时会自动插入一个默认构造的值,这可能是你想要的,也可能不是。用 value() 方法更安全,找不到直接返回默认值但不插入。

🔲 代码填空:下面的代码想统计一个字符串中每个字符出现的次数,请补充空白处。

cpp 复制代码
QString text = "hello world";
QHash<QChar, int> charCount;
for (QChar c : text) {
    if (charCount.contains(c)) {
        charCount[c] = ______;  // 提示:计数加一
    } else {
        charCount[c] = ______;  // 提示:首次出现设为1
    }
}

3.3 QSet:去重利器

QSet 本质上是个 QHash<Key, void>,只存 key 不存 value,用来做去重和集合运算。

cpp 复制代码
#include <QSet>

QList<int> numbers = {1, 2, 2, 3, 3, 3, 4};
QSet<int> uniqueNumbers(numbers.begin(), numbers.end());  // 从 QList 构造,自动去重
// uniqueNumbers 现在是 {1, 2, 3, 4}

// 集合运算
QSet<int> set1 = {1, 2, 3};
QSet<int> set2 = {2, 3, 4};
QSet<int> intersection = set1.intersect(set2);      // 交集 {2, 3}
QSet<int> union_ = set1.unite(set2);                // 并集 {1, 2, 3, 4}

3.4 隐式共享:写时复制的魔法

这是 Qt 容器的杀手级特性,我专门放在最后说因为它确实有点反直觉。

当你复制一个 Qt 容器时,表面上看是复制了整个容器,实际上内部只是复制了一个指针和一个引用计数。真正的数据复制只有在一个容器被修改时才会发生,这叫做 "detach"(分离)。

cpp 复制代码
QList<int> list1 = {1, 2, 3};
QList<int> list2 = list1;          // 浅拷贝,共享同一块数据,引用计数变为 2
// 此时 list1 和 list2 的数据指针指向同一个内存块

list2[0] = 99;                     // 写操作触发 detach,list2 独立复制数据
// 现在 list2 = {99, 2, 3},list1 仍然是 {1, 2, 3}

这个机制使得按值传递容器非常高效。比如一个函数返回 QList:

cpp 复制代码
QList<int> getNumbers() {
    QList<int> result;
    result << 1 << 2 << 3;
    return result;  // C++17 之后即使没有 RVO 也因为隐式共享很高效
}

但这里有个很重要的陷阱:迭代器和隐式共享的冲突问题,我们下一节专门讲。

4. 踩坑预防清单

⚠️ 坑 #1:迭代器失效陷阱

❌ 错误做法:

cpp 复制代码
QList<int> a, b;
a.resize(100000);
auto it = a.begin();
b = a;                          // it 现在指向共享数据
a[0] = 5;                       // a detach,it 变成指向 b 的迭代器
b.clear();                      // it 彻底失效
int value = *it;                // 未定义行为!
less 复制代码
>
> ✅ 正确做法:
> ```cpp
> QList<int> a, b;
> a.resize(100000);
> b = a;                          // 先复制
> auto it = a.begin();            // 再获取迭代器
> // 或者全程用 STL 风格迭代器,避免在迭代期间复制容器
> 

💥 后果:隐式共享导致迭代器指向错误的容器,clear() 后解引用会崩溃。

💡 一句话记住:迭代器活跃期间不要复制容器,容器复制后不要用旧的迭代器。
⚠️ 坑 #2:QHash 的 operator[] 会自动插入

❌ 错误做法:

cpp 复制代码
QHash<QString, int> scores;
if (scores["Charlie"] > 80) {   // "Charlie" 不存在但被插入了!
    qDebug() << "Good score";
}
shell 复制代码
>
> ✅ 正确做法:
> ```cpp
> if (scores.value("Charlie", 0) > 80) {  // 找不到返回 0,不插入
>     qDebug() << "Good score";
> }
> // 或者用 contains()
> if (scores.contains("Charlie") && scores["Charlie"] > 80) {
>     qDebug() << "Good score";
> }
> 

💥 后果:哈希表中多了一堆无用数据,查找性能下降,内存泄漏。

💡 一句话记住:查询用 value(),确定要插入时才用 operator[]
⚠️ 坑 #3:for 循环中的隐式 detach

❌ 错误做法:

cpp 复制代码
QList<QString> list = {"a", "b", "c"};
for (const auto &item : list) {      // 等等,真的是 const 吗?
    qDebug() << item;
}
shell 复制代码
> 这段代码在某种情况下会触发 detach,因为 range-based for 默认不是 const 的。
>
> ✅ 正确做法:
> ```cpp
> QList<QString> list = {"a", "b", "c"};
> for (const auto &item : std::as_const(list)) {
>     qDebug() << item;
> }
> 

💥 后果:不必要的内存复制,大容器时性能明显下降。

💡 一句话记住:只读遍历用 std::as_const(),养成肌肉记忆。

🐛 调试挑战:下面的代码有什么问题?

cpp 复制代码
QList<QWidget*> widgets;
for (int i = 0; i < 10; ++i) {
    widgets.append(new QWidget());
}

for (auto w : widgets) {
    delete w;
}
widgets.clear();  // 这行有必要吗?

5. 随堂测验

我们穿插了几个小测验,现在检查一下你的理解:

📝 口述回答(前面):用自己的话说说,QList 和 std::vector 有什么本质区别?什么场景下你会优先选择 QList?

🔲 代码填空(前面):统计字符出现次数的代码。

🐛 调试挑战(前面):QWidget* 容器的内存管理问题。

6. 练习项目

🎯 练习项目:学生成绩管理系统

📋 功能描述:实现一个简单的学生成绩管理程序,使用 QMap 存储学生姓名和成绩,使用 QList 存储班级所有学生。程序需要支持添加学生、删除学生、按成绩排序、统计平均分等功能。

✅ 完成标准:程序能够正确添加、删除、查询学生成绩,使用 QHash 或 QMap 实现姓名到成绩的映射,用 QList 维护学生名单。排序功能可以手动实现排序算法,也可以用 std::sort。程序启动时预置一些测试数据,退出前打印所有学生信息。

💡 提示:

  • QMap<QString, int> 存储姓名到成绩的映射可以自动按姓名排序
  • 如果需要按成绩排序,考虑用 QList<QPair<QString, int>> 配合 std::sort
  • 别忘了在添加学生时检查是否已存在同名学生
  • 统计功能可以用范围-based for 配合 std::as_const()

7. 官方文档参考链接

📎 Qt 文档 · Container Classes · 容器类总览,包含所有容器类的性能对比和用法示例 📎 Qt 文档 · Implicit Sharing · 隐式共享机制的详细解释,包括隐式共享类的完整列表


相关阅读

  1. 现代Qt开发教程(新手篇)1.1------QObject 与元对象系统 - 相似度 100%
  2. 现代Qt开发教程(新手篇)1.2------信号与槽 - 相似度 100%
相关推荐
ulias2122 小时前
Linux中的开发工具
linux·运维·服务器·开发语言·c++·windows
qq_466302452 小时前
u盘插入拔出,listView不显示盘符变化
c++·qt
小熊Coding2 小时前
Windows 上安装 mysqlclient 时遇到了编译错误,核心原因是缺少 Microsoft Visual C++ 14.0 或更高版本 的编译环境。
c++·windows·python·microsoft·django·mysqlclient·bug记录
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(六):<线程同步与互斥>线程同步(上)
java·linux·运维·服务器·c++·学习·线程
feng_you_ying_li3 小时前
C++11可变模板参数,包扩展,emplace系列和push系列的区别
前端·c++·算法
tankeven3 小时前
HJ177 可匹配子段计数
c++·算法
白藏y3 小时前
【C++】ifstream、ofstream、fstream的基础使用
c++
皮卡蛋炒饭.3 小时前
Linux进程信号
开发语言·c++
共享家95273 小时前
C++ 日志类设计
linux·c++·后端