为了讲清楚我们要说明的问题,首先我们来定义一个二维数组:

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

这是一个2行3列的二维数组,如果我们要使用范围 for 循环来输出这个二维数组中的元素的话,相应地代码如下:

1
2
3
4
5
for (auto &row : ival) {
for (auto elem : row)
cout << elem << " ";
cout << endl;
}

上面的代码能不能完成预期的输出二维数组元素的功能呢?我们来实际运行一下,在 VS2013 中的代码如下:

运行结果如下:

显然,实现了我们预期的目的,但是这里有一个问题,为什么要在最外层的范围 for 循环使用 & 操作符呢?写成 for (auto row : ival) 不行么?当然是不行的,下面就来交待一下理由。

我们不妨将 for (auto &row : ival) 改写成传统的 for 循环试试:

1
2
3
for (auto mbeg = begin(ival), mend = end(ival);
mbeg != mend; ++mbeg)
auto &row = *mbeg;

在 VS2013 中运行一下看看改写的是否正确:

运行结果如下:

显然我们改写的是对的,实际上,范围 for 循环本质上就是我们改写的传统 for 循环,下面就我们改写后的传统 for 循环来分析一下:

begin(ival) 得到的是指向 ival 首元素的指针,ival 的首元素是什么呢?二维数组本质上是数组的数组,所以我们得到的是一个指向含有 3 个元素的一维数组的指针,指向数组的指针实际上是指向数组首元素的指针的指针(见我的博文: 指向数组的指针与指向数组首元素的指针 ),因此我们对 mbeg 解引用之后将会得到一个指向某个一维数组首元素的指针,相当于一个一维数组的数组名,如果没有 & 操作符,即如果是以下的形式:

1
auto row = *mbeg;

row 的类型将会是 int*,即 row 是一个整型指针的副本,接下来的范围 for 语句就成了遍历一个指针内的元素了,这显然是错误的,与我们的初衷大相径庭,但是如果我们加上一个引用操作符( & ),对 mbeg 解引用之后得到的指针相当于一个一维数组的数组名,因此 row 的类型就变成了 int (&row)[3],即 row 是与一个含有 3 个整型数的一维数组绑定的引用,接下来的范围 for 语句就是遍历这个含有 3 个整型数的一维数组,这与我们的初衷是相符的,实际上:

要使用范围 for 语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型

编译原理的知识告诉我们, 指针的类型包括指针所指向的类型 ,因此,指向数组的指针与指向数组首元素的指针是不同类型的指针,指向数组首元素的指针与指向一个普通元素的指针也属于不同类型的指针,这是编译方面的知识,适当了解一下有助于我们理解相关的语言特性。


还有一个极易混淆的地方需要特别指出一下,在我们处理 initializer_list 类型的元素的时候,通常也会用到 & 操作符:

1
2
3
4
5
6
7
// 打印 list 中的元素
void print (initializer_list<string> list)
{
for (const auto &elem : list)
cout << elem << " ";
cout << endl;
}

这里的范围 for 语句为什么要用到引用操作符呢?实际上,不用引用操作符也是可以的,也就是说,这里的范围 for 语句可以写成如下的形式:

1
for (const auto elem : list)

这样最后仍然能够得到一样的结果,这里使用 & 操作符主要是为了避免存储空间的浪费和拷贝造成的时间效率低下而直接将 elem 绑定到一个 const string 类型的对象上(initializer_list 类型的对象里面的元素都是常量),如果不使用 & 操作符的话, elem 将是 list 里面元素的拷贝,造成了存储空间的浪费,下面将程序在 VS2013 中运行一下以验证上面的想法。

加了 & 操作符的情形:

运行结果如下:

未加 & 操作符的情形:

运行结果如下:

完全符合预期的想法。