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

张开发
2026/4/15 13:58:54 15 分钟阅读

分享文章

现代Qt开发教程(新手篇)1.4——容器
现代Qt开发教程新手篇1.4——容器相关仓库仍然已经开源正在积极火热的建设之中欢迎各位大佬提Issue和PR链接地址https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeQt1. 前言说实话我刚从 STL 转过来的时候也有个疑问为什么不用 std::vector 和 std::mapC 标准库不是挺成熟的吗后来实际项目跑起来才明白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 一样是连续存储。#includeQListQListintnumbers{1,2,3,4,5};numbers.append(6);// 尾部追加均摊 O(1)numbers.prepend(0);// 头部插入O(n) 需要移动所有元素numbers.insert(2,99);// 位置 2 插入同样 O(n)intvaluenumbers.at(3);// 安全访问越界会报错intfastnumbers[3];// 不检查越界性能更好你会发现我特别强调 “连续内存” 这个特性。这意味着什么意味着你可以用指针直接遍历意味着 CPU 缓存命中率高意味着和 C 数组互操作很方便。int*rawPtrnumbers.data();// 获取底层 C 数组指针constint*constRawPtrstd::as_const(numbers).data(); 口述回答用自己的话说说QList 和 std::vector 有什么本质区别什么场景下你会优先选择 QList3.2 QMap vs QHash有序还是快速这个问题我面试时经常问。QMap 是红黑树实现键值对按 key 排序存储查找是 O(log n)。QHash 是哈希表实现查找均摊 O(1)但迭代顺序是乱的。#includeQMap#includeQHashQMapQString,intscores;scores[Alice]95;scores[Bob]87;// 迭代时按 key 排序Alice, Bobfor(autoitscores.cbegin();it!scores.cend();it){qDebug()it.key():it.value();}QHashQString,inthashScores;hashScores[Alice]95;hashScores[Bob]87;// 迭代顺序不确定这里有个很实际的抉择如果你需要有序遍历或者需要范围查询比如查找所有以 “A” 开头的 key用 QMap。如果纯粹是 key-value 查找QHash 是性能之王。// QHash 查找示例if(hashScores.contains(Alice)){intscorehashScores.value(Alice);// 找不到返回默认构造值 0intscore2hashScores[Alice];// 找不到会自动插入默认值}注意上面那个坑operator[]找不到 key 时会自动插入一个默认构造的值这可能是你想要的也可能不是。用value()方法更安全找不到直接返回默认值但不插入。 代码填空下面的代码想统计一个字符串中每个字符出现的次数请补充空白处。QString texthello world;QHashQChar,intcharCount;for(QChar c:text){if(charCount.contains(c)){charCount[c]______;// 提示计数加一}else{charCount[c]______;// 提示首次出现设为1}}3.3 QSet去重利器QSet 本质上是个 QHashKey, void只存 key 不存 value用来做去重和集合运算。#includeQSetQListintnumbers{1,2,2,3,3,3,4};QSetintuniqueNumbers(numbers.begin(),numbers.end());// 从 QList 构造自动去重// uniqueNumbers 现在是 {1, 2, 3, 4}// 集合运算QSetintset1{1,2,3};QSetintset2{2,3,4};QSetintintersectionset1.intersect(set2);// 交集 {2, 3}QSetintunion_set1.unite(set2);// 并集 {1, 2, 3, 4}3.4 隐式共享写时复制的魔法这是 Qt 容器的杀手级特性我专门放在最后说因为它确实有点反直觉。当你复制一个 Qt 容器时表面上看是复制了整个容器实际上内部只是复制了一个指针和一个引用计数。真正的数据复制只有在一个容器被修改时才会发生这叫做 “detach”分离。QListintlist1{1,2,3};QListintlist2list1;// 浅拷贝共享同一块数据引用计数变为 2// 此时 list1 和 list2 的数据指针指向同一个内存块list2[0]99;// 写操作触发 detachlist2 独立复制数据// 现在 list2 {99, 2, 3}list1 仍然是 {1, 2, 3}这个机制使得按值传递容器非常高效。比如一个函数返回 QListQListintgetNumbers(){QListintresult;result123;returnresult;// C17 之后即使没有 RVO 也因为隐式共享很高效}但这里有个很重要的陷阱迭代器和隐式共享的冲突问题我们下一节专门讲。4. 踩坑预防清单⚠️ 坑 #1迭代器失效陷阱❌ 错误做法QListinta,b;a.resize(100000);autoita.begin();ba;// it 现在指向共享数据a[0]5;// a detachit 变成指向 b 的迭代器b.clear();// it 彻底失效intvalue*it;// 未定义行为 ✅ 正确做法 cpp QListint a, b; a.resize(100000); b a; // 先复制 auto it a.begin(); // 再获取迭代器 // 或者全程用 STL 风格迭代器避免在迭代期间复制容器 后果隐式共享导致迭代器指向错误的容器clear() 后解引用会崩溃。 一句话记住迭代器活跃期间不要复制容器容器复制后不要用旧的迭代器。⚠️ 坑 #2QHash 的 operator[] 会自动插入❌ 错误做法QHashQString,intscores;if(scores[Charlie]80){// Charlie 不存在但被插入了qDebug()Good score;} ✅ 正确做法 cpp if (scores.value(Charlie, 0) 80) { // 找不到返回 0不插入 qDebug() Good score; } // 或者用 contains() if (scores.contains(Charlie) scores[Charlie] 80) { qDebug() Good score; } 后果哈希表中多了一堆无用数据查找性能下降内存泄漏。 一句话记住查询用value()确定要插入时才用operator[]。⚠️ 坑 #3for 循环中的隐式 detach❌ 错误做法QListQStringlist{a,b,c};for(constautoitem:list){// 等等真的是 const 吗qDebug()item;} 这段代码在某种情况下会触发 detach因为 range-based for 默认不是 const 的。 ✅ 正确做法 cpp QListQString list {a, b, c}; for (const auto item : std::as_const(list)) { qDebug() item; } 后果不必要的内存复制大容器时性能明显下降。 一句话记住只读遍历用std::as_const()养成肌肉记忆。 调试挑战下面的代码有什么问题QListQWidget*widgets;for(inti0;i10;i){widgets.append(newQWidget());}for(autow:widgets){deletew;}widgets.clear();// 这行有必要吗5. 随堂测验我们穿插了几个小测验现在检查一下你的理解 口述回答前面用自己的话说说QList 和 std::vector 有什么本质区别什么场景下你会优先选择 QList 代码填空前面统计字符出现次数的代码。 调试挑战前面QWidget* 容器的内存管理问题。6. 练习项目 练习项目学生成绩管理系统 功能描述实现一个简单的学生成绩管理程序使用 QMap 存储学生姓名和成绩使用 QList 存储班级所有学生。程序需要支持添加学生、删除学生、按成绩排序、统计平均分等功能。✅ 完成标准程序能够正确添加、删除、查询学生成绩使用 QHash 或 QMap 实现姓名到成绩的映射用 QList 维护学生名单。排序功能可以手动实现排序算法也可以用 std::sort。程序启动时预置一些测试数据退出前打印所有学生信息。 提示用QMapQString, int存储姓名到成绩的映射可以自动按姓名排序如果需要按成绩排序考虑用QListQPairQString, int配合std::sort别忘了在添加学生时检查是否已存在同名学生统计功能可以用范围-based for 配合std::as_const()7. 官方文档参考链接 Qt 文档 · Container Classes · 容器类总览包含所有容器类的性能对比和用法示例 Qt 文档 · Implicit Sharing · 隐式共享机制的详细解释包括隐式共享类的完整列表相关阅读现代Qt开发教程新手篇1.1——QObject 与元对象系统 - 相似度 100%现代Qt开发教程新手篇1.2——信号与槽 - 相似度 100%通用GUI编程技术——图形渲染实战二十八——图像格式与编解码PNG/JPEG全掌握 - 相似度 100%

更多文章