C++ STL与Qt容器对比

〇、前言

  • 日常开发中,经常需要使用到一些数据结构去存放数据,纯C++开发时,只需要根据自己的需求选择合适的数据结构即可。但对于Qt/C++混编的场景,选择哪一家的数据结构就成为了一个问题,所以为了解决这个疑惑,便写篇博文详细对比下二者的差异以便后续参考。

一、数据结构对比

释义 Qt C++ STL
字符串 QString string
封装了索引的双链表 QList ×
双链表 QLinkedList list
动态数组 QVector vector
QStack stack
队列 QQueue queue
双端队列 × deque
优先队列 × priority_queue
集合 QSet set
字典/映射 QMap map
键值可重复字典/映射 QMultiMap multimap
哈希字典/映射 QHash unordered_map
键值可重复哈希字典/映射 QMultiHash unordered_multimap
缓存 QCache ×
连续缓存 QContiguousCache ×

1.1、QCache与QContiguousCache

  • 图形化界面开发时,一个不可忽视的问题就是内存优化,比如对于数量超大的列表如何加载,一个显然的优化思路是只展示用户可以看到的数据,如果超出显示区域就及时收回内存。
  • 那么基于这样的要求,QCache也就应运而生了,它提供了基于键值对的内存管理方式,当添加进一个元素后,QCache将自动获取其内存管理权限,并在需要的时候自动释放他们来为新插入的对象腾出空间,模板原型如下:
template 
class QCache
  • QContiguousCache顾名思义,是一块连续的缓存空间,即缓存中的元素是连续的。这样做的优点是可以消耗比 QCache 更少的内存和处理器周期。

1.2、deque与QList

  • deque的一个优势就是在于高效的头插和尾插,其内部实现机理是采用多段连续的数组进行“拼凑”,营造出整体内存空间连续的假象,从而对外提供下标随机访问。
  • Qt没有提供双端队列类型,但是有实现类似功能的数据结构QList,它也有高效的头插和尾插操作。Qt官网文档也建议QList应是默认的第一选择。

1.3、QList、QVector、QLinkedList

  • 需要注意的是QList其实不是链表,是优化过的vector,官方的形容是array list。它的存储方式是分配连续的node,每个node的数据成员不大于一个指针大小,所以对于int、char等基础类型,它是直接存储(所以按照内存对齐的话,应该会内存耗损更大),对于Class、Struct等类型,它是存储对象指针。

  • QList的实现模式,优点主要在于快速插入。因为其元素大小不会超过sizeof(void*),所以插入时只需要移动指针,而非如vector那样移动对象。并且,由于QList存储的是void*,所以可以简单粗暴的直接用realloc()来扩容。

  • QList的增长策略是双向增长,所以对prepend支持比vector好得多,使用灵活性高于Vector和LinkedList。缺点是每次存入对象时,需要构造Node对象,带来额外的堆开销。

  • 官方文档给出的复杂度对比如下:

    Index lookup Insertion Prepending Appending
    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的容器是基于C++的纯模板和继承实现,比如对于队列和栈这两种数据结构,实际上还是基于QList与QVector。这样做可以大大简化代码,但同时也意味耦合性更强。
class QQueue : public QList
class QStack : public QVector
  • 一个简单的例子是使用QStack时甚至可以两端同时弹出,这显然不符合栈这一数据结构先进后出的设计理念。
QStack s;
s.pop_front();
s.pop_back();
  • 在这一点上STL的设计则要更合理一些,同样是队列与栈的继承,STL的实现源码如下:
template >
	class queue
	{	// FIFO queue implemented with a container
        void push(const value_type& _Val)
		{	// insert element at beginning
			c.push_back(_Val);
		}

		void pop()
		{	// erase element at end
			c.pop_front();
		}
    protected:
		_Container c;	// the underlying container
	};

