C++顺序容器的操作及总结
基于C++ primer第九章的总结
文章目录
1. 顺序容器概述
顺序容器类型:
- vector:可变大小数组,支持快速随机访问。速度慢
- deque:双端队列,指出快速随机访问。速度慢
- list:双向链表,只支持双向顺序访问。开销大
- forword_list:单向链表,只支持单向顺序访问。开销大
- array:固定大小数组,支持快速随机访问。
- string:与vector类似,只能保存字符
使用哪种容器?
- 一般情况下应该使用vector
- 需要在中间位置插入删除元素选择 list,forword_List
- 空间的开销很重要则不要选择 list,forword_List
- 随机访问元素,选择vector或deque
- 在头尾插入删除,不会在中间位置操作,选择deque
- 特殊情况下,那种操作执行多,则选用那种容器
2. 所有顺序容器都支持的操作
迭代器范围
迭代器有公共接口,操作适用于大多数容器,forword_list不能使用迭代器的–运算符
迭代器支持的算术运算,只适用于string vector deque array等迭代器。
begin和end迭代器:begin指向首元素;end指向尾元素之后的位置
为什么? 为了防止容器中只有一个元素,being和end不相等时,容器至少含有一个元素
构成范围的迭代器的要求:
- begin和end必须指向容器中的某个元素,或是最后的位置
- begin通过递增可以到达end,begin一定在end的前面
让一个迭代器查找指定的元素:
#include <iostream> #include <list> #include <deque> #include <vector> using namespace std; vector<int>::iterator VectorFindNum(vector<int>& vec, int a); int main() { vector<int> a{ 1,2,3,4,5,6,7,8,9 }; int num = 15; auto itx = VectorFindNum(a, num); if (itx == a.end()) { cerr << "未找到!\n"; return -1; } cout << *itx << endl; return 0; } vector<int>::iterator VectorFindNum(vector<int>& vec, int a) { vector<int>::iterator it; for (it = vec.begin(); it != vec.end(); ++it) { if (*it == a) { return it; } } return vec.end(); }
2.1 begin和end成员
形成一个包含迭代器的所有元素的迭代器范围
使用auto时:当我们希望对非const对象生成迭代器时,使用begin和end,只能得到iterator版本;使用cbegin和cend可以得到const_iterator的版本;对const的对象使用,一定会生成一个const_iterator版本
注意:对于同一行的auto,一个容器有const,另一个没有,则it2会报错,因为 it2会生成const_iterator版本,与前面的非const版本不同
list<int> a{ 1,2,3,4,5,6 }; const list<int> b{ 1,2,3 }; auto it = a.begin(), it2 = b.begin(); //错误
2.2 容器的定义和初始化
一个容器初始化为另一个容器的拷贝
使用构造函数的方式:两个容器的类型和元素类型必须匹配!!
使用迭代器范围的方式:容器类型和元素类型可以不必匹配,因为获取的是元素。
list<int> a{ 1,2,3 }; vector<double> b{ 1.1,2.2,3.3 }; list<int> a1(a); list<double> a2(a); //元素类型不匹配 vector<int> a3(a); //容器类型不匹配 vector<int> b1(b.begin(), b.end()); list<string> b2(b.begin(), b.end());
与顺序容器大小相关的构造函数
提供一个初始大小与初始化值,如果未提供值,则会自动创建一个值初始化器(即自动规定数值)
标准库array
array必须要在定义时规定大小,因为其大小是固定的。
可以进行列表初始化,但要小于等于此大小,不能超过大小限制
array也支持拷贝与赋值
array<int, 10> a{ 1,2,3,4,5 }; array<int, 3> b{ 1,2,3,4 }; //越界 //支持拷贝 array<int, 10> c(a);
简单总结
六种初始化和赋值容器的方式,除了array,皆适用
vector<int> vec; // 0 vector<int> vec(10); // 0 vector<int> vec(10,1); // 1 vector<int> vec{1,2,3,4,5}; // 1,2,3,4,5 vector<int> vec(other_vec); // same as other_vec vector<int> vec(other_vec.begin(), other_vec.end()); // same as other_vec
2.3 赋值和swap
赋值相关运算适用于所有容器
在赋值中,左侧对象会变成右侧的元素对象拷贝
即:自动改变长度大小
list<int> a{ 1,2,3,4,5 }; list<int> b{ 1,2,3 }; a = b; //a自动变为与b一样的大小 a = { 1,2,3,4,5,6,7,8,9 };//自动改变大小
注意array除外:他不支持改变大小
array<int, 5> ar{ 1,2,3,4,5 }; array<int, 2> br{ 1,2 }; ar = br; //不支持 ar={0}; //不支持
使用assign
他将右边运算对象中的元素拷贝到左边运算对象,也就是说实现了迭代器的使用(array除外)
利用迭代器拷贝元素值
list<string> name; vector<const char*> str{ "我爱你","于良浩" }; //实现了将vector的const char*赋值给了list的string name.assign(str.begin(), str.end()); name.assign(name.begin(), name.end());
第二个版本:接受大小与初始值
清除原来的元素,然后再插入10个"www",相当于先clear再insert
name.assign(10, "www");
swap交换容器
速度很快:元素本身并未改变,只是交换了两个容器的内部数据结构
list<int> a(10); list<int> b(20); //交换容器,包括大小即数值 a.swap(b); swap(a, b);
如果两个对象具有指针或者引用,迭代器所指向的对象不变!!
即使被swap交换,则仍然指向一个开始指向的对象(专一)
2.4 关系运算
比较容器间及其每个元素之间的关系,每个容器都支持==和!=
相比较的两个容器必须相同类型
3. 顺序容器所特有的操作
3.1 向容器添加元素
- 注意:array不支持!插入一个元素后,原有的指针 引用和迭代器都会失效**
push_back
将一个元素追加到容器的尾部,array和forword_list不支持此方法
可用于list vector deque的尾部插入,也可以在string对象后面插入一个字符
list<int> a; for (int i = 0; i < 5; i++) { a.push_back(i); } for (auto x : a) { cout << x << " "; } string str{"woaini"}; str.push_back('d'); //在str的后面插入字符d
插入对象的值实际上是一份拷贝,与原始的提供值的对象无任何关联
push_front
可用于list; forword_list; deque支持。vector 和string不支持
forward_list<int> a; for (int i = 0; i < 5; i++) { a.push_front(i); } for (auto x : a) { cout << x << " "; }
insert插入
普通版本
list dewue vector string 都提供了这种操作,用于在其他位置插入。forword_list提供了另一种版本的insert
- 第一个参数:一个迭代器
- 第二个参数:一个插入的值
- 将一个值插入到迭代器的前面位置的元素
可以在不能使用头插的vector使用insert来达到头插。
注意:vector deque string使用insert是合法的,但是速度会很慢!!!!
vector<int> a; for (int i = 0; i < 5; i++) { a.insert(a.begin(), i); }
进阶版本
insert接受更多参数:指定数量的数值,迭代器范围,初始化列表
//插入范围内的元素 list<int> a{1,2,3}; vector<int> b{ 4,5,6 }; a.insert(a.begin(), 5, 1); //在a的前面插入5个1 a.insert(a.end(), b.begin()+2, b.end()); //在a的尾部插入b的迭代器范围的元素 a.insert(a.begin(), { 9,9,9 }); //在a的尾部插入一个参数列表
注意:VS2022 可以让迭代器指向自身的范围:(书中有误)
a.insert(a.begin(), a.begin(),a.end()); //不会出错,相当于复制二倍
使用返回值
用一个迭代器来隐式指明插入位置,并在每一次循环返回当前位置的迭代器,iter迭代器始终指向begin元素,相当于push_front头插
list<int> a{ 4,5,6 }; auto iter = a.begin(); for (int i = 3; i > 0; i--) { iter = a.insert(iter, i); }
emplace插入
也是插入的一种形式,不过与push不同的是,它处理的是类的构造函数的插入:
-
push将插入的元素拷贝到要插入的容器中
-
empl/ace将插入的元素作为某个对象的构造函数插入到容器中
struct io { int a; io() = default; io(int i) :a(i) {} }; //一个具有构造函数的类 vector<io> a; vector<io> b; a.emplace_back(); //调用无参构造函数,将创建的对象插入容器中 a.emplace_back(9); //调用构造函数 for (int i = 0; i < 5; i++) { b.emplace_back(i); } a.emplace(a.begin(),7); //两个参数的版本,调用构造函数生成对象插入到指定位置之前
3.2 访问元素
两种方式:
- 使用迭代器begin和end,begin指向第一个,end递减依次后指向最后一个
- 使用front和back成员函数,front指向第一个,back指向最后一个(返回的是头或尾的引用)
- 不能对空容器调用front和end
下标操作和安全的随机访问: 适用于string deque vector array等容器
vector<int> a{ 1,2,3 }; cout << a[1] << endl; //重载了[]运算符 cout << a.at(2) << endl; //at访问 cout << a.at(4); //out of range
3.3 删除元素
forword_list不支持pop_back;vector和string不支持pop_front
pop_front和pop_back
删除第一个或者最后一个元素
vector<int> a{ 1,2,3 }; while (!a.empty()) { a.pop_back(); } //删除容器所有元素
erase删除
可以指定迭代器指定的元素,也可以删除指定范围的元素
vector<int> a{ 1,2,3 }; auto it = a.begin() + 1; //指向第二个元素 a.erase(it);
删除多个元素
erase:删除指定范围,返回最后一个被删元素之后位置的迭代器
clear:删除所有元素
简单总结
使用单迭代erase 删除list容器的奇数元素和vector的偶数元素,注意erase的返回值,返回被删的元素之后的元素迭代器
int ia[] = { 0,1,1,2,3,5,8,13,21,55,89 }; list<int> a; vector<int> b; for (auto x : ia) { a.push_back(x); b.push_back(x); } auto ita = a.begin(); auto itb = b.begin(); while (ita != a.end()) { if (*ita % 2) { ita=a.erase(ita); } else { ++ita; } } while (itb != b.end()) { if (*itb % 2==0) { itb=b.erase(itb); } else { ++itb; } }
4. forward_list的特殊操作
他是一个单向链表,在删除或添加元素的时候,需要改变其他的元素的连接,因此没有直接插入和直接删除的函数,它提供了insert_after emplace_after erase_after ;需要一个迭代器,然后插入在他的后面,或者删除它的后面
- before_begin:返回一个首前迭代器,即首元素的前面。
- insert_after:返回新插入的元素的迭代器
- erase_after:返回删除后的下一个元素的迭代器
删除foward_list的奇数元素:
forward_list<int> a{ 1,2,3,4,5,6,7,8,9 }; auto prev = a.before_begin(); //一个前驱迭代器 auto curr = a.begin(); //一个用于遍历的迭代器 while (curr != a.end()) { if (*curr % 2) { //删除节点,则必须传递给他前驱的节点,返回删除后的下一个元素的迭代器 curr = a.erase_after(prev); } else { prev = curr; ++curr; } }
5. resize改变容器大小
array不支持resize
如果重置后的大小增大了,可以指定一个值参数, 放在后面新增的空间中,也可以是默认的
如果重置后大小减小,则直接删除多余的部分
list<int> a(10, 5); a.resize(20, 1); a.resize(5);
6. 容器操作会使迭代器无效
- vector和string,deque容器:添加删除元素在任意位置都会导致迭代器失效,但是指针和引用仍然有效
- list和forward_list容器:添加删除元素在任意位置,迭代器,指针和引用均有效
- 每次在添加元素后, 要重新定位迭代器位置:vector string deque
示例:改变容器的循环程序
-
插入删除元素后更新使用新的迭代器的例子
-
insert 插入返回原始迭代器
-
erase删除返回下一个元素迭代器
vector<int> a{ 0,1,2,3,4,5,6,7,8,9 }; auto iter = a.begin(); while (iter != a.end()) { if (*iter % 2) { iter = a.insert(iter, *iter); iter += 2; //移动迭代器到新插入的元素和原来插入前指向的元素之后 } else { iter = a.erase(iter); } } 不要保存end返回的迭代器
7. Vector对象是如何增长的
管理容量的成员函数
- capacity:在不扩张内存的情况下,可以容纳多少个元素
- reserve:通知容器他应该准备多少个元素的内存空间
- shrink_to_fit:将capacity减少为与size相同大小
- 注意:resize改变容器的大小,但并不改变内存空间
capacity空间分配函数
- size:已经保存的元素的数目
- capacity:在不分配内存空间的前提下,它最多可以保存多少个元素
capacity会预保留一些空间,它要至少与size一样大
vector<int> a; cout << "size: " << a.size() << "\t" << "capacity: " << a.capacity() << endl; for (vector<int>::size_type i = 0; i != 24; ++i) { a.push_back(i); } cout << "size: " << a.size() << "\t" << "capacity: " << a.capacity() << endl;
reserve预分配空间:capacity至少等于reserve,也可能更大
a.reserve(50);
在超出预分配空间后,再次添加数据,则会重新分配空间
注意:重新分配的额外空间是reserve预分配空间的一半(VS2022)
shrink_to_fit:释放多余的空间,等于size的大小
resize:减小容器的容量,并不影响分配的空间;增大容量,内存空间会自动增大到resize(至少)
8. 额外的string操作(再补充)
string修改操作
string的构造函数:不再赘述
substr:返回一个string的一部分;或是全部的拷贝
insert,assign(所有内容),erase在string同样使用,提供了改变string的方法
append:在末尾追加字符串。
replace:相当于替换(erase和insert结合),在某个位置删除原字符并且插入新字符
string搜索操作
每个搜索操作都返回一个string::size_type的值,表示匹配发生位置的下标;
搜索失败返回string::npos的static成员
find:查找指定字符
find_first_of
find_first_not_of…简单看一下就好了
9. 容器适配器
三个容器适配器:stack,queue,priority_queue
定义适配器
从deque容器的类型元素拷贝到stack类型
deque<int> deq{1,2,3}; stack<int> stk(deq);
创建一个适配器时,将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型
从vector上实现的空栈:
stack<string, vector<string>> str_stk;
构造方式
适配器要有添加和删除操作,并且要有访问尾元素的功能,因此不能构造在array和forward_list类型之上
queue和stack可以构造于list和deque(需要push_front等操作),不能基于vector构造
priority_queue可以构造于vector和deque(随机访问功能),不能基于list构造
栈适配器
入栈 出栈 获取栈顶元素 判断为空
//栈适配器 stack<int> intStack; for (int i = 0; i < 10; i++) { intStack.push(i); } while (!intStack.empty()) { int val = intStack.top(); intStack.pop(); }
队列适配器
//队列适配器 queue<int> Que; for (int i = 0; i < 10; i++) { Que.push(i); } while (!Que.empty()) { int val = Que.front(); Que.pop(); }
关于string得到操作和适配器的 最后一道习题,之后会单独写一篇博客
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209743.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具