9.3顺序容器操作

顺序容器操作

上一节主要讲了所有容器都支持的操作:主要是初始化、赋值、迭代器和swap。本节将介绍顺序容器特有的操作。

1.向顺序容器添加元素

除array外,向顺序容器添加元素的操作如下表:

这些操作会改变容器的大小;array不支持这些操作。
forward_list有自己版本的insert和emplace,且不支持push_back和emplace_back
vector和string不支持push_front和emplace_front

c.push_back(t)           在c的尾部创建一个值为t或由args创建的元素。返回void
c.emplace_back(args)     

c.push_front(t)          在c的头部创建一个置为t或args创建的元素。返回void
c.emplace_front(t)     

c.insert(p,t)            在迭代器p指向的元素之前创建一个值为t或由args创建的元素。返回指向新添加的元素的迭代器。
c.emplace(p,args)          

c.insert(p,n,t)          在迭代器p指向的元素之前插入n个值为t的元素。返回指向新添加的第一个元素的迭代器;若n为0,                          c.则返回p
c.insert(p,b,e)          在迭代器b和e指定的范围内的元素插入到迭代器p指向的元素之前。b和e不能指向c中的元素。返回指                          向新添加的第一个元素的迭代器;若范围为空,则返回p
c.insert(p,i1)           i1是一个花括号包围的元素值列表。将这些给定值插入到迭代器p指向的元素之前。返回指向新添加的                          第一个元素的迭代器;若列表为空,则返回p

向一个vector、string或deque插入元素会使所有只想容器的迭代器、引用和指针失效

不同的容器要使用不同的策略来分配元素空间,这直接关乎性能。如,在vector或string的尾部之外的位置,或deque首尾之外的任何位置添加元素,都需要移动大量元素,时间复杂度为O(n)。而且,同一个vector或string添加元素可能引起整个对象存储空间重新分配,需要分配一个新的内存,并将元素从旧的空间移动到新的空间中。

注意:当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,相当于值传递。实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。随后,对容器中的元素的任何改变都不会影响到原始对象,反之亦然。

①使用push_back

除了array和forward_list之外,所有顺序容器都支持push_back(尾插) 操作,将一个元素追加到容器的尾部。

如:每次读取一个string到word中,然后追加到容器的尾部:

vector<string> v;
string word;
while (cin >> word) {
    v.push_back(word);
}

②push_front

list、forward_list和deque支持push_front(头插) 操作将元素插入到容器头部。

③在容器中的特定位置添加元素

insert成员允许我们在容器的任意位置插入0个或多个元素。

每个inset函数的第一个参数都是一个迭代器,指定插入的位置,在这个迭代器之前的位置插入元素。

如:将"hello"插入到iter之前的位置

vector<string> v{ "abc","def","ghi" };
auto iter = v.begin() + 1;
auto it1 = v.insert(iter, "hello"); //insert返回插入的元素的迭代器
cout << *it1 << endl; //结果是"hello"
auto it2 = v.insert(it1, 2, "kkk"); //返回指向插入的第一个元素的迭代器
cout << *(++it2) << endl; //结果是"kkk",插入的第一个元素下一个元素也是kkk

④插入范围内元素

接受一对迭代器或一个初始化列表的insert版本将给定范围的元素插入到指定位置之前:

如:在容器v1最后两个元素插入到容器b的开始位置

vector<string> v1 = {"abc", "def", "ghi"};
vector<string> v2;
v2.insert(v2.begin(), v1.end() - 2, v1.end()); //v2有def、ghi
v2.insert(v2.begin(), {"these", "is"}); //v2此时为these、is、def、ghi

⑤使用insert的返回值

insert不管哪个版本,都返回指向插入第一个元素的迭代器的位置(如果只插入一个元素,则返回插入的那个元素),根据这个特点,可以在容器中的一个特定位置反复插入元素。

如:在容器的第2个元素之前,反复插入输入的内容

