共同点
- 容器都有开始和结束点
- 容器会记录其状态是否非空
- 容器有大小
- 容器支持交换
一、string:
使用策略:( 一般不建议在接口中使用 const string&)
- 如果不修改字符串的内容,使用 const string& 或 C++17 的 string_view 作为参数类型。后者是最理想的情况,因为即使在只有 C 字符串的情况,也不会引发不必要的内存复制。
- 如果需要在函数内修改字符串内容、但不影响调用者的该字符串,使用 string 作为参数类型(自动拷贝)。
- 如果需要改变调用者的字符串内容,使用 string& 作为参数类型(通常不推荐)。
二、vector
-
可以使用中括号的下标来访问其成员(同 string)
-
可以使用 data 来获得指向其内容的裸指针(同 string)
-
可以使用 capacity 来获得当前分配的存储空间的大小,以元素数量计(同 string)
-
可以使用 reserve 来改变所需的存储空间的大小,成功后 capacity() 会改变(同 string)
-
可以使用 resize 来改变其大小,成功后 size() 会改变(同 string)
-
可以使用 pop_back 来删除最后一个元素(同 string)
-
可以使用 push_back 在尾部插入一个元素(同 string)
-
可以使用 insert 在指定位置前插入一个元素(同 string)
-
可以使用 erase 在指定位置删除一个元素(同 string)
-
可以使用 emplace 在指定位置构造一个元素可以使用 emplace_back 在尾部新构造一个元素
特点:- 插入删除性能较高
- 适合尾部操作,这样不需要移动元素
当 push_back、insert、reserve、resize 等函数导致内存重分配时,或当 insert、erase 导致元素位置移动时,vector 会试图把元素“移动”到新的内存区域。vector 通常保证强异常安全性,如果元素类型没有提供一个保证不抛异常的移动构造函数,vector 通常会使用拷贝构造函数。因此,对于拷贝代价较高的自定义元素类型,我们应当定义移动构造函数,并标其为 noexcept,或只在容器中放置对象的智能指针。
#include <iostream>
#include <vector>
using namespace std;
class Obj1 {
public:
Obj1()
{
cout << "Obj1()\n";
}
Obj1(const Obj1&)
{
cout << "Obj1(const Obj1&)\n";
}
Obj1(Obj1&&)
{
cout << "Obj1(Obj1&&)\n";
}
};
class Obj2 {
public:
Obj2()
{
cout << "Obj2()\n";
}
Obj2(const Obj2&)
{
cout << "Obj2(const Obj2&)\n";
}
Obj2(Obj2&&) noexcept
{
cout << "Obj2(Obj2&&)\n";
}
};
int main()
{
vector<Obj1> v1;
v1.reserve(2);
v1.emplace_back();
v1.emplace_back();
v1.emplace_back();
vector<Obj2> v2;
v2.reserve(2);
v2.emplace_back();
v2.emplace_back();
v2.emplace_back();
}
输出结果:
Obj1()
Obj1()
Obj1()
Obj1(const Obj1&)
Obj1(const Obj1&)
Obj2()
Obj2()
Obj2()
Obj2(Obj2&&)
Obj2(Obj2&&)
//结果解释
Obj1() //已申请的空间的第一个位置(老空间)
Obj1() //已申请的空间的第二个位置(老空间)
Obj1() //新申请的双倍空间的第三个位置(新空间)
Obj1(const Obj1&) //将老空间的第一个位置的元素拷贝到新空间第一个位置
Obj1(const Obj1&) //将老空间的第二个位置的元素拷贝到新空间第二个位置
Obj1 和 Obj2 的定义只差了一个 noexcept,但这个小小的差异就导致了 vector 是否会移动对象。这点非常重要。
缺陷:
vector 的一个主要缺陷是大小增长时导致的元素移动。如果可能,尽早使用 reserve 函数为 vector 保留所需的内存,这在 vector 预期会增长很大时能带来很大的性能提升。
三 duque (double-ended queue) 双端队列
容器不仅可以从尾部自由地添加和删除元素,也可以从头部自由地添加和删除。
* deque 提供 push_front、emplace_front 和 pop_front 成员函数。
* deque 不提供 data、capacity 和 reserve 成员函数。
你需要一个经常在头尾增删元素的容器,那 deque 会是个合适的选择。
四 list
list 在 C++ 里代表双向链表。和 vector 相比,它优化了在容器中间的插入和删除:
- list提供高效的、O(1)复杂度的任意位置的插入删除操作
- list不提供使用下表访问其元素
- list提供push_front、emplace_front和pop_front成员
- list不提供data、capacity 和 reserve成员函数(和duque相同)
虽然 list 提供了任意位置插入新元素的灵活性,但由于每个元素的内存空间都是单独分配、不连续,它的遍历性能比 vector 和 deque 都要低。这在很大程度上抵消了它在插入和删除操作时不需要移动元素的理论性能优势。如果你不太需要遍历容器、又需要在中间频繁插入或删除元素,可以考虑使用 list。
五、forward_list(单向链表)
大部分 C++ 容器都支持 insert 成员函数,语义是从指定的位置之前插入一个元素。对于 forward_list,这不是一件容易做到的事情(想一想,为什么?)。标准库提供了一个 insert_after 作为替代。此外,它跟 list 相比还缺了下面这些成员函数:
- back
- size
- push_back
- emplace_back
- pop_back
forward_list优点:
在元素代大小较小的情况下,forward_list能节约的内存是非常可观的,在列表不长的情况下,不能反向的查找也不是大问题。提高内存的利用率,往往就能提高程序的性能。
六 queue 对列
特点: 先进先出(FIFO)的数据结构
- 不能按下标访问元素
- 没有 begin、end 成员函数
- 用 emplace 替代了 emplace_back,用 push 替代了 push_back,用 pop 替代了 pop_front;没有其他的 push_…、pop_…、emplace…、insert、erase 函数
七 stack 栈
后进先出的数据结构
stack 缺省也是用 deque 来实现,但它的概念和 vector 更相似。它的接口跟 vector 比,有如下改变:
- 不能按下标访问元素
- 没有 begin、end 成员函数
- back 成了 top,没有 front用 emplace 替代了 emplace_back,用 push 替代了 push_back,用 pop 替代了 pop_back;没有其他的 push_…、pop_…、emplace…、insert、erase 函数
stack 和内存管理中的栈的区别:
- stack:下面是低地址,向上地址则增大 (向上增长的)
- 内存管理中的栈:下面是高地址,向上地址则减小 (向下增长的)
七 关联容器 set、 map、multiset、multimap
关联容器有set(集合)、map(映射)、multiset(多重集)、multimap(多重映射)。关联容器是一种有序的容器。名字带“multi”的允许键重复,不带的不允许键重复。set 和 multiset 只能用来存放键,而 map 和 multimap 则存放一个个键值对。
- find(k) 可以找到任何一个等价于查找键 k 的元素(!(x < k || k < x)
- lower_bound(k) 找到第一个不小于查找键 k 的元素(!(x < k))
- upper_bound(k) 找到第一个大于查找键 k 的元素(k < x)
equal_range:
精确查找满足某个键的区间的话,建议使用 equal_range,可以一次性取得上下界(半开半闭)
#include <tuple>
multimap<string, int>::iterator lower, upper;
std::tie(lower, upper) = mmp.equal_range("four");
if(lower!= upper) // 检测区间非空
{
...
}
八 无序关联容器
从 C++11 开始,每一个关联容器都有一个对应的无序关联容器,它们是:
- unordered_set
- unordered_map
- unordered_multiset
- unordered_multimap
从实际的工程角度,无序关联容器的主要优点在于其性能。关联容器和 priority_queue 的插入和删除操作,以及关联容器的查找操作,其复杂度都是 O(log(n)),而无序关联容器的实现使用哈希表 [5],可以达到平均 O(1)!但这取决于我们是否使用了一个好的哈希函数:在哈希函数选择不当的情况下,无序关联容器的插入、删除、查找性能可能成为最差情况的 O(n),那就比关联容器糟糕得多了。
九、 array
array是 C 数组的替代品。C 数组在 C++ 里继续存在,主要是为了保留和 C 的向后兼容性。C 数组本身和 C++ 的容器相差是非常大的:
- C 数组没有 begin 和 end 成员函数(虽然可以使用全局的 begin 和 end 函数)
- C 数组没有 size 成员函数(得用一些模板技巧来获取其长度)
- C 数组作为参数有退化行为,传递给另外一个函数后那个函数不再能获得 C 数组的长度和结束位置
选择方案: - 如果数组较大的话,应该考虑 vector。vector 有最大的灵活性和不错的性能。
- 对于字符串数组,当然应该考虑 string。
- 如果数组大小固定(C 的数组在 C++ 里本来就是大小固定的)并且较小的话,应该考虑 array。array 保留了 C 数组在栈上分配的特点,同时,提供了 begin、end、size 等通用成员函数。