最近写代码想要实现一个功能,对于一个vector,需要将后续添加的元素进行反序操作。由于不想使用一个临时的vector,想要在原vector进行功能实现,因此就直接利用std::reverse方法进行操作。例如,想要将后续添加的4、5、6进行反序:
#include <vector>
#include <algorithm>
#include <iostream>
int main()
{
std::vector<int> a{1, 2, 3};
auto it = a.end();
a.push_back(4);
a.push_back(5);
a.push_back(6);
std::reverse(it, a.end());
for (int aaa : a) {
std::cout << aaa << " ";
}
return 0;
}
理论上说,end()迭代器不指向实际的元素,而是表示末端元素的下一个元素,这个迭代器起一个哨兵的作用,表示已经处理完所有的元素。
那么按照这个思路:end()迭代器指向的就是3的下一个元素,那么就是4。再进行reverse操作,最后的结果就应该是1 2 3 6 5 4了。实际上的输出结果为:
5 4 3 2 1 34
完完全全不是我们所设想的内容!这究竟是怎么回事呢?
问题分析
实际输出的结果,元素6没了,反而多出来了一个莫名其妙的34。当然,对于不同的编译环境下输出的结果可能是不同的,甚至可能直接会无法编译通过。
对于这种出现异常值的情况,优先想要的就是踩内存了。而对于STL容器中,最容易踩内存的就是迭代器的使用了。对于vector,当添加元素而引起存储空间被重新分配时,指向容器的指针、引用或迭代器全部失效。
如果想要试验一下,可以通过reserve操作预分配空间,让它不产生内存空间重新分配的问题。即:
std::vector<int> a{1, 2, 3};
a.reserve(100); // 预分配空间
auto it = a.end();
a.push_back(4);
a.push_back(5);
a.push_back(6);
std::reverse(it, a.end());
再看一下结果:
1 2 3 6 5 4
运行结果没有问题了。但是这种方法并不保险,首先并不能每次完全确定需要预分配多少空间,其次,在一些编译环境下,不允许这种操作(毕竟理论上迭代器失效一段时间了)。
可以等元素都添加完成(vector不会再进行导致迭代器实现的操作)之后,再进行reverse操作就可以了。即:
std::vector<int> a{1, 2, 3};
int ori_size = a.size();
a.push_back(4);
a.push_back(5);
a.push_back(6);
auto it = std::next(a.begin(), ori_size);
std::reverse(it, a.end());
这样就肯定没有问题了。
迭代器失效类别
那么什么情况下迭代器会失效呢?
向容器添加元素或者从容器中删除元素操作都可能导致使指向容器的指针、引用或迭代器失效。
一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种错误的方式,可能引起与未初始化指针一样的问题。甚至会导致无法编译通过或运行crash!
以常见的容器进行总结:
vector(序列式容器):
- 添加元素:若空间未重新分配,指向插入位置之前的元素的指针、引用或迭代器仍然有效,但指向插入位置及其之后元素的指针、引用或迭代器全部失效;若空间重新分配,所有指针、引用或迭代器全部失效
- 删除元素:指向删除位置之前的元素的指针、引用或迭代器仍然有效,但指向删除位置及其之后元素的指针、引用或迭代器全部失效
list(关联式容器):
- 添加元素:指向容器的指针、引用或迭代器依然有效
- 删除元素:指向删除位置的指针、引用或迭代器失效,指向容器其他位置的指针、引用或迭代器仍有效
deque(序列式容器):
- 添加元素:若插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用都会失效,若在首尾位置添加元素,迭代器会失效,但是指针和引用不会失效
- 删除元素:若在首尾之外的任何位置删除元素,那么指向被删除元素之外其他元素的迭代器全部失效,若在其首尾删除元素则只会使指向被删除元素的迭代器失效
接口级别的失效情况,可以参考链接:STL 容器迭代器失效总结(超级详细)。
举个删除元素的例子来进行一下对比:
#include <vector>
#include <list>
#include <deque>
#include <iostream>
int main()
{
std::vector<int> a{1, 2, 3, 4, 5, 6};
auto it_a = std::next(a.begin(), 3);
auto it_next_a = std::next(a.begin(), 4);
std::cout << "vector: " << *it_a << " " << *it_next_a << " ";
it_a = a.erase(it_a);
std::cout << *it_a << " " << *it_next_a << std::endl;
std::list<int> b{1, 2, 3, 4, 5, 6};
auto it_b = std::next(b.begin(), 3);
auto it_next_b = std::next(b.begin(), 4);
std::cout << "list: " << *it_b << " " << *it_next_b << " ";
it_b = b.erase(it_b);
std::cout << *it_b << " " << *it_next_b << std::endl;
std::deque<int> c{1, 2, 3, 4, 5, 6};
auto it_c = std::next(c.begin(), 3);
auto it_next_c = std::next(c.begin(), 4);
std::cout << "deque: " << *it_c << " " << *it_next_c << " ";
it_c = c.erase(it_c);
std::cout << *it_c << " " << *it_next_c << std::endl;
return 0;
}
编译运行结果为:
vector: 4 5 5 6
list: 4 5 5 5
deque: 4 5 5 6
可以看到,在删除操作之前,it和it_next分别为4和5。在进行删除操作之后,被删除位置的迭代器全部失效(原本应该是4,现在都变成了5,指向了下一个位置的元素)。而指向删除位置之后的迭代器it_next,对于list来说,没有影响,而对于vector和deque来说,都失效了,变成了6(下一个位置的元素)。
因此,在使用erase()函数的时候需要特别注意,该函数的返回值为被删除元素之后那个位置的迭代器。可以使用该返回值继续遍历之后的元素,但是删除操作之前定义的那些迭代器(比如被删除元素之后的位置)可能会出现问题!