1. 概要说明
基于范围的for循环(Range-based for loop)是C++11引入的一种新的循环结构,基于范围的for循环可以遍历支持迭代器的集合,如std::vector、std::list等,也可以遍历支持下标操作的集合,如std::array和普通数组。掌握基于范围的for循环,它可以方便地操作集合中的元素,而无需手动管理索引或迭代器,将有助于你编写出更高效、更优雅的C++代码。
2 如何使用基于范围的for循环
2.1 语法格式
基于范围的for循环的语法形式如下:
for (for-range-declaration : for-range-initializer) {
// 循环体
}
在这里,for-range-declaration是一个迭代变量,用于表示每一次迭代中从for-range-initializer中获取的元素的值或引用,for-range-initializer是一个表示容器或数组的表达式。迭代变量可以直接声明成for-range-initializer中元素的类型,或者声明成auto类型,使代码更加简洁。
2.2 迭代变量
2.2.1 基础类型
一般情况下,范围for循环内的迭代变量的类型应该声明成被遍历的数据、容器集合中的数据元素的类型。如下:
int arr[] = {1, 2, 3, 4, 5};
for (int i : arr) {
std::cout << i << " ";
}
std::vector<float> vec = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f};
for (float i : vec) {
std::cout << i << " ";
}
2.2.2 自动类型推断
范围fo循环可以和C++11的别一个特性:自动类型推断(auto)一起使用,使代码更加简洁。一般情况下,迭代变量可声明成auto、 auto &(左值引用类型)、auto &&(右值引用类型)、const auto &(常量引用类型)。
2.2.2.1 auto 类型
使用auto类型的迭代变量进行遍历集合时,实际上迭代变量只是集合中数据元素的拷贝。在这种情况下,循环体中不论怎么修改迭代变量的数据,都不会影响到原有集合中的数据元素。示例如下:
vector<int> v{1,2,3,4,5};
for(auto i : v) {
i++;
cout << i << " "; // 输出2 3 4 5 6
}
for(vector<int>::iterator it = v.begin(); it != v.end(); it++){
cout << *it << " "; // 输出1 2 3 4 5
}
2.2.2.2 auto &类型
使用auto &类型的迭代变量进行遍历时,迭代变量是一个非常量左值引用,它是集合中数据元素的引用,如果对迭代变量进行修改,会直接影响到集合数据元素本身。示例如下:
vector<int> v{1,2,3,4,5};
for(auto& i : v) {
i++;
cout << i << " "; // 输出2 3 4 5 6
}
for(vector<int>::iterator it = v.begin(); it != v.end(); it++){
cout << *it << " "; // 输出2 3 4 5 6
}
2.2.2.3 auto &&类型
若for-range-initializer集合返回一个临时对象(将死值),由于临时对象不能绑定在非常量左值引用,因此使用auto &声明迭代变量时会发生编译错误。这个时就需要使用 auto && 非常量右值引用声明迭代变量。示例如下:
vector<bool> vec {true, false, true, false};
// for (auto &a : vec) { // 编译报错
for (auto &&a : vec) { // 编译通过
cout << a << " ";
}
输出:
1 0 1 0
这个时候,我们会提出疑问为什么vetor<bool> 类型的集合,只能使用auto &&类型的迭代变量访问而不能使用auto &类型的迭代变量访问呢?这是因为vector<bool>集合中的元素vector<bool>::reference类型,它不是一个左值,这也是使用auto &时会编译出错的原因。
vector<bool> vec {true, false, true, false};
bool &v = vec[0]; // 编译报错, vec[0]不是一个左值
bool *w = &vec; // 编译报错,无法将一个临时变量地址绑定给指针
auto &&u = vec[0]; // 编译通过,非常量右值引用
关于右值引用的详细说明,请参考【C++ 11】(八)右值引用
2.2.2.4 const auto&类型
在auto &的前面加上了const修饰,意味着迭代变量是集合中数据元素的引用,但是它是只读的不能被修改。示例如下:
vector<int> v{1,2,3,4,5};
for(const auto& i : v) {
i++; // 编译出错
cout << i << " ";
}
3.使用注意
2.1 循环中修改集合中元素个数
范围for循环内进行遍历操作时,如果修改了集合中元素的个数,那么迭代器将会无效。
如下例子中,在集合追加一个元素后,迭代器会无效,输出结果自然就会是错误的。
#include <vector>
#include <iostream>
int main(void) {
std::vector<int> v{ 5, 5, 0, 5, 1 };
for(auto&& i : v) {
std::cout << ' ' << i;
if (5 == i) {
v.emplace_back(123); // 追加元素后、迭代器无效
}
}
}
结果:
5 5 -1978514432 -946396368 1
2.2 迭代器的生命周期结束无效
迭代器(对象)的生命周期结束后,此时遍历这个对象,也会输出不正确结果甚至发生崩溃现象。
例子如下:
#include <initializer_list>
#include <iostream>
#include <vector>
struct something
{
std::vector<int> v;
something(const std::initializer_list<int>& l ) : v(l) {}
std::vector<int>& get_vector() { return v; }
~something() noexcept { std::cout << "destructor" << std::endl; }
};
int main()
{
for( auto e : something { 1,2,3,4,5,6,7,8,9,0 }.get_vector() )
{
std::cout << e; // something是临时对象、此时它的的生命周期已结束
}
std::cout << std::endl;
}
4. 结束语
范围for循环是C++11引入的一个强大的新特性,它大大简化了遍历数组和容器的代码,使代码更加简洁,更具可读性。掌握范围for循环,将有助于你编写出更高效、更优雅的C++代码。