Qt扫盲-Qt容器之容器总述

总述

Qt提供了通用的基于模版实现方式的容器类:能做到通用性很大程度上就是依靠 模板 特性。容器本身就是为了存储数据的,但是这里的数据主要是在程序运行时,存储在内存中的数据,运行结束后就释放内存了。

Qt的内置容器特点轻便线程安全、相对于STL更容易使用
Qt的容器是隐式共享的,它们是可重入的,并且它们针对速度、低内存消耗和最小的内联代码扩展进行了优化,从而产生了更小的可执行文件。此外,在它们被用于访问它们的所有线程用作只读容器的情况下,它们是线程安全的。

Qt支持两种访问容器数据的方式,提供了 Java-style 和 STL-style的迭代器,Java的那种方式更容易使用并高度功能化,同时STL的方式也很高效,并且支持STL的算法和容器结合的功能,异常强大。

Qt还提供了 foreach关键字,用来快速迭代整个容器(这个我经常使用)。

一、容器类

Qt提供了序列式容器: QList, QLinkedList, QVector, QStack, 以及QQueue.对于大多数应用开发来说,QList是最好的容器使用类型。QList被设计的其实是一个数组链表。因此它在链表头尾操作时有非常好效率。如果我们需要指针类型格式的链表,就要使用 QLinkedList。QVector的底层就是一个数组,那样它的内存和数组一样是连续的。常用的 LIFO(后入先出) 以及 FIFO(先入先出)等数据结果也有对应的容器:QStack、QQueue,也就是栈和堆。

Qt提供 关联式容器:QMap, QMultiMap, QHash, QMultiHash, 以及 QSet。带"Multi"容器可以方便地支持与单个键关联的多个值。哈希容器通过使用哈希函数而不是二进制搜索来对排序集提供更快的查找。
下面列出容器概述表

描述
QList 这个类最常用,Qt很多API的容器参数基本上都是QList类型的【因为这个类开销小,很多API默认参数基本就是QList类型】,因为QList 是使用数组实现的,数据之间有顺序的,也就支持索引的方式,从而确保基于索引的访问非常快。数组有的功能,QList都有,增删改查,数组的优缺点,QList也是一样的继承下来了。【优点:索引方便、首尾添加方便;缺点:数据量巨大时插入不方便】
QLinkedList 类似QList,除了实现的方式是指针结构体形式,而不是数组形式外,其他在使用是没啥区别;学过数据结构的都知道指针链表的特点是啥,插入快,遍历慢【都是数据量大的时候】
QVector 特点:节点内存连续,优缺点和数组一致,但是功能封装的比数组强大。
QStack 其实就是QVector的子类,使用了QVector的局部功能,就是使用了栈的特性(LIFO)
QQueue 这个是QList的一个子类,使用的QList的局部功能,就是使用了队列的特性(FIFO)
QSet 子集的特性:全局保留一份数组,可以去重复。
QMap<Key, T> 这提供了一个字典(关联数组),该字典将 Key 类型的键映射到 T 类型的值。通常每个键都与单个值相关联。QMap按键顺序存储其数据;如果顺序无关紧要,QHash是一个更快的选择。 QMap会排序键,通过键访问值。
QMultiMap<Key, T> 字面来看,就是一个键能对应多个值,类似信号可以关联多个槽函数一样,一对多
QHash<Key, T> 也是和QMap功能相似,只是通过哈希算法实现的键值匹配,而且匹配更快,但是键是无序排列
QMultiHash<Key, T> 和QMultiMap功能一直,只是实现方式不一致,有自己的适应范围。

