8.4.3 指向多维数组的指针变量

1 多维数组的指针
多维数组可以看作是一维数组的延伸,多维数组的内存单元也是连续的内存单元。换句话说,C语言实际上是把多维数组当成一维数组来处理的。下面以二维数组为例说明这个概念。
比如,现在有一个int型的二维数组a[3][4],计算机认为这是一个一维的数组a[3],数组的三个元素分别是a[0],a[1]和a[2]。其中每个元素又是一个一维数组,例如a[0]又是一个包含a[0][0],a[0][1],a[0][2]和a[0][3]共4个元素的数组。如果我们要引用数组元素a[1][2],可以首先根据下标1找到a[1],然后在a[1]中找到第3个元素a[1][2]。假设数组a的起始地址为FF10,对应的内存情况如图:

DS:FF10
a[0][0] DS:FF12
a[0][1] DS:FF14
a[0][2] DS:FF16
a[0][3]
DS:FF18
a[1][0] DS:FF1A
a[1][1] DS:FF1C
a[1][2] DS:FF1E
a[1][3]
DS:FF20
a[2][0] DS:FF22
a[2][1] DS:FF24
a[2][2] DS:FF26
a[2][3]

可以看到,二维数组a[3][4]的12个元素在内存中是顺序排列的,因此&a[0][0]是数组第一个元素的地址,为FF10。
a[0][0],a[0][1]这些数组元素都有具体的内存单元值。但是,a[0],a[1]和a[2]这三个数组元素不占用内存单元,它们只是代号(其实就是一个指针常量),是虚拟的东西。a[0]本身又是一个数组,包含a[0][0],a[0][1],a[0][2]和a[0][3],那么a[0]作为数组名称,按照C语言的语法,a[0]就是数组首地址,也就是数组第一个元素的地址,即a[0][0]的地址 FF10。同理,a[1]是第一个元素a[1][0]的地址,即FF18;a[2]是第一个元素a[2][0]的地址,即FF20。
相对于a[0],a[1]和a[2]来说,a也是数组的名称,是数组的首地址,即FF10。a是指向数组首地址的指针,a+1代表什么?a是数组名称,a[0],a[1]和a[2]是元素,那么a+1就是&a[1](如果一个一维数组int b[3],那么b+1是不是&b[1]?),即第二行元素的首地址,指针值为FF18。同理,a+2就是&a[2],指针值为FF20。
也可以换个角度去理解。a是数组首地址,指向二维数组第一行的首地址;计算a+1时,指针a跳过的是整个a[0],a+1指向二维数组第二行的首地址。指针a在做加法时,跳过的是一行元素。
a[1]+1代表什么?指针a[1]是一维数组a[1]中第一个元素a[1][0]的地址FF18。对于一维数组a[1]来说,指针a[1]+1指向了a[1]中第二个元素a[1][1]的地址,即FF1A。这里指针加法的单位是一列,每次跳过a[1]中的一个数组元素。
*a代表什么?*a也就是*(a+0),数组第一个元素的值,即a[0]。前面讲过,a[0]是一个代号,它不是一个具体元素的值,而是内嵌的一维数组a[0]的名字,a[0]本身也是一个指针值。同理,*(a+1)就是a[1],*(a+2)就是a[2]。
如果要访问二维数组里第i行第j列的某个元素,可以直接引用a[i-1][j-1],也可以使用指针的方法。指向第i行第j列元素的指针为a[i]+j,或者*(a+i)+j,因此,间接用指针引用二维数组里第i行第j列的某个元素,可以表示为*(a[i]+j)或者*(*(a+i)+j)。
特别需要指出的是,a[i]不是一个具体的数组元素,它是一个代号,实际上是一个指针值,代表i+1行里中第一个元素的首地址。因此&a[i]不能直接理解为a[i]的物理地址,a[i]不是变量,不存在物理地址;&a[i]代表第i+1行的行指针值,和a+i等价。
同样,*a[i]也不能理解为对一个数组元素进行指针运算符操作。a[i]是一个指针值,是i+1行中第一个元素的首地址,那么*a[i]就是i+1行里中第一个元素的值,即a[i][0]。
总之,二维数组a[i][j]的指针由于出现了虚拟的a[i]这个概念,所以对于初学者很不好理解,请读者仔细消化理解。下表是上述概念的综合:


