顺序容器
- vector 可变大小数组,支持快速随机访问,在尾部插入或删除元素可能会很慢
- deque 双端队列,支持快速随机访问,在头尾插入/删除速度很快
- list 双向链表,只支持双向顺序访问,在list任何位置插入/删除速度很快
- forward_list 单向链表,只支持单向顺序访问,在链表任何位置插入/删除速度很快
- array固定大小数组,支持快速随机访问,不能添加或删除元素
- string 与array类似,但是是用来保存字符的
选用容器的基本原则
- 通常来说我们选用的是vector
- 如果程序中有很多小元素,且空间额外开销较大,则不要使用链表
- 如果程序只有在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则输入阶段使用list,随后拷贝到一个vector里
容器操作:
C c(b, e); // 构造c,将迭代器b和e指定的范围内的元素拷贝到c(不支持array)
a.swap(b); // 交换a和b的元素,等价于swap(a, b)
c.max_size(); // c可保存的最大元素数目
c.size(); // c中元素的数目(不支持forward list)
// 添加或删除元素
c.insert(args); // 将args中的元素拷贝进c
c.emplace(inits); // 使用inits构造c中的一个元素
c.erase(args); // 删除args指定的元素
c.clear(); // 删除c中的所有元素,返回void
迭代器
迭代器范围
它的元素范围实质是以中国左闭合区间
[begin, end)
因为它 end指向的是尾元素之后的位置
begin 和 end成员
begin和end最常见的用途就是形成一个包含容器中所有元素的迭代器范围
begin和end还有多个版本
如: cbegin 返回的是一个const类型的iterator, rbegin则返回的是反向迭代器,cebegin则返回的是const reverse迭代器
当不需要写访问时,我们应该采用const版本
容器定义和初始化
之前有记录过,这里就强调下顺序容器内自带的
只有顺序容器能使用
C seq(n); // seq包含n个元素,这些元素进行了值初始化,这个构造函数是explicit的
C seq(n, t); // seq包含n个初始值为t的元素
容器的拷贝
一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配
如
vector<string> str1{"adsad", "dasda", "sadads"};
vector<string> str2(str1);
当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了
而且新容器和原容器的元素类型也可以不同,只要能将拷贝的元素进行转换
vector<const char*> art{"ad", "dasda", "dasdasd"};
vector<string> str(art.begin(), art.end());
for(auto i:str){
cout<<i<<endl;
}
输出结果:
ad
dasda
dasdasd
这里传递了容器art的begin 和 end 迭代器进行初始化,因此str里面的元素就是art里面的元素
两个迭代器能表示一个范围,因此我们也能得到一个容器的子序列
vector<string> str(art.begin(), art.begin()+2);
列表初始化
就是用花括号括起来一系列值,进行初始化,其中容器大小与初始值数量一样
list<string> authors = {"dasda", "aaa", "dddd"};
标准库array
标准库array中大小是固定的,因此在初始化的时候还要传递大小
array<int, 42>
由于array的大小是固定的,所以构造函数行为也有点不一样。array创造出来就是非空的,没有赋值的元素都会进行默认初始化
array<int, 5> i{};
for(auto j:i){
cout<<j<<endl;
}
比如这个没有进行初始化的array i,它实际的元素值是全部被默认初始化为0
注意:我们的内置数组是不支持拷贝或对象赋值的
内置数组就是 int a[3] = xxxxx;
而我们的array类型是支持拷贝和赋值操作
使用assign
assign操作不适用于关联容器和array
只适用于顺序容器
seq.assign(b, e); // 将seq中的元素替换为迭代器b和e范围中的元素
seq.assign(il); // 将seq中的元素替换为初始化列表中il中的元素
seq.assign(n, t); // 将seq中的元素替换为n个值为t的元素
swap
当对象是array的时候,swap才是对每个元素进行拷贝,交换。
其他对象的话,swap只是交换两个容器的内部数据结构
容器大小操作
关系运算符
规则:
- 如果两个容器具有相同大小且所有元素都两两对应相等, 则这两个容器相等,否则不等
- 如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小的容器小于较大的容器
- 如果两个容器都不是另一个容器的子序列,则比较结果取决于第一个不相等的元素比较结果
容器的关系运算符使用元素的关系运算符完成比较
只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器
课后练习:比较一个list 和 vector元素
bool compare(vector<int> b, list<int> c){
auto lb = c.cbegin();
auto le = c.cend();
auto vb = b.cbegin();
auto ve = b.cend();
bool flag = true;
for(; lb!=le;lb++, vb++){
if(*lb!=*vb){
flag = false;
return flag;
}
}
return flag;
}
顺序容器操作
添加元素
- push_back 在尾部添加一个元素
- push_front 在头部添加一个元素
- c.insert(p, t) 在迭代器p指向的元素之前插入一个t,返回新添加的元素的迭代器
- c.insert(p, n, t)在迭代器p指向的元素之前插入n个值为t的元素,返回新添加的第一个元素的迭代器
- c.insert(p, b, e) 将迭代器b和e指定的范围内的元素插入到迭代器p指向的元素之前
- c.insert(p, il) il是一个花括号包起来的元素值列表,将这些值插入到迭代器p指向的元素之前,返回新添加的第一个元素的迭代器
重点:容器元素是拷贝
当我们用一个对象来初始化容器时,或将对象插入到容器中时,实际上放入到容器的时对象的拷贝,也就是对容器内元素的操作,与外面的对象是毫无关联
push_front
在头部添加元素
list<int> a{};
for(size_t ix=0; ix!= 4; ++ix){
a.push_front(ix);
}
for(auto i:a){
cout<<i<<" ";
}
输出:
3 2 1 0
特定位置插入insert
传入一个迭代器和值
vector<string> str{"ZZK"};
str.insert(str.begin(), "Hello?");
for(auto i: str){
cout<<i<<endl;
}
这个结果等价于push_front
插入范围内元素
传入n,代表元素个数
vector<string> str{"ZZK"};
str.insert(str.begin(), 3, "Hello?");
for(auto i: str){
cout<<i<<endl;
}
输出结果:
Hello?
Hello?
Hello?
ZZK
如果我们传递传递给insert一对迭代器,它们不能指向添加元素的目标容器
善用insert的返回值
list<string> lst;
auto iter = lst.begin();
string word;
while(cin>>word){
iter = lst.insert(iter, word);
}
for(auto i:lst){
cout<<i<<" ";
}
输入:boy handsome
输出结果:handsome boy
这里我们定义了一个列表,取iter为开始的迭代器,
每次输入的时候,将word插入到iter前面,并返回它的迭代器,因此每次iter都是指向开头元素的,因此等价于push_front
emplace操作
c.emplace_back(xxx, xxx, xxx)
c是某个类型的容器,emplace函数则是传入对应类型的参数。
比如c的是一个Sales_data的容器,它的构造函数需要id,isbn。我们就可以在emplace里传入
c.emplace_back(id_val, isbn_val); // 实际值替换掉id_val和isbn_val
然后它会调用对应的构造函数,构造出一个对象,然后再调用push_back,将对象添加到尾部
访问元素
- c.back() 返回尾元素的引用
- c.front() 返回首元素的引用
- c[n] 返回c中下标为n的引用
- c.at(n) 返回c中下标为n的引用
at和下标操作只适用于string, vector, deque, array
back不适用于forward_list
注意:对一个空容器调用front和back,就跟使用越界的下标一样,是很严重的错误
为了更安全访问元素,更建议使用at函数来进行访问
这样当出现越界情况后,会抛出一个out_of_range异常
删除元素
- pop_back() 删除c中尾部元素 返回void
- pop_front() 删除c中首元素 返回void
- c.erase§ 删除迭代器p所指的元素, 返回一个指向被删元素之后的元素的迭代器,若p指向尾元素,则删除后返回的是尾后迭代器
- c.erase(b, e) 删除b到e之间的元素,返回一个指向最后一个被删元素之后元素的迭代器,若e本身就是尾后迭代器,则函数也返回尾后迭代器
- c.clear() 清空所有元素
删除deque中除首尾位置之外的任何元素都会使所有迭代器,引用和指针失效。指向vector或string中删除点之后位置的迭代器,引用和指针都会失效
list<int> lst{1, 2, 3, 4, 5};
while(!lst.empty()){
lst.pop_front();
for(auto i:lst){
cout<<i<<" ";
}
cout<<endl;
}
输出结果:
2 3 4 5
3 4 5
4 5
5
注意和python中列表pop的区别,这里的pop都是不返回值的
forward_list操作
forward_list 是一个单向链表,由于单向链表没有前驱,所以它需要单独设计出来一个操作,它是通过对之后的元素进行操作
- lst.before_begin() 返回指向链表首元素之前不存在的元素的迭代器,此迭代器无法解引用
- lst.insert_after(p, t)在迭代器p后面插入一个值t
- lst.insert_after(p, n, t)在迭代器p后面插入n个值为t的元素
- lst.insert_after(p, b, e)
- lst.insert_after(p, il) il是一个花括号列表, 返回一个指向最后一个插入元素的迭代器
- emplace_after()
- erase_after§
- erase_after(b, e) 返回被删元素之后元素的迭代器
对forward_list 操作一定要注意两个迭代器, 一个指向我们要处理的元素,另一个指向其前驱
forward_list<int> flst{1, 2, 3, 4, 5, 6, 7, 8};
auto prev = flst.before_begin();
auto curr = flst.begin();
while(curr != flst.end()){
if(*curr %2){
curr = flst.erase_after(prev);
}
else{
prev = curr;
++curr;
}
}
上面的例子是删除链表中偶数的元素。如果能整除2,则删除,并将删除后的元素的迭代器赋值给curr,如果是奇数,则把当前迭代器赋值给前驱,curr往前前进
使用resize改变容器大小
当我们要缩小容器的时候,会把容器后面的元素删除
当扩大容器的时候,若没有指定值,则默认为0,当然我们也可以指定
假设ilist是一个大小为10的容器
ilist.resize(25, 5);
则扩大至25个元素,后面的元素均赋值为5
容器操作可能使迭代器失效
在向容器添加元素之后
- 如果容器是vector或string,且存储空间被重新分配,则指向容器的迭代器,指针和引用都会失效。若存储空间未重新分配,指向插入位置之前的元素的迭代器,指针和引用仍有效,之后的无效
- 对于deque,插入到除首尾位置之外的任何位置都会导致迭代器,指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的指针和引用不会失效
- 对于list和forward_list,知晓容器的迭代器,指针,引用仍有效
删除元素之后
- 对于list和forward_list 则仍有效
- 对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器,引用或指针也会失效。如果是删除deque的尾元素,则尾后迭代器也会失效,但其他迭代器,引用指针不受影响。 若删除的是=首元素==, 这些也不受影响
- 对于vector和string,指向被删元素之前元素的迭代器,引用,指针仍有效
当我们删除元素,尾喉迭代器总会失效
因此我们需要保证每次改变容器的操作之后都需要正确地重新定位迭代器
不要保存end返回的迭代器
我们需要的时候直接调用end()
如果保存的话可能会带来bug
vector增长情况
为了避免因扩张而反复造成重新分配内存空间
通常vector是直接分配一个比实际需求要大的内存空间
size是大小,capacity是实际分配的容量
c.shrink_to_fit() 将capacity 减少尾与size() 相同大小
c.capacity() 不重新分配内存空间的话,c可以保存多少元素
c.reserve(n) 分配至少能容纳n个元素的内存空间, 仅影响vector预先分配多大的内存空间
vector<int> vec{1, 2, 3, 4, 5};
vec.push_back(1);
cout<<vec.size()<<" "<<vec.capacity();
输出结果:
6 10
当我们使用了预留空间,那么是不会重新分配内存空间
string操作
- string s(cp, n) s是cp指向的数组中,前n个字符的拷贝
- string s(s2, pos2) s是string s2 从下标pos2 开始的字符的拷贝
- string s(s2, pos2, len2) s是string s2从下标pos2开始的长度为len2个字符的拷贝
substr
返回的是一个子串
s.substr(pos, n) 返回从pos开始的n个字符的字符串
修改string的操作
- s.insert(pos, args) 在pos之前插入args指定的字符, 返回指向第一个插入字符的迭代器
- s.erase(pos, len) 删除从位置pos开始的len个字符,返回指向s的引用
- s.assign() 将s中的字符替换为指定字符,返回指向s引用
- s.replace(range, args)将范围内的字符替换为args的字符,返回指向s的引用
assign总是替换字符串所有内容,append总是将新字符追加到string末尾
string搜索操作
s.find() 查找第一次出现的位置
s.rfind() 查找最后一次出现的位置
s.find_first_of(args) 查找args中任何一个字符第一次出现的位置
s.find_last_of(args) 查找args中任何一个字符最后出现的位置
s.find_first_not_of(args) 查找第一个不在args中的字符
s.find_last_not_of(args) 查找最后一个不在args的字符
args可以是以下形式
(c, pos) 从pos位置开始查找
(cp, pos, n) 从pos位置开始查找指针cp指向的前n个字符
compare比较
pos1, n1, s2 将s中从pos1开始的n1个字符与s2比较
pos1, n1, s2, pos2, n2 将s中从pos1开始的n1个字符与s2中从pos2开始的n2个字符比较