原文地址:https://blog.youkuaiyun.com/deng821776892/article/details/105663142/
前言
相信大家在学习C++过程中都会学习STL容器,至于容器实现的原理可以去看侯捷老师《STL源码剖析》。网上也有很多资源,为了支持正版这里就不放出来了,有需要可以评论区留言。STL容器的实现很复杂但是使用还是很一致的,相较于Java来说,C++的函数都是一致的。比如在所有容器里面调用大小值都是使用size()
函数。但是Java就不是这样,处理字符串的时候是使用size()
函数,处理list集合却是使用length()
函数。这一点看起来很简单,但是对于开发人员来说意味着需要多记忆一个内容。在C++里面绝大多数的容器都是拥有相同的成员函数,针对那一两个异类,我们可以单独进行分析即可。
*但是这么多容器我应该选择哪一个呢?*今天就给大家弄明白。
无规律容器
对于无规律容器来说,所有数据在内部都是无规律存放的。
vector
常见的操作:
- 可以使用中括号的下标来访问其成员;
- 可以使用 data 来获得指向其内容的裸指针;
- 可以使用 capacity 来获得当前分配的存储空间的大小,以元素数量计;
- 可以使用 reserve 来改变所需的存储空间的大小,成功后 capacity()会改变;
- 可以使用 resize 来改变其大小,成功后 size() 会改变;
- 可以使用 pop_back 来删除最后一个元素;
- 可以使用 push_back 在尾部插入一个元素;
- 可以使用 insert 在指定位置前插入一个元素;
- 可以使用 erase 在指定位置删除一个元素;
- 可以使用 emplace 在指定位置构造一个元素;
- 可以使用 emplace_back 在尾部新构造一个元素。
但是vector有一个特点就是在当前分配的空间占满之后,会重新分配一个两倍大小的空间,然后将原来的元素拷贝过去。
有的人可能会问为什么不是移动过去呢? 因为移动的话子在这过程中出现异常情况,那么原来的数据都会丢失,不满足强异常安全如果我们需要使用移动构造函数,可以进行声明然后将函数设置为noexcept
。
由于vector最大的问题就是大小增长带来的移动问题,所以尽可能提前预留好足够的空间,这样可以有效地提升vector的性能。
deque
deque是指的double-ended queue
deque的接口和vector相比有以下区别:
- deque 提供 push_front、emplace_front 和 pop_front 成员函数。
- deque 不提供 data、capacity 和 reserve 成员函数。
这是deque的内部布局:
可以看到的是,大部分都是顺序存储。所以deque满足vector的一些优点,而且能够克服vector随着大小增长导致的内存数据移动问题。 并且可以头尾增删元素。
list&forward_list
list在C++里面代表双向链表。
list内部结构:
同vector相比:
- list 不提供使用下标访问其元素;
- list 提供 push_front、emplace_front 和 pop_front 成员函数;
- list 不提供 data、capacity 和 reserve 成员函数。
list的特点:不适合于遍历容器,但是对于在中间频繁插入或删除元素十分有效。
list由于自身结构的特点,一部分标准算法无法实施,所以提供了成员函数作为替代:
merge | remove | remove_if | reverse | sort | unique |
---|
既然有了一个list为什么还要来一个forward_list呢? 由于list至少要有两个指针域会增加内存大小,而forward_list仅仅需要一个指针域。对于一些对内存敏感的项目,forward_list是一个不错的选择。
forward_list内部结构:
queue&stack
至于queue和stack都是依赖于deque的容器,也称为容器适配器。相较于deque可以双端操作来说,queue和stack都砍掉了一部分功能的操作。
queue只能在后面push()
,然后在前面pop()
;
stack只能在后面push()
,然后在后面pop()
。
为什么对于queue(stack)的pop()
只能发挥void,而不是返回容器的top元素呢?
因为pop()
返回元素时,如果要满足异常安全,实现起来十分复杂。但是如果将这两步分离,则尽管pop()
可能出现异常但是我们已经不关心内部的元素值了,所以出现异常是满足异常安全的。而top()
是一个只读操作,满足异常安全。
有规律容器
有规律容器,数据存放的位置与数据本身有关。
priority_queue
priority_queue和queue类似都是底子是其它容器,是一个容器适配器。他的特点在于,如果使用less作为其比较模版的话,最大的数值会出现在容器的“顶部”。如果我要最小值出现在容器顶部,那么可以将比较模版修改为greater。既然说到了less比较模版,那么就来看一下less吧。
less是一个函数对象,他是通过重载operator()
实现的,行为就是对指定对象进行<比较操作。
有序关联容器
有序关联容器包括set
,map
,multiset
,multimap
,内部是由红黑树实现的。
关联容器没有前后之分,但是依然提供了insert,emplace等函数。而且由于红黑树结构的特点,标准的find函数没办法实现查找功能。所以加入了几个查找函数:
- find(k) 可以找到任何一个等价于查找键 k 的元素(!(x < k || k < x))
- lower_bound(k)
找到第一个不小于查找键 k 的元素(!(x < k)) - upper_bound(k) 找到第一个大于查找键 k 的元素(k < x)
无序关联容器
无序关联容器,底层是用hash函数实现的。哈希函数大家都不陌生,但是针对复杂的类,hash函数需要进行特化,使得对于不同的对象值,得到的哈希结构尽可能均匀分布。
无序关联容器包括:unordered_set
,unordered_map
,unordered_multiset
,unordered_multimap
。
无序关联容器的特点在于其性能,插入删除操作时间复杂度均为O(log(n))
array
这个array是为了和C语言里面的数组兼容而设置的。但是他和C语言的数组还有一些区别:
- C 数组没有 begin 和 end 成员函数(虽然可以使用全局的 begin 和 end 函数);
- C 数组没有 size 成员函数;
- C 数组作为参数有退化行为,传递给另外一个函数后那个函数不再能获得 C 数组的长度和结束位置。
选择方法
根据功能选择:
需要先进先出,应选择queue;需要先进后出,应选择stack。
根据主要操作:
需要大量插入操作,应选择list;需要大量遍历操作,应选择vector,array,deque。
根据数据特征:
存在大小关系,应选择有序关联容器;数据之间不存在强关系,应选择无序关联容器。
根据数据量是否固定:
数据量不固定,应选择vector;数据量固定,应选择array。
总结
到这里都将近3000字了,但是STL的故事却不是这三两千字能够说得清楚的。虽然平时的使用中对于STL内部的结构并不需要十分清楚。但是了解的内部结构对于理解容器的一些操作有好处。正如侯捷老师说的那句:源码之前,了无秘密。
但是希望通过这一篇文章能够让大家对于C++容器能够有一个高屋建瓴的认识,对于C++容器大厦的结构有所了解。也希望通过这一篇文章能够让大家能够回答标题的那个问题,众多C++容器如何挑选?