C11简洁之道:循环的改善 1、  for循环的新用法 2、  使用细节

  在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;
}

  程序的执行结果:

 C11简洁之道:循环的改善
1、  for循环的新用法
2、  使用细节

  我们可以从结果中看出,不论基于范围的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位系统运行结果:

 C11简洁之道:循环的改善
1、  for循环的新用法
2、  使用细节

  从结果看出,这并不是我们需要的结果,如果把vector换成list,结果又不一样。

  因为介于范围的for循环其实是普通for循环的语法糖,同普通的循环一样,在迭代时修改容器可能引起迭代器失效,导致一些意料之外的结果。由于我们这里是没法看到迭代器的,所以在基于范围的for循环中修改容器到底会造成什么样的影响非常困难。