目录
- 1.数组名理解
- [2. 使用指针来访问数组](#2. 使用指针来访问数组)
- [3. 一维数组传参的本质](#3. 一维数组传参的本质)
- 4.冒泡排序
-
- [4.1 讲解算法原理](#4.1 讲解算法原理)
- [4.2 核心代码实现](#4.2 核心代码实现)
- [5. 二级指针](#5. 二级指针)
- 6.指针数组
- 7.指针数组模拟二维数组
1.数组名理解
之前我们用指针来访问数组时,有以下的代码:
c
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
我们使用了&arr[0]来访问数组第一个元素的地址,但是数组名本来就是地址,而且是数组首元素的地址。
c
int main()
{
//数组名是数组首元素的地址
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", &arr[0]);
printf("%p\n", arr);
printf("%d\n", *arr);
return 0;
}
这段代码的结果:

我们发现数组名数组首元素的地址打印出来的结果一摸一样,数组名就是数组首元素(第一个元素)的地址。
我们再来看一段代码:
c
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%zu\n", sizeof(arr));
return 0;
}
输出的结果是:40, 假设数组名是数组首元素的地址,那应该是4 / 8 个字节才没问题。
数字名就是数组首元素的地址是对的,但是有两个例外:
- sizeof(数组名),sizeof中单独放是数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
- &arr ,这里的数组名表示整个数组,表示的是整个数组的地址。
除此之外,任何地方使用数组名,数组名都表示数组首元素的地址。
我们再看一下这段代码:
c
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", &arr[0]);
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
打印出的结果:

三个打印出的结果又一样,arr和&arr到底有什么区别?
我们来看一下下面的代码:
c
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0] + 1);
printf("%p\n", arr);
printf("%p\n", arr + 1);
printf("%p\n", &arr);
printf("%p\n", &arr + 1);
return 0;
}
输出的结果:

虽然 &arr 和 arr 打印出来的地址的编号可能一样,都是数组的起始地址,但它们的含义不同。
| 表达式 | 含义 | +1 后移动距离 |
|---|---|---|
arr |
首元素地址 | 跳过 1 个元素 |
&arr[0] |
首元素地址 | 跳过 1 个元素 |
&arr |
整个数组的地址 | 跳过整个数组 |
它们的地址值可能一样,但指针类型不同,所以 +1 后移动的距离也不同,下面我们通过一张图片的方式来对三者进行对比:

2. 使用指针来访问数组
结合前面的知识,我们就可以用指针来访问数组了。
c
int main()
{
int arr[10] = { 0 };
int* p = arr; //arr就是数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
//给数组赋值为1-10
for (int i = 0; i < 10; i++)
{
*(p + i) = i + 1; //访问下标为i的元素
//也可以这么写*(arr + i) , p[i] , arr[i]
}
for (int i = 0; i < 10; i++)
printf("%d ", *(p + i));
return 0;
}

3. 一维数组传参的本质
数组是可以传递给函数的,于是我们就来讨论一下数组传参的本质。
我们之前都是在函数外部计算数组元素,我们可以把数组传给一个函数后,函数内部求数组的元素个数吗?
c
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;
}
输出结果:

通过上述代码我们发现,在函数内部没有正确获得数组的元素个数。
我们来分析一下代码:
虽然参数写成了 int arr[],但在函数内部它本质上等价于:
c
int* arr
也就是说,arr 实际上是一个 指针 。在函数内部:sizeof(arr)计算的是 计算的是⼀个首元素地址的⼤⼩(单位字节) ,不是整个数组的大小(单位字节)。因为此时 arr 已经不是完整数组,而是指向首元素的指针。从本质上来说,数组传参传递的是数组首元素的地址。
为什么sz2 = 2,而不是1呢?
数组传参后,函数里的 arr 是指针;(X64)中指针占 8 字节,int 占 4 字节,所以 sz2=2。
指针变量的大小相关知识点可点击下面的链接:C语言指针深入浅出1。

总结:一维数组传参,形参的部分可以写为数组的形式,也可以写成指针的形式。
4.冒泡排序
4.1 讲解算法原理
冒泡排序的核心思想是:两两元素相互比较。
下面我们来举个例子,假设一个arr数组中有这样一堆数:
c
int arr[10] = {9,8,7,6,5,4,3,2,1,0};
我们就可以有这样的算法思想,每一趟处理一个数字,将当前数字与其他未排序元素进行比较,最大的放在最右边(默认是升序)。

4.2 核心代码实现
c
void bubble_sort(int arr[], int sz)
{
for (int i = 0; i < sz - 1 ; i++) //确定趟数
{
for (int j = 0; j < sz - 1 - i; j++) //处理每一趟内部的元素
{
if (arr[j] > arr[j + 1]) //默认处理为升序
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
for (int i = 0; i < sz; i++)
printf("%d ", arr[i]);
return 0;
}
思考一下,对于某些接近有序的数组,传统冒泡排序仍然会继续执行剩余趟数,造成不必要的时间开销,比如看一下这个数组:
c
int arr[10] = { 9,0,1,2,3,4,5,6,7,8 };
当第一趟交换完之后,数组就有序了,可以设置一个标记变量,如果某一趟排序过程中没有发生任何交换,说明数组已经有序,可以提前退出循环,这样就可以极大的节省时间成本。
优化过后的数组:
c
void bubble_sort(int arr[], int sz)
{
for (int i = 0; i < sz - 1; i++) //确定趟数
{
int flag = 1; //设置一个标记来判断,假设本趟没有发生交换
for (int j = 0; j < sz - 1 - i; j++) //处理每一趟内部的元素
{
if (arr[j] > arr[j + 1]) //默认处理为升序
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = 0; //发生了交换,说明数组还不一定有序
}
}
if (flag == 1)
break; //本趟没有交换,数组已有序,提前结束
}
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
for (int i = 0; i < sz; i++)
printf("%d ", arr[i]);
return 0;
}
5. 二级指针
指针变量也是变量,变量也有地址,变量的地址存放在二级指针中了。
c
int main()
{
int a = 0;
int* pa = &a; //一级指针
int** ppa = &pa; //二级指针(用来存储一级指针变量的地址)
return 0;
}

对二级指针运算有:
c
int main()
{
int a = 0;
int* pa = &a;
int** ppa = &pa;
*ppa -> ppa 存放的是 pa 的地址,所以* ppa 访问的是 pa。
**ppa -> **ppa 等价于*pa,最终访问的是 a 的值。
return 0;
}
6.指针数组
我们进行指针数组学习之前类比一下,整型数组,存放的是整型的数组,字符数组,存放的是字符的数组,那么,指针数组就是存放指针的数组,数组的每一个元素类型都是指针类型。

指针数组中的每个元素都是用来存放地址(指针)的,指针数组每个元素是地址,可以指向一块区域,这样就可以用来模拟二维数组了。
7.指针数组模拟二维数组
c
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
//数组名是数组首元素的地址,类型是int*类型的,就可以存放在arr数组中
int* arr[] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", *(arr[i] + j)); //j就是偏移量
//也可以这么写arr[i][j]
}
printf("\n");
}
return 0;
}

arr[i] 访问的是指针数组中的第 i 个元素,这个元素保存的是某个一维数组的首地址;因此 arr[i][j] 就可以访问对应一维数组中的第 j 个元素。这种方式可以模拟二维数组的访问效果,但本质上并不是真正连续的二维数组,因为每一行数据可能存放在不同的内存位置,而在二维数组中,每个数组元素的地址都是连续的 。

完