1、 for循环的新用法
在C++98/03中,通过for循环对一个容器进行遍历,一般有两种方法,常规的for循环,或者使用<algorithm>中的for_each方法。
for循环遍历:
void func(void) { std::vector<int> arr; for(auto it = arr.begin(); it != arr.end(); ++it) { std::cout << *it << std::endl; } }
for_each方法:
void vFuncCall(int n) { std::cout << n << std::endl; } void func2(void) { std::vector<int> arr; std::for_each(arr.begin(), arr.end(), vFuncCall); }
for_each相比一般的for循环,只需关注容器元素的类型,但是都是基于范围的循环,必须显示的给出容器的开始(begin)和结束(end)。C++11中改善了这种遍历方式,不再需要给出容器的两端,循环会自动根据容器的范围自动的展开,在循环中屏蔽了迭代器的遍历细节,直接抽取容器中的元素进行运算。我们来看C++11中是怎么遍历容器的:
void func(void) { std::vector<int> arr; for(auto n : arr) { std::cout << n << std::endl; } }
是不是很简洁,n表示arr中的一个元素, auto会被编译器自动推导出类型,此例中被推导为vector中的int类型。当然,也可以直接写出类型:
std::vector<int> arr; for(int n : arr)
同时,这种循环中,冒号前面的变量支持隐式转换的,因此在使用时需要注意:
std::vector<int> arr; for(char c : arr) //int会被隐式转换为char
在这种循环中,我们都是只读方式来遍历容器的,如果需要改变容器中的值,我们需要加上引用,如果是只希望遍历而不是修改,我们可以使用const auto & 来定义n的类型,这样对于复制负担比较大的容器元素(std::vector<std::string>数组等)也可以无耗的进行遍历。
std::vector<int> arr; for(auto & n : arr) { std::cout << n++ << std::endl; //打印,并把元素的值+1 }
2、 使用细节
2.1 推导类型
我们来看使用范围的for循环和普通的for循环有什么区别:
std::map<std::string, int> mmsi_test = {{"1", 1}, {"2", 2}, {"3", 3}}; //一般情况的for循环 for(auto it = mmsi_test.begin(); it != mmsi_test.end(); ++it) { std::cout << it->first << "->" << it->second << std::endl; } //基于范围的for循环 for(auto val : mmsi_test) { std::cout << val.first << "->" << val.second << std::endl; }
可以看出:
- for循环中的val类型是std::pair,对于map这种关联性容器来说,需要使用val.first或者val.second来提取键值;
- auto自动推导出来容器元素的类型是value_type,而不是迭代器。
2.2 容器约束
如果我们要改变某些容器元素的值,通过auto &可以解决大多数问题,但是某些特殊容器并不能达到我们预期的结果。比如我们希望在循环中对set的值进行修改,但是set的内部元素的值是可读的----由set容器的特性决定的,因此for循环中的auto &会被推导为const xx &。同样基于范围的map遍历一样,for循环得到的std::pair引用,是不能修改first的。
void func(void) { std::set<int> ss = {1, 2, 3}; for(auto &val : ss) { //error increment of read-only refrence 'val' std::cout << val++ << std::endl; } }
2.3访问频率
我们先来看一段代码,测试一下C++11中的循环对于容器的访问频率。
#include <iostream> #include <vector> std::vector<int> g_arr = {1,2,3,4,5}; std::vector<int>& func(void) { std::cout << "get range->" << std::endl; return g_arr; } int main(void) { for(auto val : func()) { std::cout << val << std::endl; } return 0; }
程序的执行结果:
我们可以从结果中看出,不论基于范围的for循环迭代了多少次,func()只在第一次迭代之前被调用,在循环之前就确定好迭代的范围,而不是在每次迭代之前都去调用一次end()。所以可以得出结论:基于范围的for循环,冒号后面的表达式只会被执行一次。
那么我们来看如果在基于范围的for循环中修改容器会出现什么情况:
#include <iostream> #include <vector> int main(void) { std::vector<int> arr = {1,2,3,4,5}; for(auto val : arr) { std::cout << val << std::endl; arr.push_back(100); //扩大容器 } return 0; }
在centos6.7 64位系统运行结果:
从结果看出,这并不是我们需要的结果,如果把vector换成list,结果又不一样。
因为介于范围的for循环其实是普通for循环的语法糖,同普通的循环一样,在迭代时修改容器可能引起迭代器失效,导致一些意料之外的结果。由于我们这里是没法看到迭代器的,所以在基于范围的for循环中修改容器到底会造成什么样的影响非常困难。