list<string> lst{"abc", "def", "ghi"};
auto iter = lst.begin() + 1;
string word;
while (cin >> word) {
    iter = list.insert(iter, word); //在iter位置不断插入word,并更新迭代器
}

由于每次插入元素都会使迭代器失效,因此每次插入都需要更新迭代器,正好利用insert特性返回插入的第一个元素。

⑥使用emplace操作

当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造函数。即容器某个元素对象进行有参构造和拷贝构造的区别。

如:

struct test {
    test(int a, int b):_a(a), _b(b){}
    int _a;
    int _b;
}
vector<test> v;
v.emplace_back(10, 20);
v.push_back(10, 20); //错误:没有两个参数版本的push_back函数

2.访问元素

在顺序容器中访问元素的操作如下表:

at和下标操作只适用于string、vector、deque和array。
back不适用于forward_list

c.back();           返回c中尾元素的引用。若c为空,函数行为未定义
c.front();          返回c中首元素的引用。若c为空,函数行为未定义。
c[n];               返回c中下表为n的元素的引用,n是一个无符号整型。若                             n>=c.size(),则函数行为未定义,即n范围为[0,c.size())
c.at(n);            返回下标为n的元素的引用。如果下标越界,则错误

对一个空容器调用front和back,就像使用一个越界的下标一样。

①下标操作和安全访问操作

下标运用必须有两个前提

  • 容器不为空,若容器为空,front(),back()和下标操作都会越界,导致函数行为未定义。
  • 下标不能超出范围,范围为[0,c.size())。

==在运用下标运算符,或者访问容器首尾元素时必须确保满足上述两个前提!!!!==否则,虽然能通过编译,但是运行时会出错。

如:

vector<int> v;
v.front(); //错误:容器为空
v.push_back(10);
v[2]; //错误:下标必须属于区间[0,v.size()),即[0,1)

②访问成员函数返回的是引用

在容器中访问元素的成员函数(即front、back、下标和at)返回的都是引用。 如果容器是一个const对象,则返回的是常量引用。如果容器不是const的,则返回的是普通引用,我们可以用来改变元素的值。

如:

vector<int> v1{1, 2, 3};
const vector<int> v2{1, 2 ,3};
v1[1] = 3; //正确
v2[1] = 3; //错误:v2返回的是常量引用,不能改变引用的值

3.删除元素

非array容器有很多种删除元素方式,如下表:

这些操作会改变容器的大小,所以不适用于array(大小是固定的)
forward_list有特殊版本的erase
forward_list不支持pop_back;vector和string不支持pop_front
c.pop_back()      删除c中尾元素。若c为空,则函数行为未定义。函数返回void
c.pop_front()     删除c中首元素。若c为空,则函数行为未定义。函数返回void
c.erase(p)        删除迭代器p所指定的元素,返回一个指向被删元素之后元素的迭代               器,若p指向尾元素,则返回尾后迭代器。若p是尾后迭代器,则行为未定义
c.erase(b,e)      删除迭代器b和e所指定范围内的元素。返回一个指向最后一个被删元               素之后元素的迭代器,若e本身就是尾后迭代器,则函数也返回尾后迭代器
c.clear()         删除c中所有的元素

删除deque中除了首尾之外的任何元素都会使所有迭代器、引用和指针失效。指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。

注:删除元素的成员函数并不检查其参数,在删除元素之前,必须确保它们是存在的。即删除一个元素的前提是能访问它,也就是满足容器不为空,且迭代器不能越界这两个前提条件。

①pop_front和pop_back成员函数

forward_list不支持pop_back;vector和string不支持pop_front,这两个函数返回void,如果需要弹出元素的值,就必须在弹出操作之前保存它

vector<int> v{1, 2, 3};
int save = v.back();
v.pop_back();
cout << save << " " << v.back() << endl;
//结果为3 2

②从容器内部删除一个元素

erase函数可以删除一个由迭代器指定的单个元素,也可以删除由一堆迭代器指定的范围(左闭合区间)内的所有元素。这两种形式的erase都返回指向删除的(最后一个)元素之后的迭代器。

