14.7 不能拥有多维数组
这是语言中一个公开的缺陷,所以我在声明以下观点之前无意消除你的戒心:
Imperfection:C++(www.cppentry.com)不支持多维数组。
好吧好吧,我承认我这种说法有点像花边新闻吸引眼球所惯用的伎俩,而未遵循一个好的标题所理应具备的品质。准确地说,C和C++(www.cppentry.com)确实(在某种意义上)支持多维数组。标准(C++(www.cppentry.com)-98 8.3.4.3)声明:"当几个数组……一个挨着一个的时候就会形成一个多维数组。"然而标准同样提到:"用于指定界限的常量表达式……只可以忽略对应于第一维的。"所以,除了最左边的那一维之外,其他各维必须对应于(编译期)常量。因而:
- void process3dArray(int ar[][10][20]); // Ok
- void process3dArray(int ar[][][]); // 错误!编译器无所适从!
对我来说,这是个不折不扣的缺陷,因为我不认为在一个严肃的项目中我会去使用这样的多维数组,我可不想除了第一维之外再也得不到额外的灵活性。在这种情况下,人们或者去寻找迂回解决方案(换句话说,使用一维数组模拟多维数组并手工计算偏移量);或者为那些固定的维指定最大值,并让动态改变的下标处于最大值范围内;或者去使用某个多维数组类。
C/C++(www.cppentry.com)内建的数组采用连续的内存布局策略,为给定维度的每个元素分配的内存,按升序方式包含有下一维度的所有元素。例如,数组a3[2][3][4]的内存布局实际上如图14.1所示:
|
| 图14.1 |
这种连续的内存布局 非常有用,它允许"数组切片"(子数组)的传递,这很简单,只需获取该"切片"部分的首元素地址即可。例如,某段代码操纵一个[3][4]的二维数组,则该段代码同样可以操纵a3[0]或a3[1],如程序清单14.7所示:
程序清单14.7
- void print_array(int (*pa2)[3][4]);
-
- int a3[2][3][4]; // 2×3×4的三维数组
- int a2[3][4]; // 3×4的二维数组
- int (*pa2)[3][4]; // 指向3×4二维数组的指针
-
- pa2 = &a2; // 令指针指向二维数组
- pa2 = &a3[0]; // 令指针指向三维数组的二维切片
-
- print_array(pa2); // 将指向数组的指针作为参数传递
- print_array(&a2); // a2
- print_array(&a3[0]); // a3的一个切片
- print_array(&a3[1]); // a3的另一个切片
本例展示了这样一种情况:数组在作为实参传递给一个函数的时候并没有"退化"成指针(见14.4节),但这是因为函数形参被声明为一个指向数组的指针而非一个数组所致,正如我们所知,数组可以被看成指针来对待。晕头转向了吗?
数组的布局策略解释了为什么多维数组必须要求除第一维之外的所有维都有固定的大小。编译器在将下标索引的形式转换为"*(指针+偏移量)"的形式时必须知道其他所有维的大小,才能够计算出正确的偏移量(见14.2.1小节),从而计算出用户实际想要操纵的内存位置。例如,元素a3[1][0][3]的地址实际上是这样计算出来的(D1、D2分别表示二、三两维的大小):
- a3[1][0] + 3
- (a3[1] + 0 * D2) + 3
- ((a3 + 1 * D1 * D2) + 0 * 4) + 3
除非编译器已经知道D1、D2的大小,否则它就无法计算出实际的偏移量。本例中编译器计算出来的结果是: - a3 + 15
我们曾在14.2节看到,数组可以退化成指针。现在,我们需要将这个规则稍微精化一些。对于一个数组来说,其第一维几乎总是被编译器解释为一个指针。这有点像鸡和蛋的问题,但这就是我们的语言。
好吧,数组的第一维会退化,因此我们可以将先前的例子中的函数重写如下:
- void process3dArray(int (*ar)[10][20]);
这就是说,process3dArray()接受一个指向"10×20"二维数组的指针。很明显,这里编译器仍然知道第二、三维的大小,并且可以正确地使用该数组。然而,下面这种退化形式是非法的:- void process3dArray(int (*ar)[][]);
同样明显的是,这种形式没有包含有关第二、三两维大小的任何信息。编译器要想编译的话,就得具有足够的洞察力以便从调用上下文中推导出数组维度的大小并对ar进行正确的操纵。所以,使用process3dArray()的惟一途径就是指明3个维度的大小(或至少最后两个维度的大小)。这可以在编译期通过常量来指明,但最好采用process3dArray()的第一种形式,即显式指明维度的那个。一个替代方案是使用全局变量,这种形式相当糟糕。我们可以采用加入额外参数的方法,如下:
- void process3dArray( int *ar, size_t extent0, size_t extent1
- , size_t extent2);
这太不够简洁了,对不对?另外,它也无情地践踏了封装性以及(尤其是)可维护性。幸运的是,要想做出一个非常漂亮的解决方案也不算困难--并不能说简单,因为对于这样的东西而言,在表达力、灵活性以及效率上求得一个平衡着实不易。当然,我们将会用到类模板。具体细节你可以参看第33章,我故意在这里留了一点悬念,不然你怎么会继续往下读呢?不过在这里有一点可以指出,该解决方案依赖于能够从N+1维数组中取出N维切片的事实,如前所述。