main()
{
int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12},i,j;

/*打印a*/
printf("\na=%X", a);

/*打印a+i, &a[i]*/
for(i=0;i<3;i++)
printf("\na+%d=%X, &a[%d]=%X", i,a+i,i,&a[i]);

/*打印a[i], *(a+i)*/
for(i=0;i<3;i++)
printf("\na[%d]=%X, *(a+%d)=%X", i,a[i],i,*(a+i));

/*打印a[i]+j, *(a+i)+j, &a[i][j]*/
for(i=0;i<3;i++)
for(j=0;j<4;j++)
printf("\na[%d]+%d=%X, *(a+%d)+%d=%X, &a[%d][%d]=%X",
i,j,a[i]+j, i,j,*(a+i)+j, i,j,&a[i][j]);

/*打印**(a+i), *a[i]*/

for(i=0;i<3;i++)
printf("\n**(a+%d)=%X, *a[%d]=%X", i, **(a+i),i, *a[i]);

/*打印*(a[i]+j), *(*(a+i)+j), a[i][j]*/
for(i=0;i<3;i++)
for(j=0;j<4;j++)
printf("\na[%d]+%d=%d, *(a+%d)+%d=%d, &a[%d][%d]=%d",
i,j,*(a[i]+j), i,j,*(*(a+i)+j), i,j,a[i][j]);
}
程序运行的结果为(打印的地址值是动态分配的,不同环境下可能不同;但数组的地址偏移量是相同的):
a=FFBE
a+0=FFBE, &a[0]=FFBE
a+1=FFC6, &a[1]=FFC6
a+2=FFCE, &a[2]=FFCE
a[0]=FFBE, *(a+0)=FFBE
a[1]=FFC6, *(a+1)=FFC6
a[2]=FFCE, *(a+2)=FFCE
a[0]+0=FFBE, *(a+0)+0=FFBE, &a[0][0]=FFBE
a[0]+1=FFC0, *(a+0)+1=FFC0, &a[0][1]=FFC0
a[0]+2=FFC2, *(a+0)+2=FFC2, &a[0][2]=FFC2
a[0]+3=FFC4, *(a+0)+3=FFC4, &a[0][3]=FFC4
a[1]+0=FFC6, *(a+1)+0=FFC6, &a[1][0]=FFC6
a[1]+1=FFC8, *(a+1)+1=FFC8, &a[1][1]=FFC8
a[1]+2=FFCA, *(a+1)+2=FFCA, &a[1][2]=FFCA
a[1]+3=FFCC, *(a+1)+3=FFCC, &a[1][3]=FFCC
a[2]+0=FFCE, *(a+2)+0=FFCE, &a[2][0]=FFCE
a[2]+1=FFD0, *(a+2)+1=FFD0, &a[2][1]=FFD0
a[2]+2=FFD2, *(a+2)+2=FFD2, &a[2][2]=FFD2
a[2]+3=FFD4, *(a+2)+3=FFD4, &a[2][3]=FFD4
**(a+0)=1, *a[0]=1
**(a+1)=5, *a[1]=5
**(a+2)=9, *a[2]=9
a[0]+0=1, *(a+0)+0=1, &a[0][0]=1
a[0]+1=2, *(a+0)+1=2, &a[0][1]=2
a[0]+2=3, *(a+0)+2=3, &a[0][2]=3
a[0]+3=4, *(a+0)+3=4, &a[0][3]=4
a[1]+0=5, *(a+1)+0=5, &a[1][0]=5
a[1]+1=6, *(a+1)+1=6, &a[1][1]=6
a[1]+2=7, *(a+1)+2=7, &a[1][2]=7
a[1]+3=8, *(a+1)+3=8, &a[1][3]=8
a[2]+0=9, *(a+2)+0=9, &a[2][0]=9
a[2]+1=10, *(a+2)+1=10, &a[2][1]=10
a[2]+2=11, *(a+2)+2=11, &a[2][2]=11
a[2]+3=12, *(a+2)+3=12, &a[2][3]=12
从输出的结果可以看到,&a[i]和a[i]的输出值都是一样的。第i+1行的首地址是FFBE,第i+1行、第1列元素的首地址也是FFBE,这并不矛盾,因为二者都是一个指针值。但是,二者的含义是截然不同的,指针加法的偏移量也是不同的:前者加1指针跳到下一行;后者加1指针跳到下一列。

2 指向多维数组的指针变量
可以定义指针变量,指向多维数组及其元素。下面仍以二维数组为例进行说明。
二维数组在内存中是连续的内存单元,我们可以定义一个指向内存单元起始地址的指针变量,然后依次拨动指针,这样就可以遍历二维数组的所有元素。例如:
main()
{
float a[2][3]={1.0,2.0,3.0,4.0,5.0,6.0},*p;
int i;
for(p=*a;p<*a+2*3;p++)
printf("\n%f ",*p);
}
结果输出:
1.000000
2.000000
3.000000
4.000000
5.000000
6.000000
在上述例子中,定义了一个指向float型变量的指针变量。语句p=*a将数组第1行,第1列元素的地址赋给了p,p指向了二维数组第一个元素a[0][0]的地址。根据p的定义,指针p的加法运算单位正好是二维数组一个元素的长度,因此语句p++使得p每次指向了二维数组的下一个元素,*p对应该元素的值。
根据二维数组在内存中存放的规律,我们也可以用下面的程序找到二维数组元素的值:
main()
{
float a[2][3]={1.0,2.0,3.0,4.0,5.0,6.0},*p;
int i,j;
printf("Please input i =");
scanf("%d", &i);
printf("Please input j =");
scanf("%d", &j);
p=a[0];
printf("\na[%d][%d]=%f ",i,j,*(p+i*3+j));
}
输入下标i和j的值后,程序就会输出a[i][j]的值。这里我们利用了公式p+i*3+j计算出了a[i][j]的首地址。计算二维数组中任何一个元素地址的一般公式如下:
二维数组首地址+i*二维数组列数+j
请读者分析二维数组的内存分配情况,自行证明上述公式。
上述的指针变量指向的是数组具体的某个元素,因此指针加法的单位是数组元素的长度。我们也可以定义指向一维数组的指针变量,使它的加法单位是若干个数组元素。定义这种指针变量的格式为:
数据类型 (*变量名称)[一维数组长度];
说明:
(1) 括号一定不能少,否则[]的运算级别高,变量名称和[]先结合,结果就变成了后续章节要讲的指针数组;
(2) 指针加法的内存偏移量单位为:数据类型的字节数*一维数组长度。
例如,下面的语句定义了一个指向long型一维、5个元素数组的指针变量p:
long (*p)[5];
指针变量p的特点在于,加法的单位是4*5个字节,p+1跳过了数组的5个元素。
上述指针变量的特点正好和二维数组的行指针相同,因此我们也可以利用指针变量进行整行的跳动:
main()
{
float a[2][3]={1.0,2.0,3.0,4.0,5.0,6.0};
float (*p)[3];
int i,j;
printf("Please input i =");
scanf("%d", &i);
printf("Please input j =");
scanf("%d", &j);
p=a;
printf("\na[%d][%d]=%f ",i,j,*(*(p+i)+j));
}
说明:
(1) p定义为一个指向float型、一维、3个元素数组的指针变量p。
(2) 语句p=a将二维数组a的首地址赋给了p。根据p的定义,p加法的单位是3个float型单元,因此p+i等价于a+i,*(p+i)等价于*(a+i),即a[i][0]元素的地址,也就是该元素的指针。
(3) *(p+i)+j等价于& a[i][0]+j,即数组元素a[i][j]的地址;
(4) *(*(p+i)+j)等价于(*(p+i))[j],即a[i][j]的值。
p在定义时,对应数组的长度应该和a的列长度相同。否则编译器检查不出错误,但指针偏移量计算出错,导致错误结果。
例 8-1 输入一组整数(数量不超过100),统计其中偶数和奇数的总和(用指针方式)。
可以使用一个int型数组保存输入的一组整数,然后利用指针依次遍历数组的元素,统计偶数和奇数的总和。a[i]和*(a+i)是等价的。
偶数和奇数的辨别可以用整数%2的方法,%2为0则为偶数,否则为奇数。
main()
{
int a[100],i,j,nLen, nEvenCount=0, nOddCount=0,*p=a;

/*输入整数的个数*/
printf("\nPlease input count of integers: ");
scanf("%d",&nLen);

/*依次输入一组整数*/
printf("Please input integers: ");
for(i=0;i<nLen;i++)
scanf("%d",p+i); /*等价于scanf("%d", &a[i])*/

/*统计偶数和奇数的个数*/
for(i=0;i<nLen;i++,p++)
{
if(*p%2==0)
nEvenCount++;
else
nOddCount++;
}

/*打印输出结果*/
printf("There are %d even numbers, %d odd numbers. ",nEvenCount, nOddCount);
}
程序的运行结果为:
Please input count of integers: 6
Please input integers: 1 3 4 65 8 66
There are 3 even numbers, 3 odd numbers.

