STL 是 C++ 标准模板库,可以理解为一个包含数据结构和算法的软件框架,一共有六大组件
- 容器
- 算法
- 迭代器
- 配接器
- 仿函数
- 空间配置器
这篇文章主要总结序列式容器,下一篇总结关联式容器
常用的序列式容器有 vector、list、deque、string
vector
是一个可变大小的序列式容器,其底层结构是动态的数组,在堆中分配连续的内存空间来存储元素,它和数组不同之处就是,数组一般是静态的,大小是固定的,而 vector 的容量大小会随着有效元素的增加而变大
vector 同时也具有数组的特性,例如:
- O(1) 时间的快速访问
- 插入和删除的时间复杂度为O(N)
vector 的扩容规则 分为两种情况
-
如果后面内存够用的话,直接在原来的位置扩容,在 VS 编译器是以 2 倍的扩容方式
-
如果后面内存不够用,则操作系统会在内存的另一块区域寻找更大的内存空间,然后进行拷贝元素,最后释放原来的空间,所以 vector 在扩容的时候,会引发迭代器失效的问题,而这种扩容效率也比较低效
vector 迭代器
迭代器(iterator)就相当于是一个工具,当然也可以把它理解为指针,但又不完全是指针,迭代器是指针的泛指,因为迭代器有很多种,比如Java和 C# 都有他们自己的迭代器;C++ 中的迭代器是一种检查容器内元素并遍历元素的数据类型
使用案例
int myints[] = {16,2,77,29 };
int n = sizeof(myints) / sizeof(myints[0]);
std::vector<int> fifth(myints, myints +n);
// 写法一:定义迭代器,名字是 it
std::vector<int>::iterator it = fifth.begin();
// 写法二:auto it = fifth.begin()
for (; it != fifth.end(); ++it) {
std::cout << *it<<' ';
}
std::cout << '\n';
当然一个容器的迭代器有可能会有很多种类,比如,正向,反向,const 类型的迭代器
// 无法修改的 const 迭代器
std::vector<int>::const_iterator it = fifth.cbegin();
逆向迭代器
std::vector<int>::reverse_iterator it = fifth.rbegin();
迭代器的失效问题(重点)
什么是迭代器失效,就是对容器的一些操作影响了元素的位置
(1) 删除 pos 位置的数据会导致 pos 迭代器失效,以及 pos 位置后面的迭代器也将失效
例如 1 2 3 4 5 6 7 8 9 ,假如此时迭代器指向6,那我们把 6 元素删除后,vector 中 6 后面的元素会依次向前走, 此时迭代器指向的时候 7 元素了,但是如果还对迭代器进行 ++ 操作,就会出错,因为被操作的迭代器是指向 6 的,它已经被删除了,正确做法是让 erase 重新赋值到指向 7 的迭代器,因为每删除一个元素,erase 自动返回指向下一个位置的迭代器
迭代器失效,错误案列
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
// 实现删除v中的所有偶数
// 下面的程序会崩溃掉,如果是偶数,erase导致it失效
// 对失效的迭代器进行++it,会导致程序崩溃
vector<int>::iterator it = v.begin();
while (it != v.end()){
if (*it % 2 == 0) {
// 错误案列,没有返回下一个迭代器
v.erase(it);
}
++it;
}
正确做法
//erase会返回删除位置的下一个位置
vector<int>::iterator it = v.begin();
while (it != v.end()){
if (*it % 2 == 0) {
// 正确案列,这里要返回当前元素的下一个迭代器
it = v.erase(it);
}
else {
++it;
}
}
(2) 在 pos 位置插入元素会导致 pos 迭代器失效,如果插入的元素导致容器增容,那么整个迭代器就会失效,因为原来的空间很有可能就释放了,而迭代器依然指向的是原来的空间,这时候再去访问,就会发生内存错误
(3) 当插入一个元素时,end 操作方位的迭代器失效,因为之前的 end 迭代器指向的位置,和插入后 end 位置不一样,需要更新
另外还有些注意事项,如下
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
v.erase(pos);
cout << *pos << endl; // 此处会导致非法访问
// 在pos位置插入数据,导致pos迭代器失效。
// insert会导致迭代器失效,是因为insert可
// 能会导致增容,增容后pos还指向原来的空间,而原来的空间已经释放了。
pos = find(v.begin(), v.end(), 3);
v.insert(pos, 30);
cout << *pos << endl; // 此处会导致非法访问
总结一下迭代器失效解决办法
当遍历时插入元素时,//想要指向下一个元素,要跳过当前和被添加的元素
iter = v.insert(iter,\*iter);
iter+=2;
遍历时删除元素,erase 函数返回的就是删除之后的元素的迭代器
iter=iter.erase(iter);
vector 基础使用
常见的四种构造函数
// 无参数构造
std::vector<int> first;
first.bush_back(100);first.bush_back(100);first.bush_back(100);first.bush_back(100);
// 构造并初始化 4 个 100
std::vector<int> second(4, 100);
// 使用迭代器进行构造
std::vector<int> third(second.begin(), second.end());
// 拷贝构造
std::vector<int> fourth(third);
尾插: push_back(),尾删:pop_back()
查找: find(), 插入:insert(), 删除:erase(), 交换:swap()
// 使用 find 查找 3 所在位置的 iterator
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
// 在 pos 位置之前插入 30
v.insert(pos, 30); std::vector<int>::iterator it = fifth.begin();
// 在 pos 位置删除元素
v.erase(pos);
vector 优点
时间效率 :由于底层是一块连续的内存空间,所以访问元素的时间复杂度是常数阶的
空间效率:相比于数组来说,更加节省空间,因为它可以动态的申请空间,而不像数组一样,在运行之前就必须指定大小,但在很多情况下我们都不知道需要的空间有多大,所以使用 vector 可以避免因为申请的空间过于大而造成浪费…
vector 缺点
和 list 相比,在进行插入删除操作的时候,效率低
而且 vector 只能在尾部进行插入和删除,不能任意位置进行插入删除
list 底层结构是一个带头结点的双向循环链表,所以在常数范围内可以任意位置进行插入和删除的,并且该容器可以前后双向迭代,因为一个节点有两个指针,分为前指针和后指针
下面总结了它的使用方法
构造函数
std::list<int> l1;
l1.push_back(100);
l1.push_back(100);
l1.push_back(100);
l1.push_back(100);
// l2中放4个值为100的元素
std::list<int> l2(4, 100);
// 用l2的[begin(), end())左闭右开的区间构造l3
std::list<int> l3(l2.begin(), l2.end());
// 用l3拷贝构造l4
std::list<int> l4(l3);
常用函数
std::list<int> l5(array, array + n);
// 返回第一个结点的值
l5.front();
// 返回最后一个节点的值
l5.back();
// 在第一个节点前面插入
l5.push_front(0);
// 在最后一个节点后面插入
l5.push_back(100);
// 删除第一个节点
l5.pop_front();
//删除最后一个节点
l5.pop_back();
// 在 pos 位置插入 val,默认插入一个val,在pos参数后面可以指定插入的个数
l5.insert(pos,val);// pos 是迭代器,而不是一个 int 值
// 在 pos 位置删除元素
l5.erase(pos);
l5.erase(first,last); //删除区间为 [first,last)的值
// 增加有效元素,默认值为 0,指定值为 val
// n 是最终的元素个数,所以n 必须要大于原来容器值的个数
l5.resize(n,val);
l5.swap(list&l4);//交换两个链表的内容
l5.clear()//清空l5
这里稍微说明一下,push_back 和 push_front 的缺点
既然有缺点就会有对比,和 emplace_back , emplace_front 相比起来,push_back 和 push_front 稍微有点低效率.如果是内置类型,那么push_back 和 emplace 效率基本差不多.
如果是自定以类型,其低效率主要表现在,在调用 push_back 函数之前要先构造好对象,然后把对象传入到 push_back 参数中,传进行之后,push_back 还得调用一次拷贝构造函数,而对于 emplace_back 只需要调用一次构造函数即可,没有调用拷贝构造函数
Date d(2019, 12, 20);
l2.push_back(d); // push_back 是无法直接传入数值的
l2.emplace_back(2019,12,21);
list 迭代器
list 迭代器用法和 vector 迭代器几乎差不多
定义迭代器
list<int>::iterator it = L.begin();
list<int>::reverse_iterator it = L.rbegin();
list<int>::const_iterator it = L.cbegin();
迭代器失效的问题(重要)
list 迭代器只有在删除的时候才会失效,而且仅仅是当前被删除元素的迭代器,其它的迭代器不受影响。
int array[] = { 16,2,77,29 };
int n = sizeof(array) / sizeof(array[0]);
std::list<int> l5(array, array + n);
// C++11 另一种迭代器的定义
auto it = l5.begin();
while (it != l5.end()) {
l5.erase(it);
// 错误案列:it 迭代器已经失效,所以不能 ++ 了
++it;
}
auto it = l5.begin();
while (it != l5.end()) {
// 正确案列:删除后会自动返回下一个 it
it = l5.erase(it);
}
list 与 forward_list 非常相似:最主要的不同在于 forward_list 是单向链表,只能朝前迭代,而 list 是双链表,可以前后迭代
list 的应用以及优缺点
与其他的序列式容器相比(array,vector,deque),list 通常在任意位置进行插入、移除元素的执行效率更好
与其他序列式容器 array vector ,相比 list 和 forward_list 最大的缺陷是不支持任意位置的随机访问,因为底层并不是连续的内存空间,元素之间是通过指针的指向来访问;所以只能从头部或者尾部迭代,在查找时需要更多的时间开销,
另外,list 还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大 list 来说这可能是一个重要的因素)
vector 和 list 比较
vector | list | |
---|---|---|
底层结构 | 动态顺序表 | 带头节点的双向链表 |
随机访问 | 支持随机访问 | 不支持随机访问 |
插入和删除 | 任意位置插入和删除效率低 | 效率高,不搬移元素 |
空间利用率 | 底层为连续空间,利用率高 | 底层节点动态开辟 |
迭代器 | 原生态指针 | 对原生指针封装了 |
迭代器失效 | 插入和删除都可能导致失效 | 只有删除的时候才会失效 |
使用场景 | 高效存储,不关心插入和删除 | 大量的插入和删除 |
string 序列式容器
在 C++ 中,string 也可以理解为一种字符串的数据类型
在 string 中会有一个深拷贝和浅拷贝的问题,是关于指针和内存的问题,我单独写了文章,因为放在一起的话字数会很多
参考文章https://blog.csdn.net/qq_43763344/article/details/90575615
deque 序列式容器
双端队列是动态大小的序列式容器,其两端可以伸缩,它的底层结构还是比较复杂的, 是 中央控制器和多个缓冲区,其复杂之处主要体现在 首尾可以快速增删,以及支持任何位置的随机访问
因为 deque 和 vector 功能特别相似,但 deque 在头部和尾部进行数据插入和删除操作更加高效,与vector不同的是,deque不能保证所有的元素存储在连续的空间中,在 deque 中通过指针加偏移量方式访问元素可能会导致非法的操作,因为其底层并不是连续的内存空间,它的元素可能分散在不同的存储块
由于在 deque 中保存信息,依然可以常数阶访问任何一个位置,这些额外的信息,也是 deque 相比于 vector 的优势,特别是在扩容的时候,不需要进行拷贝元素,但是 vector 有个库函数弥补了这个缺陷,这个库函数就是 reserve ,功能是提前预留好空间,这样 vector 在插入的时候,就不会进行拷贝元素了
deque 迭代器
deque 迭代器的工作是比较复杂的,需要在不同区块间跳转,所以它非一般指针,deque 看起来像是连续的内存空间,其实都是迭代器在背后实现的
deque 的内存区块不再被使用时,会自动被释放。deque的内存大小是可自动缩减的
底层示意图

常用接口
构造函数
deque<int>d1{100,100,100,100};
deque<int>d2(4, 100);
deque<int>d3(d2.begin(), d2.end());
deque<int>d4(d2);
deque 基本操作函数以及迭代器操作和 list 特别相似
front() 返回 deque 首元素的引用
back() 返回 deque 尾元素的引用
push_back(val) 在尾部插入元素
push_front(val) 在头部插入元素
push_pop() 在尾部删除元素
pop_front()在头部删除元素
//在deque的position位置插入值为val的元素
iterator insert (iterator position, const value_type& val)
//删除deque中position位置的元素,并返回该位置的下一个位置
iterator erase (iterator position)
void swap (deque& x) //交换
void clear() 清除元素
// 利用标准库中的算法对deque中的元素进行升序排序
sort(d.begin(), d.end());
deque 应用场景
deque 最大的应用就是作为标准库中 stack 和 queue 的底层结构,因为存储元素或者查找删除元素的序列式容器 vector 和 list 比 deque 高效
stack 和 queue 属于适配器
STL 容器适配器
参考文章https://blog.csdn.net/qq_43763344/article/details/100053612