3.6 多维数组
其实C++中没有什么多维数组,所说的多维数组其实就是数组的数组。
当一个数组的元素依旧是数组时,通常使用两个维度来定义它:一个维度表示数组本身大小,另外一个维度表示其元素(数组的数组)大写:
int ia[3][4];
大小为3的数组,每个元素是含有4个整数的数组。
int arr[10][20][30] = {0};
大小为10的数组,它的每个元素都是大小为20的数组,这些数组的元素又包含30个整数的数组,最后将所有元素初始化为0。
由内到外的顺序阅读此类定义有助于更好地理解其真实含义。在第一条语句中,定义名称为ia,显然ia是一个含有3个元素的数组。再往右边写着ia的元素的维度,所以ia的元素本省又是含有4个元素的数组。在观察最左边,就能知道真正储存的元素(ia的元素的数组)是整数。因此最后可以明确第一条语句的含义:定义了一个大小为3,名为ia的数组,该数组的每个元素都是含有4个整数的数组。
再用同样的方式理解第二条arr的定义,首先arr是一个大小为10的数组,它的每个元素都是大小为10的数组,而这些数组的元素又都是含有30个整数的数组。并且,定义数组时对下标运算符的数量并没有限制,因此只要愿意就可以定义一个数组,它的元素是数组,数组的元素又是数组(禁止套娃!)
对于二维数组来说,常吧第一个维度成为行,第二个维度称为列。
多维数组的初始化
允许使用花括号括起来的一组值初始化多维数组,这点和普通的数组一样。下面初始化形式中,多维数组的每一行分别用花括号括了起来:
int ia[3][4]= //三个元素,每个元素都是大小为4的数组 { {0,1,2,3}, //第1行的初始值 {4,5,6,7}, //第2行的初始值 {8,9,10,11} //第3行的初始值 };
其中内层嵌套的花括号并非必需的,例如下面的初始化语句,形式上更为简洁,完成的功能和上面的代码完全一致:
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; //没有标识每行的花括号,与之前的初始化语句是等价的
类似一维数组,在初始化多维数组时也并非所有元素的值都必须包含在初始化表之内。如果仅仅想初始化每一行的第一个元素,通过如下的语句:
int ia[3][4] = {{0},{4},{8}}; //显式地初始化每行的首元素
其他没有列出的元素执行默认初始化,这个过程和一位数组一样。此时如果去掉内层花括号,结果就不同了。
int ix[3][4] = {0,3,6,9}; //显式地初始化第1行,其他元素执行值的初始化。
此时的含义是,它初始化了第一行的4个元素,其他元素被初始化0。
多维数组的下标引用
可以使用下标运算符来访问多维数组的元素,此时数组的每个维度都对应一个下标运算符。
如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素;举个例子:arr[0][0][0]的下标运算符数量为3,并且arr是3维的,此时下标运算符数量和数组的维度就是一样多了。
反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是定索引的一个内层数组:
ia[2][3] = arr[0][0][0]; //用arr的首元素为ia最后一行的最后一个元素赋值 int (&row)[4] = ia[1]; //把row绑定到ia的第二个4元素数组上
在第一个例子中,对于用到的两个数组来说,表达式提供的下标运算符数量和它们各自的维度相同。在等号左侧,ia[2]得到数组ia的最后一行,此时返回的是表达ia最后一行的那个一维数组而并非任何实际元素;对于这个一维数组再去取下标,得到编号为[3]的元素,也就是这行的最后一个元素。
经常使用for循环来处理多维数组的元素,二维数组大多数就两层嵌套的for循环去处理,外层for循环处理行,内层for循环处理列;三维数组中,最外层循环表示面,中间层表示行,最内层表示列。以此遍历数组:
constexpr size_t rowCnt = 3 , colCnt = 4; int ia[rowCnt][colCnt]; for(size_t i = 0;i != rowCnt;++i) { for(size_t j = 0;j != colCnt;++j) { ia[i][j] = i * colCnt + j; } }
使用范围for语句处理多维数组
由于在C++11新标准中新增了范围for语句,所以前一个程序可以简化为如下形式:
size_t cnt = 0; for (auto &row : ia) { for(auto &col : row) { col = cnt; ++cnt; } }
ia是一个由数组构成的数组,每次遍历,相当于一行一行的遍历了ia。所以每次遍历row相当于取出一行ia。相当于:对于外层数组的每一个元素。
而col又相当于每一行的row的引用。相当于:对于内层数组的每一个元素。
这个循环赋值给ia元素的值和之前的那个循环是完全相同的,区别在于通过使用范围for语句把管理数组索引的任务交给了系统。因为要改变元素的值,所以得把控制变量row和col声明成引用类型。 第一个for循环遍历ia的所有元素,这些元素是大小为4的数组,因此row的类型就应该是含有4个整数的数组的引用。 第二个for循环遍历那些4个元素数组中的某一个,因此col的类型是整数的引用。每次迭代把cnt的值赋给ia的当前元素,然后将cnt加1。
在上面的例子中,因为要改变数组元素的值,所以我们选用引用类型作为循环控制变量,但是其实还有一个深层次的原因促使我们这么做。
for(const auto &row:ia) for(auto col:row) cout<<col<<endl;
这个循环中并没有任何写操作,但是我们还是将外层循环的控制变量声明成了引用类型,这是为了 避免数组ia被自动转换成指针 。
for(auto row:ia) for(auto col:row)
此时ia被自动转化成指针,row也变成指针了,auto col:row就变成col遍历row的每一个地址。但是我们又不要遍历地址。所以外层的引用方式必须要加的。
程序无法通过编译。这是因为,想之前一样第一个循环遍历ia的所有元素,注意这些元素实际上是大小为4的数组。因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样会得到的row的类型就是int*,显然内层的循环就不合法了,编译器将试图在一个int*内遍历,这显然和程序的初衷不一样。
Tips:要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
指针和多维数组
当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。
因为多维数组实际上是数组的数组,所以由多维数组名转换而来的指针实际上是指向第一个内层数组的指针:
int ia[3][4]; //大小为3的数组,每个元素是含有4个整数的数组 int (*p)[4] = ia; //p指向含有4个整数的数组 p = &ia[2]; //p指向ia的尾元素。
(*p)意味着p是一个指针。接着看右侧,指针p指向的是一个维度为4的数组;再看左侧可知,数组中的元素是整数。所以,p是指向含有4个整数的数组的指针。
//在上述声明中,圆括号必不可少: int *ip[4]; //整形指针的数组 int (*ip)[4]; //指向含有4个整数的数组
随着C++11新标准提出,通过使用auto或者decltype就能尽可能地避免在数组前面加上一个指针类型了:
//输出ia中每个元素的值,每个内层数组各占一行 //p指向含有4个整数的数组 for(auto p = ia;p != ia + 3; ++p) { //q指向4个整数数组的首元素,也就是说,q指向一个整数 for(auto q = *p; q != *p + 4;++q) cout << *q <<' '; cout<<endl; }
外层的for循环首先声明了一个指针并且令其指向ia的第一个内层数组,然后依次迭代直到ia的全部3行都处理完为止。其中递增运算++p负责将指针p移动到ia的下一行。
内层的for循环负责输出内层数组所包含的值。它首先令指针q指向p当前所在行的第一个元素。*p是一个含有4个数组的数组,像往常一样,数组名被自动地转换成指向该数组首元素的指针。内层for循环不断迭代直到我们处理完了当前内层数组的所有元素为止。为了获取内层for循环的终止条件,再一次解引用p得到指向内层数组首元素的指针,给它加上4就得到了终止条件。
当然,使用标准库函数begin和end也能实现同样的功能,而且看起来更简洁一些:
//p指向ia的第一个数组 for(auto p = begin(ia); p != end(ia); ++p) { //q指向内层数组的首元素 for(auto q = begin(*p); q != end(*p); ++q) cout<< *q << ' ' ; //输出q指向的整数 cout <<endl; }
在此代码中,循环终止条件由end函数负责判断。虽然我们能够判断出p和q的类型,但是直接使用auto关键字我们就不必再操心这些类型是什么了。
类型别名简化多维数组的指针
读,写和理解一个指向多维数组的指针是让人不胜其烦的工作,使用类型别名能让工作变得简单一点。
using int_array = int[4]; //新标准下类型别名的声明 typedef int int_array[4]; //等价typedef声明 //输出ia中每个元素的值,每个内层数组各占一行 for(int_array *p = ia; p != ia + 3;++q) { for (int *q = *p;q != *p + 4; ++q) cout<< *q <<' '; cout<<endl; }
程序将类型“4个整数组成的数组” 命名为int_array,用类型名int_array定义外层循环的控制变量让程序看着更简洁;