目录
文章目录
1 背景
曾遇到过同事们提出的一些问题,感觉单纯的回答是或者不是其实是不负责任的,稍微系统的分享一下相关知识很有必要。
1 循环遍历map,过程中插入一个元素,迭代器是否会失效?
2 qset 比qhash慢吧?
3 qstack 比qlist高效吗?
2 顺序容器和关联容器
数组和列表
队列和栈
树和哈希
3 STL容器和QT容器
STL容器简介
STL(Standard Template Library,标准模板库)是惠普实验室开发的一系列软件的统称。
STL的代码从广义上讲分为三块:algorithm(算法)、container(容器)和iterator(迭代器),几乎所有的代码都采用了模板类和模版函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。
在实际的开发过程中,数据结构本身的重要性不会逊于操作于数据结构的算法的重要性,当程序中存在着对时间要求很高的部分时,数据结构的选择就显得尤为重要。
经典的数据结构数量有限,但是我们常常重复着一些为了实现向量、链表等结构而编写的代码,这些代码都十分相似,只是为了适应不同数据的变化而在细节上有所出入。STL容器就为我们提供了这样的方便,它允许我们重复利用已有的实现构造自己的特定类型下的数据结构,通过设置一些模版类,STL容器对最常用的数据结构提供了支持,这些模板的参数允许我们指定容器中元素的数据类型,可以将我们许多重复而乏味的工作简化。
- 向量(vector) 连续存储的元素
- 列表(list) 由节点组成的双向链表,每个结点包含着一个元素
- 双队列(deque) 连续存储的指向不同元素的指针所组成的数组
- 集合(set) 由节点组成的红黑树,每个节点都包含着一个元素,节点之间以某种作用于元素对的谓词排列,没有两个不同的元素能够拥有相同的次序
- 多重集合(multiset) 允许存在两个次序相等的元素的集合
- 栈(stack) 后进先出的值的排列
- 队列(queue) 先进先出的执的排列
- 优先队列(priority_queue) 元素的次序是由作用于所存储的值对上的某种谓词决定的的一种队列
- 映射(map) 由{键,值}对组成的集合,以某种作用于键对上的谓词排列
- 多重映射(multimap) 允许键对有相等的次序的映射
QT容器简介
在QT库中为我们提供了一系列的基于模板的容器类。这些类可以被用来存储特定类型的项。例如,如果你需要一个大小可变的QString数组,那么可以使用QVector。
这些容器类都是隐式共享的,可重入的,并且在速度上进行了优化,内存占用少,内联代码扩展少,从而可以产生更小的可执行文件。此外,当他们被用作只读容器时,还是线程安全的。对于遍历这些容器来说,可以使用两种类型的迭代器:Java风格的迭代器和STL风格的迭代器。其中,Java风格的迭代器更容易使用,特别是对于Java工作人员来说,它提供了高层次的函数;然而,STL风格的迭代器会更高效,并且可以和Qt和STL的通用算法结合使用。另外,Qt还提供了一个foreach关键字,使遍历容器中的每一项更容易了。Qt中的容器和STL中的类似,也分为序列式容器和关联式容器。
其中,序列式容器有:QList,QLinkedList,QVector,QStack,QQueue。对大部分应用程序来说,QList都是一个很好的选择。尽管它在底层被实现为一个array-list,但它为我们提供了非常快速的添加操作,包括在头部添加和在尾部添加。如果你确实需要一个linked-list,可以使用QLinkedList;如果你想确保你的元素占用连续的内存空间,可以使用QVector。而QStack和QQueue是两个提供了LIFO和FIFO语义的方便类。除了序列式容器,Qt中还提供了关联式容器:QMap,QMultiMap,QHash,QMultiHash,QSet。这些容器中存储的都是key-value对。其中,"Multi"容器又支持一个key可以关联多个value。"Hash"容器通过使用一个hash函数而不是二分搜索提供了更快速的查找操作。
STL和QT容器—复杂度比较
- QLinkedList —— std::list 两者都是双向链表,两者可以直接互转
- QVector —— std::vector 两者都是动态数组,都是根据sizeof(T)进行连续分配,保证成员内存连续,能够用
- data()直接取出指针作为c数组使用,两者可以直接互转
- QMap —— std::map 两者都是红黑树算法,但不能互转,因为数据成员实现方式不同。std::map的数据成员用的是std::pair,而QMap用的是自己封装的Node,当然还是键值对
- QMultiMap —— std::multimap 同上
- QList —— stl没有对应类。QList其实不是链表,是优化过的vector,官方的形容是array list,据说它更类似于boost::ptr_deque,不过我没用过后者。它的存储方式是分配连续的node,每个node的数据成员不大于一个指针大小,所以对于int、char等基础类型,它是直接存储,对于Class、Struct等类型,它是存储对象指针。
QList的实现模式,优点主要在于快速插入。因为其元素大小不会超过sizeof(void*),所以插入时元素移动比
vector要快得多。当然可以用QVecotr<void*>来模拟QList,就是用起来不太方便。另外,QList的增长策略是双向增长,所以对prepend支持比vector好得多,使用灵活性高于Vector和LinkedList。缺点是每次存入对象时,需要构造Node对象,带来额外的堆开销。然而Qt对QList的支持还是很充足,用户甚至可以用宏为自己要放入list的对象进行属性指定来辅助编译优化。对了,QList虽然是个特殊的Vector,但提供的接口仍然是和std::list的互转,挺奇葩的…… - QBitArray —— std::bitset 功能相同,实现相似,都是构造一个array,用位操作来存取数据。不同的是,
QBitArray数据的基础元素是unsigned char,而bitset是unsigned long。所以QBitArray可能在空间消耗上会
省一点。至于效率上么……得让懂编译的人来解答了,我是觉得,32位cpu上,char的位操作和int的位操作应该是
一样的开销。不过二者最大的差别是,std::bitset是定长,数据元素分配在栈上。QBitArray是变长,数据元素分配在堆上。
这个肯定有性能差别QHash —— std::hash_map 都是各自实现了自己的hashTable,然后查询上都是用node->next的方式逐一对比,不支持互转,性能上更多的应该是看hash算法。QHash为常用的qt数据类型都提供好了qHash()函数,用户自定类型也能通过自己实现qHash()来存入QHash容器。
QSet —— std::set 二者不能互转,实现方式有本质的区别。QSet其实是改造过的QHash,用key来存数据,
value置空。而std::set用的是红黑树。所以,效率上一般应该是QSet高,但数据大了可能会有撞车问题
QT迭代器使用
04容器使用关键点
元素资格
什么样的数据有资格存入容器?
这些容器中存储的值可以是任何能被赋值的数据类型,即该类型必须提供一个默认的构造
函数、一个拷贝构造函数、一个赋值运算符。这样的数据类型涵盖了大部分你可以存储的
类型,包括基本类型入int和double,指针类型,Qt的数据类型QString,QDate,QTime,
但不包括QObject或其子类(QWidget,QDialog,QTimer等等)。如果你尝试构建一个
QList类型的变量,编译器就会提示你QWidget类的拷贝构造函数和赋值操作
符是被禁用的。如果你想存储这些类的对象,可以存储它们的指针类型,例如
QList<QWidget*>。
QMap<Key, T>的Key类型必须提供operator<();
QHash<Key, T>的Key类型必须提供operator==() 和 a global hash function called qHash()
隐式共享
很多Qt中的C++类运用了隐式数据共享机制去最大化资源有效利用和最小化复制克隆操作。
隐式共享类当作为函数参数传递的时候,不仅安全而且效率很高,因为传递的时候只是传递的
数据的指针,数据本身只当自己被修改的时候才会去复制。简称写时复制。
QString str1 = “freedom”;
QString str2 = str1; // str2 对 str1 进行浅拷贝
str2[0] = ‘k’; // str2先对str1 进行深拷贝,再修改某个元的值,此时 str2 = "kreedom“
str1 = str2 // str1 对 str2 进行浅拷贝。str1之前指向的内存空间将被释放掉。
要注意:跨线程信号槽发送一个变量,需要同步处理,否则可能数据不同步。
容器的默认值
例如,QVector会自动的使用默认构造函数的值初始化它的元素,QMap::value()方法在指定
的key不存在的情况下会返回一个默认构造函数产生的值。对大部分数据类型来说,这只是意味
着默认构造函数创建了一个值,例如,QString的默认构造函数会创建出一个空字符串。但是,
对于int和double这类的基本类型,和指针类型,C++语言并不会指定任何初始化。在这种情况
下,Qt的容器会自动的用0对它们进行初始化。
注意:
Qmap:推荐用contains() and value() 而不是operator 。因为operator 默默的插入一个
元素,如果对应的key不存在。
QList ,QVector :at() 比 operator快, 因为不进行深拷贝。
Qmap 和 Qhash
红黑树和哈希表的本质区别。
两种容器之间的接口区别:
- QHash占用的存储空间明显多于QMap
- QHash查找速度上显著于QMap
- QHash以任意的方式进行存储,而QMap则是以key顺序进行存储.
- Qhash 的键类型必须提供operator==()和yige 全局的qHash(key)函数。而QMap的键类型
key必须提供operator<()函数.
迭代器的*运算符
对于QMap和QHash来说,*运算符返回一个元素的value部分。如果你想获得key,可以在
迭代器上调用key() 方法。而处于对称性,迭代器还提供了value() 方法来获得value()值。
通过key获取Value时
A、当key存在,返回对应的Value
B、当key不存在,返回值类型所对应的“零”值
生长策略
- QString会一次分配4个字符,直到其大小达到20。
从20到4084,按每次扩大一倍的方式增长。更准确的说,它增长到下一个2的n次方,在减去12。
20=25-12,52=26-12,等等。(减去12是因为有些内存分配器需要 使用一些字节为每个
内存块做簿记。)
从4048开始,每次增加2048个字符(4096个字节)。这是有意义的,因为现代的操作系统在重
新分配一个buffer时并不会拷贝所有的数据;只是物理内存页的简单排序,只有位于第一页和最后
一页的数据需要拷贝。 - 对大部分应用程序来说,Qt提供的默认生长算法就足够了。如果你需要更多的控制,QVector,
QHash,QSet,QString和QByteArray提供了三个函数,允许你去检查和指定你需要多少内存去存储元素:
capacity():基于已分配的内存,返回元素的个数(对QHash和QSet来说,就是哈希表中桶的数量)
reserve(size):显式的预分配size个元素的内存
squeeze():释放为存储元素的多余内存。
如果你知道大约将在容器中存储多少个元素,你可以在开始插入元素前调用reserve(),
预分配好需要的空间;然后,在完全插入元素后,可以调用squeeze()来释放多余的内存。
总结
1) 如果需要随机访问,用vector
2) 如果存储元素的数目已知,用vector
3) 需要任意位置随机插入删除,用list
4) 只有需要更多在容器的首部尾部插入删除元素,用deque
5) 元素是复杂结构用list,也可以用vector存储指针(需要额外的精力去维护内存),看需求
6) 如果操作是基于键值,用set map
7) 如果需要经常的搜索,用map set, map set 的区别是map中的元素都是pair
QA
1 迭代器和算法在哪里?
算法+迭代器+容器 确实是模板库的有机体,不过内容太大,讲不了。
2 到底是STL还是QTL?
看个人,看习惯。本人之前喜欢用STL,现在喜欢用QTL。原因是Qt帮助文档实在是太方便了,接口也更人性化。
3 哪里有权威的文档?
assistant(Qt助手)。写ppt的时候网上找了很多资料,但是最后发现,最方便的还是看Qt助手。