在C语言这里,指针与数组的结合使用非常灵活,这里将一些基础的知识和常见的用法进行总结。
数组在底层的存储方式
我们都知道数组可以有多个维度,但它们的存储方式都是按照一维的方式进行存储,在地址上是连续的,可以在如下例子中看出
int arr1[5];
for(int i = 0;i < 5;i++){
arr1[i] = i;
}
printf("arr1的地址是%p\n",arr1);
for(int i = 0;i < 5;i++){
printf("arr1[%d]的地址是%p\n",i,&arr1[i]);
}
int arr2[3][3];
for(int i = 0;i < 3;i++){
for(int j = 0;j < 3;j++){
arr2[i][j] = i * 3 + j;
}
}
printf("arr2的地址是%p\n",arr2);
for(int i = 0;i < 3;i++){
for(int j = 0;j < 3;j++){
printf("arr2[%d][%d]的地址是%p\n",i,j,&arr2[i][j]);
}
}
结果如下
arr1的地址是000000000061FDF0
arr1[0]的地址是000000000061FDF0
arr1[1]的地址是000000000061FDF4
arr1[2]的地址是000000000061FDF8
arr1[3]的地址是000000000061FDFC
arr1[4]的地址是000000000061FE00
arr2的地址是000000000061FDC0
arr2[0][0]的地址是000000000061FDC0
arr2[0][1]的地址是000000000061FDC4
arr2[0][2]的地址是000000000061FDC8
arr2[1][0]的地址是000000000061FDCC
arr2[1][1]的地址是000000000061FDD0
arr2[1][2]的地址是000000000061FDD4
arr2[2][0]的地址是000000000061FDD8
arr2[2][1]的地址是000000000061FDDC
arr2[2][2]的地址是000000000061FDE0
我们不难看出,arr1数组是一维整数数组,从结果中很容易可以看出arr1[ 1 ]和arr[ 2 ]之间相差了4个字节,这是因为int类型的数据在这里占4个字节,而arr[ 0 ] ~ arr[ 4 ]之间每个地址之间都相差四个字节,所以arr1数组是按照一维连续进行存储的,同理观察arr2的输出结果也不难发现其是一维连续存储的。
总的来说就是数组存储在一段连续的地址中。这时候就有人想如果定义一个非常大的数组导致系统找不到这么一段存储空间怎么办,那当然是出错了。不同的编译器对数组的长度都做了限制(可以自己用循环找到最大值),一般来说,静态数组的长度比一般的数组的更长。
用指针表示一维数组
定义:int *p = (int*)malloc(5 * sizeof(int));
解释:malloc函数用于向系统申请空间,在这里申请了一个连续的、大小为5个int类型变量长度和的空间,malloc前面的(int*)表示返回一个int类型的指针,指针p指向这块空间的第一个int,就类似于arr[0].
运用:
int a = 1;
int *p = (int*)malloc(5 * sizeof(int));
for(int i = 0;i < 5;i++){
*(p + i) = i;//也可用 p[i]=i
}
//对应下面第一幅图
for(int i = 0;i < 5;i++){
printf("p[%d] = %d \n",i,*(p + i));
}
for(int i = 0;i < 5;i++){
printf("p[%d] = %d \n",i,p[i]);
}
解释:第一种获得存储数据的方法通过指针地址的移动实现(注意p的值是一直不变的,变的是(p+i)这个整体的值,当然p的值也可以改变,但当你还想重新获得p所指位置前面的数据时想要对p做减法),第二种与一般的数组类似。
用指针表示二维数组
int **p;
p = (int**)malloc(3 * sizeof(int*));
//内存中对应变化如下图
for(int i = 0;i < 3;i++){
*(p + i) = (int*)malloc(3 * sizeof(int));
}
//内存中对应变化如下图
使用:
for(int i = 0;i < 3;i++){
for(int j = 0;j < 3;j++){
*(*(p + i) + j) = i * 3 + j;
}
}
for(int i = 0;i < 3;i++){
for(int j = 0;j < 3;j++){
printf("p[%d][%d] = %d\n",i,j,p[i][j]);
}
}
结果
p[0][0] = 0
p[0][1] = 1
p[0][2] = 2
p[1][0] = 3
p[1][1] = 4
p[1][2] = 5
p[2][0] = 6
p[2][1] = 7
p[2][2] = 8
下面是数组各元素的地址
for(int i = 0;i < 3;i++){
printf("p[%d]的地址是%p\n",i,&p[i]);
}
for(int i = 0;i < 3;i++){
for(int j = 0;j < 3;j++){
printf("p[%d][%d]的地址是%p\n",i,j,&p[i][j]);
}
}
p[0]的地址是00000000001D6B90
p[1]的地址是00000000001D6B98
p[2]的地址是00000000001D6BA0
p[0][0]的地址是00000000001D6BB0
p[0][1]的地址是00000000001D6BB4
p[0][2]的地址是00000000001D6BB8
p[1][0]的地址是00000000001D6BD0
p[1][1]的地址是00000000001D6BD4
p[1][2]的地址是00000000001D6BD8
p[2][0]的地址是00000000001D6BF0
p[2][1]的地址是00000000001D6BF4
p[2][2]的地址是00000000001D6BF8
从上面的示例我们可以比较清楚地看到二维指针用于二维数组时的整个过程,我总结出以下几点:
①用二维指针定义的数组在内存上并不是连续的,而是按照定义时的分成三个连续的部分(指的是p[ i ][ j ]部分)。
②二维指针定义的数组在定义完成后的使用与常用方法定义的数组的使用是非常相似的。
③二维指针定义的数组在定义时并不能像一维指针那样一次申请到所需的空间,而是要分几次进行。
总结
数组名和指针十分相似,可以说数组名就是一个不可以改变指向的指针。一般方法定义的二维数组在存储方式上与用二维指针定义的数组存在差异,前者(假设为 n * m)可以看成是一次性申请了一块大小为 n * m 的空间,后者就是分 n 次申请,每次申请大小为 m 的空间。