文章目录
C语言梦开始的地方10:指针进阶
上一章我们了解什么是指针,指针类型的意义,指针整数运算,野指针,assert断言和指针的使用和传址调用。本章将继续了解指针的其他使用方法以及上一章的留下的坑。
数组名的理解
上一章我们写了这样的代码
c
int arr[5] = {1,2,3,4,5};
int *p = arr;
printf("*(p+1):%d\n",*(p + 1));
那么这里为什么是 int *p = arr; 而不是 int *p = &arr; 呢?
因为类型不匹配,&arr 是指向整个数组的指针,如果要存它,必须写成 int (*p)5 = &arr;。
但是,我们平时遍历数组只需要元素指针,所以通常不用后者。
不过,我们平时遍历数组只需要指向元素的指针,所以通常用 arr 就够了。为了直观地验证"数组名 arr 在数值上等于首元素的地址",我们打印一下看看:
c
#include <stdio.h>
int main()
{
int arr[5] = {1,2,3,4,5};
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
我们发现数组名和数组首元素的地址打印出的结果一模一样,数组名就是数组首元素(第一个元素)的地址。
但是数组名有两个例外:
- sizeof(数组名) ------ 此时的数组名代表整个数组,返回整个数组占用的字节数。
- &数组名 ------ 取出的是整个数组的地址(类型为 int (*)N),虽然数值与首元素地址相同,但步长不同。
为什么说是两个例外呢?
因为数组arr我们知道数组名就是数组首元素的地址,地址使用sizeof计算时我们在数组计算大小的一章说过是按数组长度 * 数组类型大小 但是按照我们上一章的了解应该是按32/64位平台 的4/8字节 但是我们知道实际上不是这样的是按数组长度 * 数组类型大小
&数组名这个代码就能说明
c
#include <stdio.h>
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0] + 1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr + 1);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr + 1);
return 0;
}
这里我们发现&arr0和&arr0+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr0 和 arr 都是首元素的地址,+1就是跳过一个元素。
但是&arr 和 &arr+1相差40个字节,因为&arr是获取整个数组的地址 + 1当然会跳过整个数组。
那么为什么&arr打印的是首元素地址?
&arr 和 arr 虽然数值相同,但类型不同。可以把 arr 看作"一楼101室的门牌号",而 &arr 看作"整栋一楼的地址"。它们指向同一个物理位置,但 arr+1 走到102室(跳过4字节),&arr+1 则直接走到二楼(跳过整个数组40字节)。
使用指针访问数组
我们在上一章了解过 *(p + 1) 等价于 p1,那么我们结合刚才的了解此时在看指针访问数组就很简单就了解了。
c
#include <stdio.h>
int main()
{
int arr[10] = {0};
// 输入
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
// 输入
int *p = arr;
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);
// scanf("%d", arr+i);//也可以这样写
}
// 输出
for (i = 0; i < sz; i++)
{
printf("%d ", p[i]);
}
return 0;
}
一维数组传参的本质
在我们了解函数那一章节时说到过使用数组当参数时函数内部不能使用sizeof来计算数组大小,因为无法计算正常大小。
c
#include <stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
这就要了解数组传参的本质了,上个小节我们了解了:数组名是数组首元素的地址 ;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组首元素的地址。
所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写sizeof(arr) 计算的是一个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
冒泡排序
冒泡排序的核心思想就是:两两相邻的元素进行比较。
c
void bubble_sort(int arr[], int n) {
for(int i = 0; i < n-1; i++) {
int swapped = 0;
// j < n-1-i 是因为每轮排序后,末尾已排好序的元素个数增加1
// 所以内层循环的比较次数逐轮递减
for(int j = 0; j < n-1-i; j++) {
if(arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swapped = 1;
}
}
if(!swapped) break; // 已有序
}
}
int main()
{
int arr[] = {3, 1, 7, 5, 8, 9, 0, 2, 4, 6};
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
二级指针
我们知道指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
答案:二级指针。

对于二级指针的运算有:
- *ppa 通过对ppa中的地址进行解引用,这样找到的是pa , *ppa 其实访问的就是pa .
c
int a = 10;
int *pa = &a; // 一级指针
int **ppa = &pa; // 二级指针 -》等价于 *pa = &a;
- **ppa 先通过*ppa 找到pa ,然后对pa 进行解引用操作: *pa ,那找到的是a .
c
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
指针数组
指针数组是指针还是数组?
我们类比一下,整型数组,是存放整型的数组,字符数组是存放字符的数组。
那指针数组呢?是存放指针的数组。

那么指针数组的每个数组都是用来存放地址(指针)的
如下图:

指针数组的每个元素是地址,又可以指向一块区域。
指针数组模拟二维数组
我们在数组章节了解过二维数组本质就是多个一维数组组成的,那么只有我们让指针数组存放存放多个一维数组就可以用指针数组模拟二维数组,如下图:

c
#include <stdio.h>
int main()
{
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {2, 3, 4, 5, 6};
int arr3[] = {3, 4, 5, 6, 7};
// 数组名是数组首元素的地址,类型是int*的,就可以存放在parr数组中
int *parr[3] = {arr1, arr2, arr3};
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
实际上并非完全是二维数组,因为真正的二维数组在内存中所有行是连续紧挨的,而这里 arr1、arr2、arr3 三块内存不一定在物理上连续。
完