基于范围的for循环

在 C++98/03 中,不同的容器和数组遍历的方式不完全相同,写法也不统一。
C++11 基于范围的 for 循环以简洁、统一的方式来遍历容器和数组,使用起来也更方便。

for 循环新语法

介绍新语法之前,看一个使用迭代器遍历容器的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> t { 1,2,3,4,5,6 };
for (auto it = t.begin(); it != t.end(); ++it)
{
cout << *it << " ";
}
cout << endl;

return 0;
}

在遍历的过程中需要给出容器的两端:开头 begin 和结尾 end,因为这种遍历方式不是基于范围设计的。在基于范围的for循环中,不需要再传递容器的两端,循环会自动以容器为范围展开,并且循环中也屏蔽了迭代器的遍历细节,直接抽取容器中的元素进行运算,使用这种方式进行循环遍历使得编码和维护变得更简单。

C++98/03 中普通的 for 循环,语法格式如下:

1
2
3
4
for (表达式 1; 表达式 2; 表达式 3)
{
// 循环体
}

C++11 基于范围的 for 循环,语法格式如下:

1
2
3
4
for (declaration : expression)
{
// 循环体
}

在上面的语法格式中 declaration 表示遍历声明,在遍历过程中,当前被遍历到的元素会被存储到声明的变量中。expression 是要遍历的对象,它可以是表达式、容器、数组、初始化列表等。

使用基于范围的 for 循环遍历容器,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> t { 1,2,3,4,5,6 };

cout << "遍历修改之前的容器: ";
for (auto &value : t)
{
cout << value++ << " ";
}
cout << endl << "遍历修改之后的容器: ";

for (auto value : t)
{
cout << value << " ";
}
cout << endl;

return 0;
}

在上面的例子中,将容器中遍历的当前元素拷贝到了声明的变量 value 中,因此无法对容器中的元素进行写操作。如果需要在遍历过程中修改元素的值,需要使用引用。

对容器的遍历过程中,如果只是读数据,不允许修改元素的值,可以使用 const 定义保存元素数据的变量,建议使用 const auto &,这样相对于 const auto 效率更高一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> t { 1,2,3,4,5,6 };
for (const auto& value : t)
{
cout << value << " ";
}

return 0;
}

使用细节

关系型容器

使用基于范围的 for 循环有一些需要注意的细节,先看一下关系型容器 map 的遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <string>
#include <map>
using namespace std;

int main()
{
map<int, string> m {
{1, "lucy"},{2, "lily"},{3, "tom"}
};

// 基于范围的 for 循环方式
// auto 自动推导出的类型是容器中的 value_type,相当于一个对组(std::pair)对象
for (auto& it : m)
{
cout << "id: " << it.first << ", name: " << it.second << endl;
}

// 普通的 for 循环方式
// auto 自动推导出的是一个迭代器类型,需要使用迭代器的方式取出元素中的键值对(和指针的操作方法相同)
for (auto it = m.begin(); it != m.end(); ++it)
{
cout << "id: " << it->first << ", name: " << it->second << endl;
}

return 0;
}

元素只读

基于范围的 for 循环内部声明一个变量的引用就可以修改遍历的表达式中的元素的值,但是这并不适用于所有的情况。

对应 set 容器来说,内部元素都是只读的,这是由容器的特性决定的,因此在 for 循环中 auto & 会被视为 const auto &。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <set>
using namespace std;

int main()
{
set<int> st { 1,2,3,4,5,6 };
for (auto &item : st)
{
cout << item++ << endl; // error,不能给常量赋值
}

return 0;
}

在遍历关联型容器时也会出现同样的问题。基于范围的 for 循环中,虽然可以得到一个 std::pair 引用,但是不能修改里边的 first 值(key 值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <string>
#include <map>
using namespace std;

int main()
{
map<int, string> m {
{1, "lucy"}, {2, "lily"}, {3, "tom"}
};

for (auto& item : m)
{
// item.first 是一个常量
cout << "id: " << item.first++ << ", name: " << item.second << endl; // error
}

return 0;
}

访问次数

基于范围的 for 循环遍历的对象可以是一个表达式/容器/数组等。假设对一个容器进行遍历,在遍历过程中 for 循环对这个容器的访问频率是一次还是多次呢?通过示例代码验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <vector>
using namespace std;

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

vector<int>& getRange()
{
cout << "get vector range..." << endl;
return v;
}

int main()
{
for (auto val : getRange())
{
cout << val << " ";
}
cout << endl;

return 0;
}

示例代码可知,不论基于范围的 for 循环迭代了多少次,函数 getRange() 只在第一次迭代之前被调用,得到这个容器对象之后就不会再去重新获取这个对象。对基于范围的 for 循环来说,冒号后边的表达式只会被执行一次。在得到遍历对象之后会先确定好迭代的范围,基于这个范围直接进行遍历。如果是普通的 for 循环,在每次迭代的时候都需要判断是否已经到结束边界。

参考资料

https://subingwen.cn/cpp/for/


基于范围的for循环
https://lcf163.github.io/2021/09/17/基于范围的for循环/
作者
乘风的小站
发布于
2021年9月17日
许可协议