注意:
存储在各种容器中的值可以是任何可分配的数据类型。符合条件的数据类型必须能提供复制构造函数和赋值运算符。对于某些操作,还需要默认构造函数。这里涵盖了能被存储在容器中的大多数数据类型,包括基本类型(如 int 和 double等)、指针类型和 Qt 数据类型(如 QString、QDate 和 QTime等)。但是不包括QObject或任何QObject子类类型如(QWidget,QDialog,QTimer等)。 如果尝试实例化 QList< QWidget >,编译器将把 QWidget 的复制构造函数和赋值运算符禁用,然后这个容器就失效了。Qt为我们想到了另外一种方式:把这些类型的对象的地址存储在容器中,也就是将它们存储为指针类型,例如 QList<QWidget *> ,这样就能达到我们的目的.

//不能使用这种方式
QList <QWidget> widgets;

//而是这样
QList<QWidget *> ptr_widgets;

二、迭代器

迭代器提供了一种统一的方法来访问容器中的项目。Qt的容器类提供了两种类型的迭代器:Java风格的迭代器和STL风格的迭代器。

一、Java风格

Java风格的迭代器在Qt 4中就被引入了,这种风格已经是Qt应用程序中使用的标准迭代器。它们比STL风格的迭代器更易于使用,但代价是效率略低。
对于每个容器类,有两种 Java 样式的迭代器数据类型:

  • 一种提供只读访问权限
  • 另一种提供读写访问权限
容器名 只读 iterator 读写 iterator
QList, QQueue QListIterator QMutableListIterator
QLinkedList QLinkedListIterator QMutableLinkedListIterator
QVector, QStack QVectorIterator QMutableVectorIterator
QSet QSetIterator QMutableSetIterator
QMap<Key, T>, QMultiMap<Key, T> QMapIterator<Key, T> QMutableMapIterator<Key, T>
QHash<Key, T>, QMultiHash<Key, T> QHashIterator<Key, T> QMutableHashIterator<Key, T>

学习的话要重点学习 QList 和 QMap。QLinkedList,QVector和QSet的迭代器类型与QList的迭代器具有完全相同的接口;同样的,QHash 的迭代器类型与 QMap 的迭代器具有相同的接口。
与 STL 样式的迭代器(如下所述)不同,Java 样式的迭代器指向项之间,而不是直接指向项。 因此,它们要么指向容器的最开头(在第一个项目之前),要么指向容器的最末尾(在最后一个项目之后),要么指向两个项目之间。下图显示了包含四个项目的列表的有效迭代器位置作为红色箭头:
在这里插入图片描述

1. QListIterator

如下面的代码一样:迭代器 i 是一个只读的迭代器,正向遍历 初始化后的位置是在 A 前面

//正向遍历
QList<QString> list;
list << "A" << "B" << "C" << "D";

QListIterator<QString> i(list);
while (i.hasNext())
	qDebug() << i.next();

//反向遍历
QListIterator<QString> i(list);
i.toBack();
while (i.hasPrevious())
	qDebug() << i.previous();

一些方法的示意样子图
在这里插入图片描述
下表总结了 QListIterator API:

功能 解释
toFront() 移动迭代器到可迭代对象的第一个位置(在节点最前)
toBack() 移动迭代器到可迭代对象的最后一个位置(在节点末尾)
hasNext() 判断可迭代对象是否有下一个对象
next() 返回下一个对象,并移动迭代器到后一个位置
peekNext() 返回下一个对象,但不移动迭代器
hasPrevious() 判断可迭代对象是否有前一个对象
previous() 返回前一个对象,并移动迭代器到前一个位置
peekPrevious() 返回前一个对象,但不移动迭代器

这个函数名其实以及说的很清楚,大概知道意思,实在不知道再去查帮助文档。

2. QMutableListIterator

面对要修改迭代对象时,就需要使用 QMutableListIterator 来完成

//删除节点
QMutableListIterator<int> i(list);
while (i.hasNext()) {
    
    
	if (i.next() % 2 != 0)
		i.remove();
}

//修改节点值 一
QMutableListIterator<int> i(list);
while (i.hasNext()) {
    
    
	if (i.next() > 128)
		i.setValue(128);
}

//修改节点值 二
QMutableListIterator<int> i(list);
while (i.hasNext())
	i.next() *= 2;

