c++11关于for循环的新特性
std::vector<int> A(5, 2);
for (auto b : A) {
b += 1;
} // A 中的元素不变
for (auto &b : A) {
b += 2;
} // A 中的元素变为 {4, 4, 4, 4, 4}
for (const auto &b : A) {
std::cout << b << std::endl;
}
三种用途有显著的差别,
- 第一种 b b b复制了 A A A中元素的值,因此修改 b b b的值无法改变 A A A中元素的值;
- 第二种 b b b是作为 A A A中元素的别名,因此修改 b b b的值等价于修改 A A A中元素的值;
- 第三种 b b b也是作为 A A A中元素的别名,但加上了 c o n s t const const修饰符保证了 b b b的值不会被修改,因此在非必要的情况下,推荐使用第三种用法。
但是除此之外还有坑!!!
那就是应当避免在循环体中对 A A A进行插入、删除等操作,因为对容器本身的操作会改变迭代器,并且编译器不会给出提醒报错,使得这样的错误难以被发现。虽然都明白这一点,但实际使用的时候还是很容易在一堆for循环中逐渐迷失自己。。。
这里具体讨论一下可能会出现的问题和解决的办法,情况如下:
vector数组
一、删除操作
有部分文章说明,在这情况下运行会出错,如下代码仔细调试大概就能理解了。
std::vector<int> A = {1, 2, 3, 4, 5, 6};
for (auto b : A) {
A.erase(std::find(A.begin(), A.end(), b));
} // 程序报错
这是一段会运行出错的代码,运行调试可以发现,程序运行过程如下:
A | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
step1 | 2 | 3 | 4 | 5 | 6 | 6 |
step2 | 2 | 4 | 5 | 6 | 6 | 6 |
step3 | 2 | 4 | 6 | 6 | 6 | 6 |
step4 | 2 | 4 | 6 | 6 | 6 | 6 |
step5 | 2 | 4 |
黄色标记了 A A A中被删除的部分,以及对应的物理位置上的数据,可以发现:
- 前三次循环依次删除了 1 , 3 , 5 1,3,5 1,3,5三个元素,因此可以推测出,该用法 f o r for for循环实际上记录了当前迭代元素的物理位置,当前的第 0 , 1 , 2 0, 1, 2 0,1,2位置;
- 第四次循环时读取了上一步被删除的位置 3 3 3的元素 6 6 6,因此又删除了 A A A中元素 6 6 6;
- 第五次循环时读取了已被删除的位置 4 4 4的元素 6 6 6, s t d : : f i n d std::find std::find返回了数组的 e n d ( ) end() end(),但数组的 e n d ( ) end() end()是不能被删除的,所以程序报错。
这种问题并不是c++11带来的新特性,如下一段代码,在 A A A中有偶数个元素时并不会报错,但奇数个就会报错。
std::vector<int> A = {1, 2, 3, 4, 5, 6};
for (auto it = A.begin(); it != A.end(); it++) {
A.erase(it);
} // 最后 A 中的元素为 {2, 4, 6},但如果输入奇数个元素则会报错
实际上还有很多情况会出现隐藏的 bug,如下的这种代码始终维持了 A A A的容器大小不变(删除元素也不会真正释放内存空间),这种情况完全可以正常的编译运行而不被发现。
std::vector<int> A = {1, 2, 3, 4, 5, 6};
for (auto b : A) {
A.erase(std::find(A.begin(), A.end(), b));
A.push_back(b + 10);
} // 最后 A 中的元素为 {2, 4, 6, 13, 21, 35}
解决方法!
如果我想要在迭代的同时删除当前的元素,需要一次性完成,除去拷贝另一份数据之外,最好的办法就是使用自带的返回值.
如下一段代码就是删除所有奇数的示例
std::vector<int> A = {1, 2, 3, 5, 6, 9};
for (auto it = A.begin(); it != A.end(); ) {
if ((*it) % 2 == 1) {
it = A.erase(it);
} else {
it++;
}
} // 最后 A 中的元素为 {2, 6}
二、插入操作
插入操作与删除操作有显著的不同,如果进行了这种操作,一般情况下,无论是编译还是运行 bug 都很难被发现。
std::vector<int> A = {1, 2, 3, 4, 5, 6};
for (auto b : A) {
A.push_back(b + 10);
} // 最后 A 中的元素为 {1, 2, 3, 4, 5, 6, 11, 12, 13, 14, 15, 16};
上述代码很奇怪地只循环了六次,而如果使用传统的迭代器写法则会变为死循环。实际上前者只在循环开始的时候计算了一次数组的末尾位置(曾经就被这点狠狠地坑过!!!!),而后者每次循环都会重复计算末尾位置。因此如果想要容器内每个元素都被处理且包括新加入的元素,还是要老老实实地用迭代器。
std::vector<int> A = {1, 2, 3, 4, 5, 6};
for (auto it = A.begin(); it != A.end(); it++) {
A.push_back((*it) + 10); // 注意,存在隐藏 bug !!!
} // 死循环
但是
使用迭代器在数组中插入元素是高危行为,数组每次插入元素后需重新计算迭代器!
(除非已经事先占据了空间)
std::vector<int> A = {1, 2, 3, 4, 5, 6};
for (auto it = A.begin(); it != A.end(); it++) {
A.insert(it, 10);
} // 运行报错
上述代码会报错,而且调试过程显示,有时会在数组的开头加入了一系列奇怪的 0 0 0或其他数字,原因是 insert() 方法会通过将数组拷贝到新的位置来实现,使用原先迭代器指向的位置此时已毫无意义。push_back() 存在类似的问题,那就是如果存储空间不够了,STL是完全有可能将数组拷贝到另外一个更大的空间的,这种情况下,原先的迭代器将毫无意义!!!
- 如何在迭代的同时正确地插入元素,其实单纯地通过下标计算,就避免迭代器的访问,例如:
std::vector<int> A = {1, 2, 3, 4, 5, 6};
for (auto it = 0; it != A.size(); it++) {
if (cond) {
A.push_back(...);
}
}
set容器
如果将数组换成集合容器,情况又有所不同,集合的插入操作并不会使得迭代器失效,而删除操作如果删去了迭代器指向的元素,迭代器则会失效。
一、删除操作
如下神奇的代码,它不会报错,运行也不会出错!!!这和集合的红黑树存储结构有关,有的情况下恰好不会报错,但如果 A = { 1 , 2 , 3 , 4 , 5 } A=\{1,2,3,4,5\} A={1,2,3,4,5},则会报错
std::set<int> A = {1, 2, 3, 4, 5, 6};
for (auto b : A) {
A.erase(b);
} // 最后 A 中的元素为 {2, 3, 5}
std::set<int> A = {1, 2, 3, 4, 5};
for (auto b : A) {
A.erase(b);
} // 运行报错
和之前的数组一样,下面的代码可以正确的运行
std::set<int> A = {1, 2, 3, 5, 6, 9};
for (auto it = A.begin(); it != A.end(); ) {
if ((*it) % 2 == 1) {
it = A.erase(it);
} else {
it++;
}
} // 最后 A 中的元素为 {2, 6}
二、插入操作
下面的这部分代码恰恰是死循环,应该与红黑树的插入操作有关,原理暂没搞明白,但是集合插入删除并不会改变集合最大元素的位置,因此这里的范围迭代与使用迭代器的迭代并不会出现较大差异。
std::set<int> A = {1, 2, 3, 4, 5, 6};
for (auto b : A) {
A.insert(b + 10);
} // 死循环
如果插入比最大值小的元素的情况则不会出现死循环
std::set<int> A = {1, 2, 3, 4, 5, 6};
for (auto b : A) {
A.insert(b - 10);
} // 最后 A 中的元素为 {-9, -8, -7, -6, -5, -4, 1, 2, 3, 4, 5, 6};
总结
虽然上述内容都写在了相关文档里面,但面对实际程序的复杂情况,还是有很大的可能搞错,大部分情况下,运行时报错会帮助我们修复bug,但是面对插入操作的bug,常常难以被发现,总结下来就是: