9.6 高维数组名

9.6.1 高维数组名是指针

本节以二维数组为例,重点讲解高维数组的数组名的含义。如下定义了一个二维数组:

9.6 高维数组名 - 图1

作为二维数组的数组名,“a”可以进行“[]”运算(也就是可以进行一元“*”运算),所以显然“a”是一个指针。问题的重点在于其类型。

如图9-16所示,由于“*a”即“a[0]”本身是由3个“int”类型变量组成的一维数组“int [3]”,所以“a”是指向一个由3个“int”类型数据构成的一维数组的指针,这种类型在C语言中写做:

9.6 高维数组名 - 图2

图9-16 二维数组名的含义之一

9.6 高维数组名 - 图3

下面代码的输出证实了这一点。

程序代码9-21

9.6 高维数组名 - 图4

输出结果如图9-17所示。

9.6 高维数组名 - 图5

图9-17 高维数组名是指针

从输出结果可以看出,“&a[0][0]”与“a”的值相同,这表明这两个指针都始于同一个起点,也就是数组开始存储的第一个byte。然而“a+1”在数值上比“a”大“0022FF64-0022FF58=C”即十进制的12,说明“a”指向一大小为12byte的数据类型。最后3条的输出表明“a”、“a[0]”及“int [3]”类型所占据的内存空间皆为12byte。这就证实了“a”这个二维数组名是一个指向“int [3]”类型一维数组的指针,即“it()[3]”类型。

定义这种类型的指针变量的方法是:

9.6 高维数组名 - 图6

其中的“()”是必需的,这是因为“[]”的优先级比“”要高,在说明“p_a”类型的时候,为了说明“p_a”首先与“”相结合是一个指针变量,必须将“p_a”用“()”括起来以表明“p_a”是与“”紧密结合。下面的定义则表示另一种含义:

9.6 高维数组名 - 图7

这里由于标识符“a_p”的前后有“”和“[]”两个类型说明符,而“[]”的优先级别更高,因而“a_p”是一个数组名,“[]”中的“3”表示这个数组一共有3个元素,定义“int a_p[3];”中的其他部分说明的是数组元素的类型,本例中数组“a_p”的3个元素皆为“int *”类型。

回到原来“a”的定义。现在已经分析出了“a”的类型是指向由3个“int”类型数据所构成的一维数组的指针,显然“a+1”也是同样类型的表达式,由于表达式“*(a+1)”等价于“a[1]”,所以它指向“a[1]”,而“a[1]”同样是一个“int [3]”类型的一维数组。

9.6.2 高维数组名是内存

和一维数组名一样,在关于内存的运算中,二维数组名也代表这个二维数组所占据的那块内存。也就是说代表了一个数据对象(Object)

程序代码9-22

9.6 高维数组名 - 图8

程序运行结果如图9-18所示。

9.6 高维数组名 - 图9

图9-18 高维数组名是内存

“sizeof a”的值为24,表明“a”也表示这个二维数组(“int [2][3]”类型)所占据的内存。进而“&a”为一个指向二维数组的指针(“int(*)[2][3]”类型),所以在数值上“&a+1”比“&a”大18H(24D,即sizeof(int [2][3]))。如图9-19所示,程序最后的输出表明,“a”与指向二维数组的指针变量“p”具有同样的性质。

9.6 高维数组名 - 图10

图9-19 二维数组名的另一含义

由此可见,和一维数组名一样,二维数组名同样既可以表示指向其起始元素(“a[0]”)的指针,也表示自身所占据的内存。具体的含义必须在代码的上下文中才能确定,更具体的说要视这个数组名所参与的运算才能确定。

9.6.3 “a[0]”或“*a”的含义

由于“a”是指向一维数组的指针,所以“a[0]”或“*a”当然是一维数组类型(“int [3]”)。

然而在C语言中除了数组名,没有什么东西可以表示或代表一个数组整体,因此“a [0]”或“*a”的性质和数组名一样也就不足为怪了。

一方面“a[0]”或“a”可以表示一块内存——一维数组所占据的内存,这一点非常明显。因为根据运算符的定义就可以知道“&a[0]”或“&a”就是“a”——指向一维数组的指针。而且可以通过代码证实,“sizeof (a [0])”的值是“3*sizeof(int)”

另一方面,由于“a[0]”或“a”同样都可以进行一次“[]”或一元“”运算,这说明“a[0]”也就是“a”,同样是指针。“a[0]”(也就是“a”)进行一次“[]”或“”运算后将得到“a[0][0]”这个“int”类型的值,因而“a[0]”(也就是“a”)也是“int *”类型的指针。