3. QMapIterator

我们现在将转向QMapIterator,它有些不同,因为它迭代(键,值)对。与 QListIterator 一样,QMapIterator 提供和QListIterator功能的函数如

  • toFront()
  • toBack()
  • hasNext()
  • next()
  • peekNext()
  • hasPrevious()
  • previous()
  • peekPrevious()。
    但是区别在于迭代器调用 next()、peekNext()、previous() 或 peekPrevious() 返回的对象是,键值对 的类型,我们可以调用 key(),value() 函数来获取键值对里的值。
QMap<QString, QString> map;
map.insert("Paris", "France");
map.insert("Guatemala City", "Guatemala");
map.insert("Mexico City", "Mexico");
map.insert("Moscow", "Russia");

QMapIterator<QString, QString> i(map);
while (i.hasNext()) {
    
    
	qDebug()<<"key:"<<i.next().key()<<" value: "<<i.next().value();
}

4. QMutableMapIterator

特性就是能读能写,还能搭配能使用同类型迭代器使用

//删改示例
QMap<QString, QString> map;
map.insert("Paris", "France");
map.insert("Guatemala City", "Guatemala");
map.insert("Mexico City", "Mexico");
map.insert("Moscow", "Russia");

QMutableMapIterator<QString, QString> i(map);
while (i.hasNext()) {
    
    
	if (i.next().key().endsWith("City"))
		i.remove();
}

//QHash和QMap交互
QMap<int, QWidget *> map;
QHash<int, QWidget *> hash;

QMapIterator<int, QWidget *> i(map);
while (i.hasNext()) {
    
    
	i.next();
	hash.insert(i.key(), i.value());
}

二、STL风格

STL 风格的迭代器自 Qt 2.0 发布以来就已经可用。它们与Qt和STL的通用算法兼容,并针对速度进行了优化。
对于每个容器类,有两种 STL 样式的迭代器类型:

  • 一种提供只读访问权限
  • 另一种提供读写访问权限

形式上的区别就是看有没有加上 const_ 前缀
应尽可能使用只读迭代器,因为它们比读写迭代器更快。

容器名 只读 iterator 读写 iterator
QList, QQueue QList::const_iterator QList::iterator
QLinkedList QLinkedList::const_iterator QLinkedList::iterator
QVector, QStack QVector::const_iterator QVector::iterator
QSet QSet::const_iterator QSet::iterator
QHash<Key, T>, QMultiHash<Key, T> QHash<Key, T>::const_iterator QHash<Key, T>::iterator

STL 迭代器的 API 以数组中的指针为模型。例如,++ 运算符将迭代器前进到下一个项,* 运算符返回迭代器指向的项。实际上,对于将项目存储在相邻内存位置的 QVector 和 QStack,迭代器类型只是 T * 的 typedef,而 const_iterator 类型只是 const T * 的 typedef。
重点介绍 QList 和 QMap。QLinkedList,QVector和QSet的迭代器类型与QList的迭代器具有完全相同的接口;同样,QHash 的迭代器类型与 QMap 的迭代器具有相同的接口。

1. QList-iterator

迭代器指向的节点值就是一个一个节点类型。

QList<QString> list;
list << "A" << "B" << "C" << "D";

QList<QString>::iterator i;
for (i = list.begin(); i != list.end(); ++i)
	*i = (*i).toLower();

Java 风格的迭代器不同,STL 样式的迭代器直接指向项。 容器的 begin() 函数返回指向容器中第一项的迭代器。容器的 end() 函数将迭代器返回到虚构项,该项的位置比容器中的最后一项晚一个位置。end() 标记无效仓位;它绝不能被取消引用。它通常用于循环的中断条件。如果列表为空,begin() 等于 end(),因此我们从不执行循环。
在这里插入图片描述
在迭代时,关于迭代的item如何获取,以及如何跨步长来获取,看下表