template >
	class stack
	{	// LIFO queue implemented with a container
        void push(const value_type& _Val)
		{	// insert element at end
		c.push_back(_Val);
		}

		void pop()
		{	// erase last element
		c.pop_back();
		}
    protected:
		_Container c;	// the underlying container
	};
  • 可以看到,在对应的容器内部实际上使用了另一个模板_Container作为成员变量,而对外接口则都是进行了封装后的产物。这样不但对数据结构的合理性做出了保证,而且同时将内部容器也作为一个模板参数对外提供,这样可以更加灵活的适配多种需求。
std::stack > ss;
ss.pop();

三、接口功能性

  • 在对外接口这一层面,Qt的容器可谓完爆STL,Qt的接口不光功能全面,而且还提供了许多类型转换的接口。
  • 比如通过如下的接口就可以实现获取所有key或value的列表,虽然内部逻辑就是简单的创建+拷贝,但为使用者却提供了很大便利。
template 
Q_OUTOFLINE_TEMPLATE const Key QMap::key(const T &avalue, const Key &defaultKey) const
{
    const_iterator i = begin();
    while (i != end()) {
        if (i.value() == avalue)
            return i.key();
        ++i;
    }

    return defaultKey;
}

template 
Q_OUTOFLINE_TEMPLATE QList QMap::values() const
{
    QList res;
    res.reserve(size());
    const_iterator i = begin();
    while (i != end()) {
        res.append(i.value());
        ++i;
    }
    return res;
}
  • 但同时也因为Qt接口的功能性强大,许多不规范的写法也因此诞生,比较典型的写法如下,先查询一次后再获取下标,实际上可以直接通过indexOf的返回值进行判断。
/* QList的contains和indexOf连用 */
if (calibSavedList_.contains(pSelectedButton) && i == calibSavedList_.indexOf(pSelectedButton))
  • 相较于Qt容器接口的完备性,C++出于对性能的洁癖要求,很多复杂度很高的接口并不会提供,比如头部数据的插入与弹出。

四、迭代器访问方式

  • 二者都支持迭代器的访问方式,但Qt提供了Java和STL两种风格的迭代器。
  • Java风格
Containers Read-only iterator Read-write iterator
QList, QQueue QListIterator QMutableListIterator
QLinkedList QLinkedListIterator QMutableLinkedListIterator
QVector, QStack QVectorIterator QMutableVectorIterator
QSet QSetIterator QMutableSetIterator
QMap, QMultiMap QMapIterator QMutableMapIterator
QHash, QMultiHash QHashIterator QMutableHashIterator
  • STL风格
Containers Read-only iterator Read-write 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
QMap, QMultiMap QMap::const_iterator QMap::iterator
QHash, QMultiHash QHash::const_iterator QHash::iterator

五、结论

  • 参考博文①和博文②中的测试结论,一般情况下Qt容器的效率和C++ STL容器的效率差不多。所以处于性能方面的考虑几乎可以忽略不计。

  • 在关心数据结构封装性的情况下,采用C++ STL中的容器。

  • 在关心特定需求场景时,STL中的容器更加全面(如优先队列、维护有序性的红黑树集合)

  • 在关心数据结构本身接口的扩展性和易用性时,采用Qt的容器。

  • 需要使用Qt的线性容器时,除非有频繁的列表中间插入的需求(此时选用QLinkedList),否则一般无脑选择QList。

六、参考资料

QList Class | Qt Core 5.15.10

Qt 中的顺序容器:QList、QVector - 知乎 (zhihu.com)

Qt容器类——1. QList类、QLinkedList类和QVector类 - 零分的借口 - 博客园 (cnblogs.com)

C++之STL(标准模板库)介绍_软件开发技术爱好者的博客-CSDN博客_c++ stl库

性能特性测试系列1——STL容器,QT容器性能相关比较和总结_破晓前的彷徨的博客-CSDN博客_qt容器效率

你可能感兴趣的:(C++,Qt,qt,c++,开发语言,数据结构,链表)