笔记会持续更新,有错误的地方欢迎指正,谢谢!
顺序容器概述
前言:这部分的内容在写程序的时候肯定是处处都能用到的,而且会让你的程序很简洁。本章是第三章的拓展,详细地介绍了标准库顺序容器的知识。一个容器就是一些特定类型对象的集合。
不同容器在两个方面的性能不同:
- 向容器中添加或删除元素;
- 访问容器中的元素。
详情如下:
容器 | 特征 |
---|---|
vector | 可变大小数组,支持快速随机访问,在尾部之外的位置插入或删除元素较慢。(因为在内存中元素是连续存储的,所以快速随机访问(想访问谁就访问谁)很容易,头的位置再加上你要访问的顺序就好,添加删除元素就需要移动它之后的所有元素,所以慢) |
list | 双向链表,只支持双向顺序访问,不支持随机访问,在任意位置插入删除都快,内存中不连续,通过指针指向前后元素 |
deque | 双端队列。支持快速随机访问,在头尾位置插入或删除元素很快。 |
forward_list | 单向链表,只支持单向顺序访问,在链表任意位置插入删除都快。也就是:不支持反向容器迭代器,且其迭代器不支持递减运算。 |
array | 固定大小数组,支持快速随机访问,不能添加或删除 |
string | 与vector类似,专门用于保存字符,随机访问快,在尾部插入删除快 |
标准库容器性能通常优于同类数据结构。
可以看到每个容器都对性能有所侧重,都有不同的灵活度。
那我们在选择容器的时候应该怎么选呢?
- 除非你有很好的理由选择其他容器,否则一律用vector;
- 程序要求随机访问,就用vector或deque;
- 程序有很多小的元素,需要很大的额外开销,不要用list或forward_list,因为它们再去连其他内存开销更大;
- 如果程序要在容器中间插入或删除元素,应使用list或forward_list;
- 程序只在头尾插入删除元素,用deque。
总结:在实际应用的时候,你一定要自己衡量好性能,只要你掌握了表格中各个容器的特点,相信你能做出最合适的选择!
容器库概览
这部分讲述所有容器的共性,每个容器都定义在一个头文件中,文件名和类型名相同,例如,deque定义在头文件deque中。容器均定义为模板类,我们必须提供额外信息来生成特定的容器类型。
对容器可以保存的元素类型的限制:
顺序容器几乎可以保存任意类型的元素,包括它自己。也有一些例外,比如类类型。
例子:我要保存一个类类型的对象,而这个类没有默认构造函数,于是,我得自己给它提供一个元素初始化器:
//假定a是一个没有默认构造函数的类型
vector<a> v1(10, init); //正确:10个a类型的元素init(也就是10个a的对象)
vector<a> v2(10); //错误:没有默认构造函数,无法初始化。
迭代器
之前介绍过,这里就不介绍了。
迭代器可加可减,但forward_list不支持减,理由不用解释。
反向迭代器
大多数容器还提供反向迭代器,反向迭代器++就是上一个元素。
reverse_iterator按逆序寻址的迭代器,rbegin()是尾迭代器,crend()表示前迭代器。
要遍历的话,就从crend()递减到rbegin()。
begin和end成员
list<string> a = {"1", "2", "3"};
auto it1 = a.begin(); //此auto为list<string>::iterator
auto it2 = a.rbegin(); //list<string>::reverse_iterator
auto it3 = a.cbegin(); //list<string>::const_iterator
auto it4 = a.crbegin(); //list<string>::const_reverse_iterator
当不需要修改时,最好用cbegin。
容器定义和初始化
每个容器类型都定义了一个默认构造函数。除了array之外,其他容器的默认构造函数都会创建一个指定类型的空容器。下图已经说得很清楚了:
补充:
explicit构造函数是用来防止隐式转换的。请看下面的代码:
class Test1
{
public:
Test1(int n)//普通构造函数
{
num=n;
}
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)//explicit(显式)构造函数
{
num=n;
}
private:
int num;
};
int main()
{
Test1 t1=12;//隐式调用其构造函数,成功。
Test2 t2=12;//不能隐式调用其构造函数,编译错误。
Test2 t2(12);//显式调用其构造函数,成功。这种就叫显示构造函数!!!
return 0;
}
与顺序容器大小相关的构造函数:
vector<int> a1(10, -1);//10个-1
vector<int> a2(10);//10个0
vector<string> a3(10);//10个空string
总结:如果元素类型是内置类型或有默认构造函数的类类型,可以在初始化的时候只为构造函数提供一个容器大小参数;如果没有默认构造函数,必须要显式地提供初值(具体例子可见本文“容器库概览”)。
标准库array具有固定大小:
列表初始化的数目必须等于或小于array的大小;花括号列表只能初始化不能赋值。
array的大小也是类型的一部分:
array<int, 3>; //类型是3个int的数组
array<int, 10> a1; //10个0
array<int, 3> a2 = {1, 2, 3}; //列表初始化
array<int, 3> a3 = {5}; //正确的。a3为5, 0, 0
我们不能对内置数组进行拷贝,但array可以(可能这也是为什么搞出array的原因):
array<int, 3> a = {1, 2, 3};//列表初始化
array<int, 3> copy = a;//拷贝
赋值和swap
vector<int> a1 = {1, 2, 3, 4, 5};
a1 = {1, 2}; //这样正确,a1变为1,2。
array<int, 5> b1 = {1, 2, 3, 4, 5};
b1 = {1}; //这样错误。只有array,花括号列表只能初始化不能赋值。
使用swap:
vector<string> s1(10);
vector<string> s2(20);
swap(s1, s2); //交换后,s1有20个元素,s2有10个元素。
容器大小操作
名称 | 含义 |
---|---|
size | 返回容器中元素个数 |
empty | 只在size为0时返回true |
max_size | 返回一个大于或等于该容器所能容纳的最大元素个数 |
关系运算符
容器装的元素类型支持关系运算,我们才能来用它。比如:内置类型
vector<int> v1 = {1, 3, 5, 7, 9, 12};
vector<int> v2 = {1, 3, 9};
vector<int> v3 = {1, 3, 5, 7};
v1 < v2;
v1 > v3;