再经过简单的推理,就可以轻易得出“a [0]”或“*a”与“a”在数值上完全相等的结论。因为指针记录的只是一块内存单元中最前面的那个字节的编号,而这几块内存是从同一处开始的,如图9-20所示。

9.6 高维数组名 - 图11

图9-20 a[0]的两种含义

总结一下:二维数组名的值(右值)是指向构成这个二维数组的首个一维数组的指针,同时代表这个二维数组所占据的内存。对这个指针再进行一次“”或“[]”运算就得到了一个一维数组对象“int [3]”,代表这个一维数组所占据的内存,由于能够代表数组对象的只有数组名这样的东西,因而这个对象的值(右值)的类型是“int []”,也就是指向这个一维数组的首个基本元素的指针“int ”。对于更高维的数组,可依此类推。

9.6.4 数组与指针关系总结

数组是一类数据类型的统称,在代码中用数组名表示数组,因而在前面和后面的论述中,数组名和数组实际上是相同的概念。

数组或数组名在代码中表现出两种性质:一方面具有数组类型,另一方面具有指针类型。具体地说就是,在作为左值表达式时表现为数组类型,代表数组所占据的内存空间;在作为右值表达式时表现为指针。

当作为“sizeof”、“&”运算符的运算对象时,数组或数组名为左值表达式。此外,数组或数组名不可以作为“++”、“——”运算符的运算对象,也不可以作为“=”运算符的左操作数。在其他运算场合,数组或数组名都是右值表达式。

数组或数组名作为右值表达式时,其值与数组所在内存块中存储的内容没有关系,数组所占据的内存空间存储的内容也不像结构体或联合体那样具有值的含义。数组或数组名的值(右值)表示的是指向构成这个数组的起始元素的指针。即,如果数组名为“a”;那么“a”就是指向“a[0]”的指针,无论对于几维数组这个结论都成立。

在对高维数组或高维数组名进行“*”或“[]”运算时,运算结果可能是数组类型。这个结果同样具有数组和指针两种含义,视具体运算场合才能确定究竟是何种含义。

对于指向数组的指针变量来说,由于进行“*”或“[]”运算得到的是数组类型的数据对象,因而其运算结果也同样具有数组和指针两种含义,需要视具体运算场合才能确定。

一般地说,对于n维(n>2)数组“a”,其数组名作为左值表达式时是n维数组类型,作为右值表达式时是指向n-1维数组的指针;而“*a”或“a[0]”作为左值表达式时是n-1维数组类型,作为右值表达式时是指向n-2维数组的指针……

此外请读者注意,有的书籍中认为数组始终具有数组类型,但在作为右值使用时存在着一个从数组到指针的隐式的类型转换。这与本书的叙述没有什么矛盾,只是叙述的方式不同罢了。

9.6.5 例题

例题:如图9-21所示,写一个程序,通过函数对一个5×6的二维int类型数组中从第i行第j列到第m行第n列的元素求和(假定0⩽i,j⩽4且0⩽m,n⩽5,且i×6+j⩽m×6+n),m、n、i、j由键盘输入。

9.6 高维数组名 - 图12

图9-21 例题说明示意图

不难设想,程序要求可以通过下面的函数完成:

9.6 高维数组名 - 图13

其中“p”为指向构成二维数组的第一个一维数组{2,3,4,5,6,7}的指针,“hs”为二维数组的行数。然而这种方案会给人一种笨拙的印象,因为这个函数只适用于第二维为“6”的二维数组。

实际上问题的本质是求若干在内存中连续存放的“int”类型数据的和,只要知道指向开始的那个“int”数据的指针和指向结尾的那个“int”数据的指针就完全可解。下面代码采用的就是这种解决方案。

9.6 高维数组名 - 图14

这涉及求这两个“int *”指针。

由于二维数组名“a”是指向{2,3,4,5,6,7}的指针,那么“a+i”就是指向第“i”个一维数组的指针。而“(a+i)”由于具有“int [6]”数组类型,因此作为右值是一个“int ”类型的指针,这样“*(a+i)+j”就是指向开始的那个“int”数据的指针。

同理,“a[m]+n”是指向结尾的那个“int”数据的指针。

程序代码9-23

9.6 高维数组名 - 图15

9.6 高维数组名 - 图16

运行结果如图9-22所示。

9.6 高维数组名 - 图17

图9-22 二维整型数组的求和

第1行第2列为6,第3行第4列为2,所以和为6+7+8+9+4+2+1+6+3+7+2+3+6+3+2=69。

练习

写一个函数判断一个二维数组是否为单位矩阵。所谓单位矩阵是指

(1)方阵。

(2)主对角线上元素为1,其余元素皆为0。

例如:

1 0 0

0 1 0

0 0 1