操作 含义
*i 返回当前节点
++i 迭代器后移一位
i += n 迭代器后移 n 位
–i 迭代器前移一位
i -= n 迭代器前移 n 位
i - j 两个迭代器直接的索引差

2. QMap- iterator

对于QMap和QHash,*i 返回项的值和Java-style迭代器返回是一样的 键值对 。要获取迭代器节点的键 需要调用key()。同样的迭代器类型还提供了一个value()函数来获取值。其实很像QList里面存放结构体类型一样。

QMap<int, int> map;

QMap<int, int>::const_iterator i;
for (i = map.constBegin(); i != map.constEnd(); ++i)
	qDebug() << i.key() << ':' << i.value();

3. 隐式共享的问题

  • 当迭代器在容器上处于活动状态时,应该避免复制该容器【例子一】
  • 我们应该始终获取容器的副本,并对副本进行迭代【例子二】

例子一:
像这样的写法 不对

QVector<int> a, b;
a.resize(100000); // make a big vector filled with 0.

QVector<int>::iterator i = a.begin();
// WRONG way of using the iterator i:
b = a;

a[0] = 5;

b.clear(); // Now the iterator i is completely invalid.

int j = *i; // Undefined behavior!

最好改为这样的

QVector<int>::iterator i = a.begin();
b = a;

QVector<int>::iterator i = a.begin();

a[0] = 5;

b.clear(); 

int j = *i; 

例子二

// RIGHT
const QList<int> sizes = splitter->sizes();
QList<int>::const_iterator i;
for (i = sizes.begin(); i != sizes.end(); ++i)

// WRONG
QList<int>::const_iterator i;
for (i = splitter->sizes().begin();
	i != splitter->sizes().end(); ++i)

三、foreach 关键字

如果你只想按顺序遍历容器中的所有项,你可以使用Qt的foreach关键字。该关键字是c++语言中特定于qt的附加内容,是使用预处理器实现的。

用法很简单:

foreach (variable, container)
{
//操作
}
操作里面包括:获取值节点,break退出循环 都可以

1. QList类型

//例子一
QLinkedList<QString> list;
QString str;

foreach (str, list)
	qDebug() << str;

//例子二
QLinkedList<QString> list;
  foreach (const QString &str, list)
      qDebug() << str;
    
//例子三
QLinkedList<QString> list;
  foreach (const QString &str, list) {
    
    
      if (str.isEmpty())
          break;
      qDebug() << str;
  }

2. QMap类型

直接上例子。没啥特殊的

QMap<QString, int> map;
  foreach (const QString &str, map.keys())
      qDebug() << str << ':' << map.value(str);

//多键值对映射时
QMultiMap<QString, int> map;
  foreach (const QString &str, map.uniqueKeys()) {
    
    
      foreach (int i, map.values(str))
          qDebug() << str << ':' << i;
  }

当Qt进入foreach循环时,它会自动获取容器的副本【也就是会拷贝一份副本】。如果你在迭代的时候修改了容器,那不会影响循环。(如果不修改容器,复制仍然会发生,但是由于Qt使用了隐式共享,复制容器的操作非常快)
Qt的foreach循环的替代方法是基于范围的for,它是C++ 11和更新版本的一部分。但是,请记住,基于范围的for可能会强制分离Qt容器,而foreach不会。

四、其他类似容器类

Qt包括三个模板类,在某些方面类似于容器。这些下面这些类不提供迭代器,不能与foreach关键字一起使用。

变量名 描述
QVarLengthArray<T,Prealloc > 提供了一个低级的可变长度数组。在速度特别重要的地方可以代替QVector使用。
QCache<Key,T > 提供了一个缓存来存储与Key类型的键相关联的某个T类型的对象。
QContiguousCache < T > 提供了一种高效的方法来缓存通常以连续方式访问的数据。
QPair<T1,T2 > 存储一对元素

Qt的中存在的非通用模板类型有QBitArray、QByteArray、QString和QStringList。这些类存储的都是特定类型的item,虽然能使用迭代器的方法,但是已经失去通用容器的意义了。

