指针和多维数组有什么关系?为什么要了解它们的关系?
处理多维数组的函数要用到指针,所以在使用这种函数之前,先要更深入地学习指针。
指针和多维数组的关系
int zippo[4][2]; /* 内含int数组的数组 */
数组名 zippo 是该数组首元素的地址。在本例中,zippo 的首元素是一个内含两个 int 值的数组,所以 zippo 是这个内含两个 int 值的数组的地址。
下面,我们从指针的属性进一步分析。
因为 zippo 是数组首元素的地址,所以 zippo 的值和 &zippo[0] 的值相同。而 zippo[0] 本身是一个内含两个整数的数组,所以 zippo[0] 的值和它首元素(一个整数)的地址(即&zippo[0][0]的值)相同。
简而言之,zippo[0] 是一个占用一个 int 大小对象的地址,而 zippo 是一个占用两个 int 大小对象的地址。由于这个整数和内含两个整数的数组都开始于同一个地址,所以 zippo 和 zippo[0] 的值相同。
给指针或地址加 1,其值会增加对应类型大小的数值。在这方面,zippo 和 zippo[0] 不同,因为 zippo 指向的对象占用了两个 int 大小,而 zippo[0] 指向的对象只占用一个 int 大小。因此, zippo + 1 和 zippo[0] + 1 的值不同。
解引用一个指针(在指针前使用 运算符)或在数组名后使用带下标的 [] 运算符,得到引用对象代表的值。因为 zippo[0] 是该数组首元素(zippo[0][0])的地址,所以 (zippo[0]) 表示储存在 zippo[0][0] 上的值(即一个 int 类型的值)。与此类似,zippo 代表该数组首元素(zippo[0])的值,但是 zippo[0] 本身是一个 int 类型值的地址。该值的地址是 &zippo[0][0],所以 zippo 就是 &zippo[0][0]。对两个表达式应用解引用运算符表明, *zippo 与 &zippo[0][0] 等价,这相当于 zippo[0][0],即一个 int 类型的值。
简而言之, zippo 是地址的地址,必须解引用两次才能获得原始值。地址的地址或指针的指针是就是双重间接(double indirection)的例子。
显然,增加数组维数会增加指针的复杂度。现在,大部分初学者都开始意识到指针为什么是 C 语言中最难的部分。
int zippo[4][2] = { { 2, 4 }, { 6, 8 }, { 1, 3 }, { 5, 7 } };
printf(" zippo = %p, zippo + 1 = %p\n",zippo, zippo + 1);
printf("zippo[0] = %p, zippo[0] + 1 = %p\n",zippo[0], zippo[0] + 1);
printf(" *zippo = %p, *zippo + 1 = %p\n",*zippo, *zippo + 1);
printf("zippo[0][0] = %d\n", zippo[0][0]);
printf(" *zippo[0] = %d\n", *zippo[0]);
printf(" **zippo = %d\n", **zippo);
printf(" zippo[2][1] = %d\n", zippo[2][1]);
printf("*(*(zippo+2) + 1) = %d\n", *(*(zippo + 2) + 1));
// 结果
zippo = 0x0064fd38, zippo + 1 = 0x0064fd40
zippo[0]= 0x0064fd38, zippo[0] + 1 = 0x0064fd3c
*zippo = 0x0064fd38, *zippo + 1 = 0x0064fd3c
zippo[0][0] = 2
*zippo[0] = 2
**zippo = 2
zippo[2][1] = 3
*(*(zippo+2) + 1) = 3
要特别注意,与 zippo[2][1] 等价的指针表示法是 ((zippo+2) + 1)。看上去比较复杂,应最好能理解。下面列出了理解该表达式的思路:
指向二维数组的指针
声明一个指向二维数组的指针使用的是数组指针。
int (* pz)[2];
指针的兼容性
指针之间的赋值比数值类型之间的赋值要严格。例如,不用类型转换就可以把 int 类型的值赋给 double 类型的变量,但是两个类型的指针不能这样做。
int (*pa)[2];
int (*pb)[3];
int ar[3][2];
int **p; // 一个指向指针的指针
pa = ar; // 有效都是指向内含2个int类型元素数组的指针
pb = ar; // 无效
p = ar; // 无效
*p = ar[0]; // 有效
形参是二维数组的函数声明
以 arr[2][3] 作为实参为例,支持下面两种形参是二维数组的函数声明和定义。
int f(int (*ar)[3]);
int f(int ar[][3]);
注意,数组的列数是必须和实参一致的,不能省略。这一点十分重要,因为列数向函数传递了必须的信息。
刚开始学的时候,可能会疑惑为什么 int ar 不能来接收二维数组。下面来讲解一下 int ar 和 int (*ptr)[3] 有什么差别?
主要是由于指针加法的问题。这涉及到编译器如何对一个指针进行加法操作了。因此,我们先看一下指针怎么进行加法的。
以 int ptr 为例,ptr + 3,编译器会先从 ptr 中读取地址,然后判断这个指针的类型是 int 类型,才知道对 ptr + 3 应该是加三个 int 的长度,即地址加 34 个字节。
总结一下,编译器对指针的加法操作:1. 获取指针变量指向的地址;2. 获取指针类型,计算该指针指向的类型的内存大小;3. 计算加法操作之后的地址。
如此,我们再回来看一下 int* ar 指针,对它进行 +1 操作的过程:1. 获取 ar 指针指向的地址;2. 指针指向的类型是 int(int 在 32 位系统中是 4 字节,64 位系统中是 8 字节,这里我们假设是 64 位系统),占据的内存大小是 8 字节;3. 对 ar + 1 就是在 ar 指向的地址的基础上加 18 个字节。
而 int (ptr)[3] 的加法:ptr+1 的过程:1. 获取 ptr 的地址;2. ptr 的类型是含有三个元素的数组,数组的元素是 int,因此 ptr 指向的类型占据的内存大小是 3 个 int 类型的大小,即 12 个字节;3. 对 ptr + 1 就是在 ptr 指向的地址的基础上加 112 个字节。
#include<stdio.h>
int f(int **a);
int main()
{
int a[2][3] = {{1, 7, 8},{2, 3, 4}};
f(a);
return 0;
}
int f(int **a)
{
printf("*(a+1):%d\n", *(a+1));
return 0;
}
输出结果:8。
可以看到 int* ar 的加法操作和二维数组是有区别的,因此用 ar 去接收二维数组在运行时极可能会造成程序的崩溃。
所以 int (ptr)[3] 中的 [3] 就是为了告诉编译器进行指针加法运算时应该移动的距离。
一般而言,声明一个指向 N 维数组的指针时,只能省略最左边方括号中的值:
int sum4d(int ar[][12][20][30], int rows);