如:

vector<int> v{ 1, 2, 3, 4, 5 };
auto it1 = v.erase(v.begin()); //it1返回删除元素后一个元素
cout << *it1 << endl; //结果为2
auto it2 = v.erase(v.begin(), v.end() - 1); //删除之后it1就失效了
cout  << *it2 << endl; 

4.改变容器大小

顺序容器大小操作如下表:

resize不适合于array
c.resize(n)       调整c的大小为n个元素。若n<c.size(),则多出的元素被丢弃。若必                   须添加新元素,对新元素进行值初始化。
c.resize(n,t)     调整c的大小为n个元素。任何新添加的元素都初始化为值t

如果resize缩小容器,则指向被删元素的迭代器、引用和指针都会失效;对vector、string或deque进行resize可能导致迭代器、指针和引用失效

除了arraty之外,我们可以用resize来增大或缩小容器。如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部。

list<int> l1(10, 42);    //10个int:每个值都是42
l1.resize(15);           //将5个值为0的元素添加到l1末尾
l1.resize(25, -1);       //将10个值为-1的元素添加到l1末尾
l1.resize(5);            //从l1末尾删除20个元素

5.容器操作可能导致迭代器失效

向容器添加元素和删除元素操作可能使指向容器的指针、引用或迭代器失效。使用失效的指针、引用或迭代器是一种严重的错误

向容器添加元素后:

  • 如果容器是vector或string,且存储空间被重新分配,则指向容器的迭代器、指针和引用将全部失效;如果未重新分配,指向插入位置之前的迭代器、指针和引用仍然有效,但指向插入位置之后的迭代器、指针和引用将会失效。如:
vector<int> v{1, 2 ,3, 4, 5};
auto it1 = v.begin();
auto it2 = it1 + 2;
v.insert(v.begin() + 2, 10); //此时v为{1, 2, 10, 3, 4, 5}
cout << *it1 << " " << *it2 << endl;
//由于在初始化后的vector后添加元素会导致空间被重新分配,因此it1,it2都会失效

it1 = v.begin();
it2 = it1 + 2;
v.insert(v.begin() + 2, 10); //此时v为{1, 2, 2, 10, 3, 4, 5}
cout << *it1 << " " << *it2 << endl;
//错误:空间未被重新分配,it1未失效,it2失效
  • 对于deque,插入到除首尾之外的任何位置都会导致迭代器、指针和引用会失效。如果在首尾位置添加位置,迭代器会失效,但指向存在的元素的引用和指针不会失效。
  • 对于list和forward_list,指向容器的迭代器、指针和引用仍有效。如:
list<int> s{1, 2, 3, 4, 5};
auto it1 = s.begin(); //list容器迭代器只支持前后递增操作
auto it2 = ++(++s.begin());
s.insert(it2, 10); //此时s为{1, 2, 10, 3, 4, 5};
cout << *it1 << " " << *it2 << endl;
//正确:it1和it2都未失效

当我们从一个容器删除元素后,指向容器的迭代器、指针和引用会失效

当我们删除一个元素后:

  • 对于list和forward_list,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效
  • 对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素之外其他元素的迭代器、引用或指针也会失效。如果是删除deque的首尾元素,则首尾后迭代器也会失效,但其他迭代器、引用和指针不受影响
  • 对于vector和string,指向被删元素之前元素的迭代器、引用和指针仍有效。我们删除元素时,尾后迭代器总会失效
vector<int> v{1, 2, 3, 4, 5};
auto it1 = v.begin();
auto it2 = v.end() - 2; 
int &r = v[5];
v.erase(it2);
cout << *it1 << " " << *it2 << " " << r << endl; 
//错误:it2迭代器失效,it1迭代器有效,r是删除位置之后的元素的引用,也将失效。

总结:由于迭代器添加和删除元素的代码可能使得迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器,对vector、string和deque尤其重要!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值