上面的例子中,指针变量指向了数组a的首地址,p+i即为数组元素a[i]的指针,因此scanf语句中可以使用p+i作为参数。
统计偶数和奇数时,循环每次使p指向数组的下一个元素,循环中*p的值是数组对应元素的值。注意nEvenCount和nOddCount一定要初始化为零,否则它们都是随机数。
例 8-2 编写一个函数,可以实现2*3 3*4矩阵相乘运算(用指针方式)。
使用一个2×3的二维数组和一个3×4的二维数组保存原矩阵的数据;用一个2×4的二维数组保存结果矩阵的数据。
结果矩阵的每个元素都需要进行计算,可以用一个嵌套的循环(外层循环2次,内层循环4次)实现。
根据矩阵的运算规则,上述矩阵每个元素的计算方法为:

因此,内层循环里可以再使用一个循环,累加得到每个元素的值。一共使用三层嵌套的循环。

main()
{
int i,j,k, a[2][3],b[3][4],c[2][4];

/*输入a[2][3]的内容*/
printf("\nPlease input elements of a[2][3]:\n");
for(i=0;i<2;i++)
for(j=0;j<3;j++)
scanf("%d", a[i]+j); /* a[i]+j等价于&a[i][j]*/

/*输入b[3][4]的内容*/
printf("Please input elements of b[3][4]: \n");
for(i=0;i<3;i++)
for(j=0;j<4;j++)
scanf("%d", *(b+i)+j); /* *(b+i)+j等价于&b[i][j]*/

/*用矩阵运算的公式计算结果*/
for(i=0;i<2;i++)
for(j=0;j<4;j++)
{
*(c[i]+j)=0; /* *(c[i]+j)等价于c[i][j]*/
for(k=0;k<3;k++)
*(c[i]+j)+=a[i][k]*b[k][j];
}

/*输出结果矩阵c[2][4]*/
printf("\nResults: ");
for(i=0;i<2;i++)
{
printf("\n");
for(j=0;j<4;j++)
printf("%d ",*(*(c+i)+j)); /* *(*(c+i)+j)等价于c[i][j]*/
}
}
运行结果为:
Please input elements of a[2][3]:
1 2 3
4 5 6
Please input elements of b[3][4]:
1 2 3 4
5 6 7 8
9 10 11 12

Results:
38 44 50 56
83 98 113 128
上面的例子复习了二维指针的各种表示方法,在实际应用中可以灵活使用。