顺序容器
分段写法
顺序容器的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。
标准库定义了三种顺序容器vector list deque
,还提供了三种容器适配器,适配器是根据原始的容器类型所提供的操作。
顺序容器 | 功能 |
---|---|
vector | 支持快速随机访问 |
list | 支持快速输入、删除 |
deque | 双端队列 |
顺序容器适配器 | 功能 |
---|---|
stack | 栈 |
queue | 队列 |
priority | 优先级管理的队列 |
顺序容器的初始化
以下伪代码的注释
C 容器类型名
c、 c1、c2… 容器名
b(begin) 、e(end) 迭代器b和e
n 整型
C<T> c; //创建一个名为c的空容器
C c(c2); //创建容器c2的副本c,c和c2必须要有同样的容器类型
C c(b,e); //创建c,其元素是迭代器b和e所表示的范围内元素的副本。
C c(n,t); //用n个值为t的元素创建容器c,其中值t必须是容器类型c的元素类型的值 只适用顺序容器
C c(n); //创建由n个值初始化元素的容器c 只适用于顺序容器
1、将一个容器初始化为另一容器的副本
要求:容器类型和元素类型相同
vector<int> ivec;
vector<int> ivec2(ivec); //ok
list<int> ilist(ivec); //error 容器类型不同
vector<double> dvec(ivec) //error 元素类型不同
2、初始化为一段元素的副本
要求: 不要求容器类型相同,元素类型也可以不同,只要能够相互兼容,能够将要复制的元素转换为所构架的容器元素的元素类型,就可以实现复制。
// svec的容器类型为 vector<string>
list<string> slist(svec.begin(), svec.end());
vector<string>::iterator mid = svec.begin() + svec.size()/2;
deque<string> front(svec.begin(), mid);
deque<string> back(mid, svec.end());
3、分配和初始化指定数目的元素
创建顺序容器时,可显式地指定容器大小和一个(可选的)元素初始化式。
容器大小可以是常量或者非常量表达式。元素初始化必须是可用于初始化其元素类型的对象的值。
const list<int>::size_type list_size = 64;
list<string> slist(list_size, "eh?"); //64个strings, 每个都是"eh?"
4、 容器内元素的约束
要做容器元素类型,要满足下面的要求
- 元素类型必须支持赋值运算
- 元素类型的对象必须可以复制
大多数类型满足上述的元素类型要求。除了引用类型外,因为引用类型不支持赋值运算。
还有就是IO库类型,IO库类型不支持复制或赋值。因此不能存放IO对象的容器。
5、 容器的容器
现在判断一下下面哪个语句有问题。
vector< vector<string> > lines; //acc
vector<vector<string>> lines; //error
必须要记住要隔开> >,如果连在一起会被以为是右移操作符。
顺序容器支持的操作
begin 和 end成员
符号 | 功能 |
---|---|
c.begin() | 返回一个迭代器,它指向容器c的第一个元素 |
c.end() | 返回一个迭代器,指向容器c的最后一个元素的下一个位置 |
c.rbegin() | 返回一个逆序迭代器,指向容器c的最后一个元素 |
c.rend() | 返回一个逆序迭代器,它指向容器c的第一个元素前面的位置 |
添加元素
- push_back()
操作 | 功能 |
---|---|
c.push_back(t) | 在容器c的尾部添加值为t的元素。返回void类型 |
c.push_front(t) | 在容器c的前端添加值为t的元素。返回void类型 只适用于list和deque容器类型 |
c.insert(p,t) | 在迭代器p所指向的元素前面插入值为t的新元素。返回指向新添加元素的迭代器 |
c.insert(p,n,t) | 在迭代器p所指向的元素前面插入n个值为t的新元素。返回void类型 |
c.insert(p,b,e) | 在迭代器p所指向的元素前面插入值为由迭代器b和e标记的范围内的元素。返回void 类型 |
关键概念:容器元素都是副本
在容器内添加元素时,系统时将元素值复制到容器里。类似,使用一段元素初始化新容器时,新容器存放的都是原始元素的副本。
被复制的原始值和容器中的元素互不相关。
新C++11标准 emplace
当我们使用push或insert等成员函数时,我们将元素类型的对象传给它们,这些对象被拷贝到了容器中。而当我们使用了emplace成员函数时,则是将参数传递给了元素类型的构造函数。emplace成员使用这些参数在容器管理的空间中直接构造元素。
//现在有一个类
class Person {
private:
string name;
int id;
public:
Person(string name, int id) {
this->name = name;
this->id = id;
}
};
//然后看康emplace和push_back的区别
int main() {
vector<Person> pvec;
pvec.push_back(Person("me", 10));
pvec.emplace_back("mea", 10);
pvec.push_back("me", 10) //error
}
push_back是可以直接传入一个对象,Person("me",10)
,而使用emplace是可以直接传入其参数,然后交给其类型的构造函数去构建一个对象放入vector<Person>
insert操作 由于我们直到insert(p,t)返回的时新添加元素的迭代器
通过这个我们便可以实现重复插入元素
list<string> lst
list<string>::iterator iter = lst.begin();
while(cin >> word){
iter = lst.insert(iter.word);
}
关系操作符
所有容器类型都支持用关系操作符来实现两个容器的比较。比较的容器必须具有相同的容器类型,其元素类型也必须相同
- 若两个容器具有相同的长度且所有元素都相等,则两个容器相等
- 长度不同,但较短的容器中所有元素都等于较长容器中对应的元素,则称较短的容器小于另一个容器。
- 如果两个容器都不是对方的初始子序列,则它们的比较结果取决于所比较的第一个不相等的元素。
访问元素的操作
at和下标操作只适用于string,vector,deque和array
操作 | 功能 |
---|---|
c.back() | 返回c中尾元素的引用。 |
c.back() | 返回c中首元素的引用。 |
c.back() | 返回c中下标为n的元素引用。n是一个无符号整数,n>=c.size(),则函数行为未定义 |
c.back() | 返回c中下标为n的元素的引用。下标越界,抛出out_of_range异常 |
返回的是引用
if(!c.empty()){
c.front() = 43;
auto &v = c.back(); //获得最后一个元素的引用
v = 1024;
auto v2 = c.back(); //不是引用,是c.back()的拷贝
v2 = 0;
容器大小的操作
顺序容器大小的操作 | 功能 |
---|---|
c.size() | 返回容器c的大小 |
c.max_size() | 返回容器最多能够的存储的元素数目 |
c.empty() | 返回容器大小是否为0的bool值 |
c.resize(n) | 如果c.size() < n,那么采用值初始化的新元素,否则,删除掉多出来的元素(后部的元素) |
c.resize(n, t) | 调整容器的大小,多出来的元素用t来初始化 |
注意:resize可能会使迭代器失效。在vector或deque容器上做resize操作由可能使所有的迭代器失效
如果resize操作进行了压缩,那么被删除的元素的迭代器失效。
删除元素
删除顺序容器内元素的操作 | 功能 |
---|---|
c.erase( p ) | 删除迭代器p所指向的元素,返回指向被删除元素的下一个元素 |
c.erase(b,e) | 删除迭代器b和e所标记的范围内的所有元素,返回一个被删除段后面的元素。 |
c.clear() | 删除c内所有的元素。返回void |
c.pop_back() | 删除容器c内的最后一个元素,返回void |
c.pop_front() | 删除c内的第一个元素,返回void。只适用于list和deque |
迭代器失效
添加/删除元素可能会使迭代器失效
insert 或 push操作都可能导致迭代器 失效。
- 对于vector和string,添加/删除元素位置之后的迭代器失效。
- 对于deque,插入到除了首尾位置之后元素的迭代器、指针、引用都会失效。
- 对于list、forward_list不会失效,因为加入了新元素之后,迭代器会进行更新。
当编写循环将元素插入到vector或者deque容器时,程序必须保证迭代器在每次循环后都得到更新。
重定位迭代器
在进行了添加或者删除之后,程序员需要自己进行迭代器的更新。
// 要求:若是奇数元素,则复制,否则,删除偶数元素
vector<int> vi = {1,2,3,4,5,6,7,8,9};
auto iter = vi.begin();
while(iter != vi.end()){
if(*iter % 2 ==1){
iter = vi.insert(iter, *iter);
iter += 2;
}
else{
iter = vi.erase(iter);//iter指向我们删除之后的元素
}
}
在这个代码里面,添加了元素之后,end()肯定会变化,而last = v.end()
,last这个迭代器却没有变化,导致了存储在last中的迭代器失效。last不再指向的v的最后一个元素了。
不要保存end的迭代器
vector<int> v;
auto begin = v.begin(), end = v.end();
while (begin != end) {
++begin;
begin = v.insert(begin, 42);
++begin;
}
当加了insert之后,end这个迭代器就失效了,它不是指向v中的任何元素,而是指向了v中尾元素之后的位置。因此这个代码会陷入死循环。
所以正确做法是
while(begin != v.end()){
++begin;
begin = v.insert(begin, 42);
++begin;
}
顺序容器的赋值和swap
现在有两个容器c1和c2,想要把c2的全部元素赋给c1
c1.erase(c1.begin(), c1.end());
c1.insert(c1.begin(), c2.begin(), c2.end());
顺序容器的赋值操作 | 功能 |
---|---|
c1=c2 | 删除c1的所有元素,将c2的元素复制给c1。c1和c2的类型必须相同 |
c={a,b,c…} | 将c1中的元素替换为初始化列表中元素的拷贝(array不适用) |
swap(c1,c2) | 交换c1和c2中的元素。c1和c2必须具有相同的类型。swap通常比从c2向c1拷贝元素快得多 |
c1.swap(c2) | 交换内容:调用完该函数后,c1中存放的是c2原来的元素,c2中存放的则是c1原来的元素。c1和c2的类型必须相同。该函数的执行速度通常要比将c2的元素复制到c1的操作快 |
assign操作 | 不适用关联容器和array |
c.assign(b,e) | 重新设置c的元素:将迭代器b和e标记的范围内所有的元素复制到c中。b和e必须不是指向c中元素的迭代器 |
c.assign(il) | 将容器c中的元素替换为初始化列表il中的元素 |
c.assign(n,t) | 将容器c重新设置为存储n个值为t的元素 |
使用assign
如果容器类型相同,元素类型相同,就可以直接使用赋值操作符(=)将一个容器赋给另一个容器。
如果在不同(相同)类型的容器内,元素类型不相同但是相互兼容,则赋值运算符必须使用assign函数。如:通过assign操作实现将vector容器中一段char*类型的元素赋给string类型的list容器。
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; //错误,类型不匹配
names = oldstyle.assign(oldstyle.cbegin(), oldstyle.cend());
使用swap
swap交换两个相同类型容器的内容。调用swap之后,两个容器的元素会交换
vector<string> svec1(10);
vector<string> svec2(24);
swap(svec1, svec2);
调用swap之后,svec1会右24个元素,svec1有10个元素。swap没有交换元素本身,而是交换了两个数据结构,所以操作会很快。
- 并且swap不对任何元素进行拷贝、删除或插入操作,除了array数组外。因此能在常数级时间内完成。
- 迭代器不会失效, 假设现在有一个iter指向了svec1[3],在交换之后iter指向了svec2[3],变换了容器而已。
- 虽然说可以调用成员函数的swap, s1.swap(s2),但是在泛型编程中,经常调用的是非成员函数的swap。