4.5 基于范围的for循环

类别:所有人

在C++98标准中,如果要遍历一个数组,通常会需要代码清单4-33所示的代码。

代码清单4-33


include <iostream>

using namespace std;

int main(){

int arr[5]={1,2,3,4,5};

int*p;

for(p=arr;p<arr+sizeof(arr)/sizeof(arr[0]);++p){

p=2;

}

for(p=arr;p<arr+sizeof(arr)/sizeof(arr[0]);++p){

cout<<*p<<'\t';

}

}

//编译选项:g++4-5-1.cpp


代码清单4-33中,我们使用了指针p来遍历数组arr中的内容,两个循环分别完成了每个元素自乘以2和打印工作。而C++的标准模板库中,我们还可以找到形如for_each的模板函数。如果我们使用for_each来完成代码清单4-33中的工作,代码看起来会是代码清单4-34所示的样子。

代码清单4-34


include <algorithm>

include <iostream>

using namespace std;

int action1(int&e){e*=2;}

int action2(int&e){cout<<e<<'\t';}

int main(){

int arr[5]={1,2,3,4,5};

for_each(arr,arr+sizeof(arr)/sizeof(arr[0]),action1);

for_each(arr,arr+sizeof(arr)/sizeof(arr[0]),action2);

}

//编译选项:g++ -std=c++11-c


for_each使用了迭代器的概念,其迭代器就是指针。由于迭代器内含了自增操作的概念,所以如代码清单4-33中的++p操作则可以不写在for_each循环中了。不过无论是代码清单4-33还是代码清单4-34,都需要告诉循环体其界限的范围,即arr到arr+sizeof(arr)/sizeof(arr[0])之间,才能按元素执行操作。

事实上,循环的“自动范围”这个问题,在很多语言中已经实现了。我们可以看看bash中for循环的使用方法。


for i in'1 2 3 4 5';

do

$i=expr$i+$i;

echo$i;

done


上面的bash完成了与代码清单4-33及代码清单4-34一样的功能,不过语法上,bash使用了for…in的方式,因此循环的范围是“自说明”的,是在‘1 2 3 4 5’这样的范围中完成元素操作的。很多时候,对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,也是容易犯错误的。而C++11也引入了基于范围的for循环,就可以很好地解决了这个问题。

我们可以看一下基于范围的for循环改写的例子,如代码清单4-35所示。

代码清单4-35


include <iostream>

using namespace std;

int main(){

int arr[5]={1,2,3,4,5};

for(int&e:arr)

e*=2;

for(int&e:arr)

cout<<e<<'\t';

}

//编译选项:g++ -std=c++11 4-5-3.cpp


代码清单4-35就是一个基于范围的for循环的实例。for循环后的括号由冒号“:”分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示将被迭代的范围。在代码清单4-35这个具体的例子当中,表示的是在数组arr中用迭代器e进行遍历。这样一来,遍历数组和STL容器就非常容易了。

在代码清单4-35中,基于范围的for循环中迭代的变量采用了引用的形式,如果迭代变量的值在循环中不会被修改,那我们完全可以不用引用的方式来做迭代变量。比如上例中的第二个基于范围的for循环可以被改为:


for(int e:arr)

cout<<e<<'\t';


代码依然可以很好地工作。当然,如果结合之前讲过的auto类型指示符,循环会显得更简练。


for(auto e:arr)

cout<<e<<'\t';


基于范围的for循环跟普通循环是一样的,可以用continue语句来跳过循环的本次迭代,而用break语句来跳出整个循环。

值得指出的是,是否能够使用基于范围的for循环,必须依赖于一些条件。首先,就是for循环迭代的范围是可确定的。对于类来说,如果该类有begin和end函数,那么begin和end之间就是for循环迭代的范围。对于数组而言,就是数组的第一个和最后一个元素间的范围。其次,基于范围的for循环还要求迭代的对象实现++和==等操作符。对于标准库中的容器,如string、array、vector、deque、list、queue、map、set等,不会有问题,因为标准库总是保证其容器定义了相关的操作。普通的已知长度的数组也不会有问题。而用户自己写的类,则需要自行提供相关操作。

相反,如果我们数组大小不能确定的话,是不能够使用基于范围的for循环的,比如代码清单4-36所示的用法,就会导致编译时的错误。

代码清单4-36


include <iostream>

using namespace std;

int func(int a[]){

for(auto e:a)

cout<<e;

}

int main(){

int arr[]={1,2,3,4,5};

func(arr);

}

//编译选项:g++ -std=c++11 4-5-4.cpp


上述代码会报错,因为作为参数传递而来的数组a的范围不能确定,因此也就不能使用基于范围循环for循环对其进行迭代的操作。

另外一点,习惯了使用迭代器的C++程序员可能需要注意,就是基于范围的循环使用在标准库的容器中时,如果使用auto来声明迭代的对象的话,那么这个对象不会是迭代器对象。代码清单4-37所示的这个简单的例子可以说明这一情况。

代码清单4-37


include <vector>

include <iostream>

using namespace std;

int main(){

vector<int> v={1,2,3,4,5};

for(auto i=v.begin();i!=v.end();++i)

cout<<*i<<endl;//i是迭代器对象

for(auto e:v)

cout<<e<<endl;//e是解引用后的对象

}

//编译选项:g++ -std=c++11 4-5-5.cpp


读者只需要注意e和*i的区别就可以了。