五、算法复杂度

算法复杂性关注的是随着容器中项目数量的增加,每个函数的速度有多快(或多慢)。例如,在QLinkedList的中间插入一个项目是一个非常快速的操作,与QLinkedList中存储的项目数量无关。另一方面,如果QVector包含许多项,那么在QVector中间插入一个项开销可能非常大,因为必须将一半的项在内存中移动一个位置。

“Amort ”代表“分摊行为”。比如“Amort.O(1)”意味着如果只调用该函数一次,可能会得到O(n)行为,但是如果调用它多次(例如,n次),平均行为将是O(1)。

Qt的序列式容器复杂度

变量名 查找 插入 头部插入 尾部插入
QLinkedList O(n) O(1) O(1) O(1)
QList O(1) O(n) Amort. O(1) Amort. O(1)
QVector O(1) O(n) O(n) Amort. O(1)

Qt的关联式容器以及集合复杂度

变量名 键 查找平均情况 键 查找最坏情况 插入平均情况 插入最坏情况
QMap<Key, T> O(log n) O(log n) O(log n) O(log n)
QMultiMap<Key, T> O(log n) O(log n) O(log n) O(log n)
QHash<Key, T> Amort. O(1) O(n) Amort. O(1) O(n)
QSet Amort. O(1) O(n) Amort. O(1) O(n)

六、增长策略

1. 连续地址分配容器

QVector、QString和QByteArray在内存中连续存储它们的项;QList维护一个指向其存储的项的指针数组,以提供基于索引的快速访问(除非T是指针类型或指针大小的基本类型,在这种情况下,值本身存储在数组中);QHash<Key,T >保存一个哈希表,其大小与哈希中的项目数成比例。
为了避免每次在容器末尾添加项目时都重新分配数据,这些类通常会分配多的一些的内存。

假设我们向QString字符串追加了15000个字符。那么当QString用尽空间时,将发生以下18次重新分配(总共可能有15000次): 4、8、12、16、20、52、116、244、500、1012、2036、4084、6132、8180、10228、12276、14324、16372。最后,QString分配了16372个Unicode字符,其中15000个被占用。
上面的值可能看起来有点奇怪,但以下是指导原则:

  • 0 - 20:QString一次分配4个字符,直到它的大小达到20
  • 20 - 4084:它每次都以两倍的大小前进
  • 4084 - 更大,它按2048个字符(4096字节)的块前进。这是有意义的,因为现代操作系统在重新分配缓冲区时不会复制整个数据;物理内存页面被简单地重新排序,实际上只需要复制第一页和最后一页上的数据

QByteArray 和 QList跟QString在分配上是大同小异的。
但是若 QVector 也将该算法用于可以使用memcpy()在内存中移动的数据类型(包括基本的C++类型、指针类型和Qt的共享类),但对只能通过调用 复制构造函数 和 析构函数 来移动。由于在这种情况下重新分配的成本更高,所以QVector通过在空间用尽时总是将内存加倍来减少重新分配的次数

2. 关联容器

QHash<Key,T >是完全不同的情况。QHash内部是哈希表形式的以2为幂指数增长,每次增长时,项目节点都会在新的分配区中重新定位,计算方法为qHash(key) % QHash::capacity()哈希表容量)。这也适用于QSet和QCache<Key,T>。
对于大多数应用程序来说,Qt提供的默认增长算法可以解决这个问题。如果您需要更多的控制,QVector、QHash<Key,T >、QSet、QString和QByteArray提供了三个函数,允许您检查和指定使用多少内存来存储项目:

函数名 描述
capacity() 返回容器中所有节点所占的空间大小
reserve(size) 为容器显式的预分配内存大小
squeeze() 释放掉没有使用的容器空间,缩小容器的大小

猜你喜欢

转载自blog.csdn.net/qq_43680827/article/details/123413256#comments_28562389
今日推荐