1 for each循环
1.1 只读形式
for each循环语句是在Java和Python语言中一种循环语句。C++11标准也提供了类似的语句,称为范围for(range for)语句。为了方便叙述,在后文中这个语句统一称为for each循环语句。
假设现在我们要遍历数组a
中的所有元素并输出。在C++11之前,我们一般会使用下面的for循环语句:
1 | int a[10]; |
使用这样的for循环需要提前知道数组a
的大小。在C++11标准下,我们可以使用for each循环语句,把上面的代码改写成:
1 | for (int val : a) { |
for each语句遍历给定序列中的每个元素,并对序列中的每个值执行某种操作。语法形式是:
1 | for (declaration : expression) { |
其中declaration
部分定义了一个变量,用于访问序列中的基础元素。由于我们访问的对象是数组中的元素,而数组元素的类型显然是已知的,因此在声明时可以使用auto
类型符,让编译器来决定变量c的类型。这样,上面的遍历输出可以改写成:
1 | for (auto val : a) { |
需要注意的是,遍历时我们声明了一个变量来访问数组中的每一个元素。也就是说,每次迭代时,数组中的当前元素会被拷贝给该变量。因此,这种遍历方式只能对数组元素进行只读操作,无法修改原数组中元素的值。
1.2 遍历并修改——使用引用
如果我们想使用for each语句逐一修改数组中的值,应该怎样操作呢?我们使用引用来对数组进行遍历即可。例如,下面的例子将数组a
中的每一个值修改为原来的两倍:
1 | for (auto &val : a) { |
当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上,因此可以使用该引用修改其绑定的字符。
如果想使用引用进行只读操作的遍历,我们将引用声明为const
属性即可,例如:
1 | for (const auto &val : a) { |
2 一维数组
2.1 数组与指针
C语言中的一维数组相信大家已经比较熟悉了。在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针。比如下面的例子中,指针p1
和p2
指向的地址是相同的。
1 | int nums[10]; |
2.2 指针数组
数组中元素的类型当然可以是指针。指针数组表示的是元素类型为指针的数组。例如,下面给出一个元素类型为int*
,总共有10个元素的数组:
1 | int *ptrs[10]; |
2.3 指向数组的指针
需要特别注意的是,尽管大部分情况下我们都可以把数组指针看做一个指向首元素的const
属性指针,但其实是存在着指向数组的指针这种指针类型的。例如:
1 | int nums[10]; // nums是大小为10的int型数组 |
pArray
的类型可能不太好理解。这里我们通过从内到外,从右到左的方式来考虑:首先看圆括号内,*pArray
代表这是一个指针;之后看括号外,右边的[10]
表示指向的是一个大小为10的数组,而左边的int
表示数组中元素的类型是整型。
如果尝试将一个int*
型的指针赋值给pArray
,那么编译器将会报错:
1 | int *p; |
以此类推,我们可以得知数组的引用的定义语法:
1 | int (&refArray)[10] = nums; // refArray是数组nums的引用 |
类型修饰符的数量并没有特殊的限制,因此我们可能会遇到比较复杂的数组声明。例如:
1 | int *(&array)[10] = ptrs; |
这里的array
变量是一个引用,引用的对象ptrs
是一个大小为10、元素类型为int*
的数组。
3 多维数组
对于刚开始学习C语言的同学们来说,多维数组的指针类型是非常不容易理解的。C语言中的多维数组实际上是数组的数组,即:该数组的每个元素都是一个数组。
首先,我们考虑下面这个二维数组:
1 | int arr[3][4]; |
根据上文提到的从内到外,从右到左的方式,我们可以把这个声明看成下面的形式:
1 | int (arr[3])[4]; // 这个编译是可以通过的! |
先看括号内,变量arr
是一个大小为3的数组;再看括号外,该数组的每个元素都是一个大小为4的整型数组。
理解了这一点,我们就能知道如何利用指针来遍历一个二维数组了:
1 | for (int (*i)[4] = arr; i < arr + 3; i++) { |
首先,在外层循环中,遍历的每一个元素都是一个一维数组。因此变量i
类型应该为int (*)[4]
,即指向数组的指针。可以看到,我们直接将二维数组的名字arr
赋给了变量i
,这和我们之前在一维数组中提到的内容是吻合的:编译器会自动地将数组名替换为一个指向数组首元素的指针。因为arr
是二维数组,它的首元素的类型是一个长度为4的一维数组,因此这个类型与变量i
的类型是吻合的。
之后,内层循环中,我们要获得指向整型元素的指针。由于变量i
是指向数组的指针,因此我们对变量i
进行解引用操作*i
获得的数据是一个数组。而遍历用到的变量j
是一个int*
型的指针,因此我们将当前数组*i
直接赋值给j
,编译器就会将数组类型转化为指向数组首元素的指针。
如果上面的代码不太好理解,我们可以采用类型重命名的方式增加代码的可读性:
1 | using int_array = int[4]; // 类型别名 |
我们将长度为4的int
型数组重命名为int_array
,这样一来,由于变量i
的类型是指向数组的指针,因此其类型被声明为int_array*
。相比于上面的代码,这段代码更容易让程序员理解变量的含义。
当然,由于二维数组arr
的元素类型是确定的,因此遍历用到的i
和j
两个变量的类型也是确定的。我们可以使用C++11中的auto
关键字,让编译器来帮我们确定它们的具体类型,从而省去了显式声明类型的麻烦:
1 | for (auto i = arr; i < arr + 3; i++) { |
最后,在C++11标准下,我们可以用for each循环来遍历多维数组,从而不用考虑指针i
和j
的数据类型,也不用去额外考虑数组的边界问题。但是需要特别注意的是:多维数组的for each语句必须要使用引用来遍历!原因在后面给出。
仍然以这里的二维数组arr
为例,下面将arr
中的每一个元素值都变为原来的2倍:
1 | for (auto &row : arr) { |
如果只需要对arr
进行只读遍历,将引用改为const
属性即可:
1 | for (const auto &row : arr) { |
为什么只能使用引用来遍历多维数组呢?我们再一次回顾一维数组的内容:编译器会自动地将数组名替换为一个指向数组首元素的指针。将循环写成下面的形式将无法通过编译:
1 | for (auto row : arr) { |
这是因为,在外层循环中,程序需要将二维数组的第一行赋值给变量row
。由于row
不是引用,因此在赋值时,编译器自动将该数组转化为指向该数组首元素的指针。也就是说,这里变量row
的类型被判断为int*
,是一个指向整型变量的指针,而不是指向数组的指针。换言之,这里的变量row
表示的不是数组,那么内层的循环自然就不合法了。
参考资料:
- C++ Primer 中文版(第 5 版)