目录
- 一、数组名的理解
-
- [1. 数组名是数组首元素的地址](#1. 数组名是数组首元素的地址)
- [2. 两种特殊情况](#2. 两种特殊情况)
- 二、使用指针访问数组
-
- [1. 数组传参实际上是传递指针](#1. 数组传参实际上是传递指针)
- [2. 不能在被调函数中使用通过 sizeof 计算数组大小](#2. 不能在被调函数中使用通过 sizeof 计算数组大小)
- 三、冒泡排序
- 四、二级指针
-
- [1. 理解二级指针](#1. 理解二级指针)
- [2. 使用二级指针](#2. 使用二级指针)
- [3. 指针数组](#3. 指针数组)
- [4. 通过指针数组来模拟二维数组](#4. 通过指针数组来模拟二维数组)
一、数组名的理解
1. 数组名是数组首元素的地址
数组名实际上是数组首元素的地址,但是有两种特殊情况除外。下面通过代码来进行验证:
通过上述代码,我们发现不仅 arr 的值和 &arr[0] 的值相同,而且加 1 后的值也是一样的。这证明了 arr 等价于 &arr[0],都是 int* 类型,数组名实际上就数组首元素的地址。
2. 两种特殊情况
在下面两种特殊情况下,数组名代表整个数组:
(1)sizeof(数组名)
(2)&数组名
下面通过代码来进行验证:
通过上述代码可以看出,&arr 取出的是整个数组的大小,因加 1 加了 10 个 int 类型的大小;而 sizeof(arr) 计算的是整个数组的大小。
那么为什么 arr、&arr[0] 和 &arr 的值相同?因为取出的地址只是这块空间的首个字节的地址,而进行加减整数和解引用操作是在该地址所表示类型的基础上进行的。如下图:
二、使用指针访问数组
1. 数组传参实际上是传递指针
现在我们了解了数组名是数组首元素的地址,那么数组传参也就相应的有两种形式:数组传参和指针传参。如下代码:
c
// 打印数组元素
// 数组传参
void print1_arr(int arr[], int sz)
{
int i;
for (i = 0; i < sz; ++i)
printf("%d\n", arr[i]);
printf("\n");
}
// 指针传参
void print2_arr(int* pi, int sz)
{
int i;
for (i = 0; i < sz; ++i)
printf("%d\n", *(pi + i));
printf("\n");
}
现在通过这两种传参形式,来打印数组元素:
从上述代码中,我们可以发现一个问题,不管是数组传参还是指针传参,传递的都是数组名,而现在也不是两种特殊情况,所以数组名代表数组首元素的地址,传递给两个函数的都是数组 arr 首元素的地址。而在 C 语言中只有指针可以存储地址,所以的出结论数组传参实际上传的是指针。
实际上 arr[i] 等价与 *(arr+i),只不过数组使用下标更好理解与编写代码,而编译器最终还是会解释为 *(arr+i) 的形式。我们可以在指针传参中进行验证:
可以看到编译器并未报错,而运行程序照样能执行:
2. 不能在被调函数中使用通过 sizeof 计算数组大小
前面我们学习了可以使用 sizeof(数组名) / sizeof(数组首元素) 计算数组的大小,那是因为我们在创建数组的时候,编译器知道在当前函数中该数组名标识符代表一个数组,所以可以这样计算。但是当我们把数组名作为参数传给另一个函数时,实际上传递的是数组首元素的地址,也就是一个指针。那么再使用操作符 sizeof 进行计算时,得出的就是指针的大小。
下面通过代码进行验证上述结论:
可以看到第二条 printf() 语句打印的是 2,只是由于我当前使用的 x64 环境,地址占 64 位,也就是 8 个字节,而 arr[0] 是 *(arr+0) 也就是 int 类型,占 4 个字节,所以结果为 2。
三、冒泡排序
冒泡排序是一个比较简单实用且易懂的排序,冒泡排序的原理是:从前往后或者从后往前,相邻元素两两进行比较,如果不满足条件就交换。下面是示例:
我们可以看到上述排序排的是升序,如果前面的元素大于后面的元素就要进行交换。一共有 9 个数,进行了 8 次比较,最后最大的数据排在了最后,也就是说数字 9 已经在它应该在的位置上了。那么下一次就只需要排序前面 8 个数了,如果再重复第一次的步骤,那么 8 个数,进行 7 次比较,最后最大的数排在了最后,也就是数字 8 排在了倒数第二个位置。如此往复,一共需要进行 8 轮排序,因为 8 轮排序确定 8 个数的位置,剩下最后一个数就肯定在自己的位置。
接下来转换为一般情况,假设有 n 个数,那么需要进行 n-1 轮排序,第 i 轮排序需要进行 n - i 次比较。
那么如果转换成代码,就需要使用一个双层嵌套循环,外层控制排序轮数,内层控制比较次数。如下相应的冒泡排序函数 bubble_sort() 函数:
c
// 冒泡排序
void bubble_sort(int arr[], int sz)
{
int i;
// 外层排序轮数循环
for (i = 0; i < sz - 1; ++i)
{
int j;
// 内层比较次数循环
for (j = 0; j < sz - i - 1; ++j)
{
// 排升序,如果前面大于后面就交换
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
我们可以看到,外层循环条件 i < sz - 1,一共循环 sz - 1 次;内存循环条件 sz - i - 1,下一次循环比上一次要少比较一次,因为一轮循环确定一个数的位置。完全对应上面所简述的原理,现在我们来实践一下:
悬念: 上面所写代码只是最简单的冒泡排序,如果遇到下面的数据:
9 1 2 3 4 5 6 7 8
实际上当完成第一轮排序之后,该数据就已经有序:
1 2 3 4 5 6 7 8 9
而剩下的排序全都是无用功,白白浪费时间。改进的办法是通过一个额外变量,判断该轮排序之后是否有序,这就留给大家改进了。
四、二级指针
1. 理解二级指针
前面就说了指针是用来存放地址的,那么只要是变量,它就有属于它的内存空间,那么我们就可以通过取地址符(&)来取出这块内存空间的首字节的地址,也就是该变量的地址。
指针变量也是变量,它也有自己的内存空间,那么指针变量的地址存储在那里呢?前面说过指针是用来存储地址的,那么指针的地址应该也是存储在指针里面的,我们来回顾一下指针的创建方法:
c
int a = 10;
char c = 'c';
int *pa = &a; // 指向 int 的指针
char *pc = &c; // 指向 char 的指针
符号 * 代表该标识符是一个指针,然后去掉符号 * 和标识符剩下的就是指针所指向的对象的类型。同理,那么指向指针的指针应该就是按照如下方式创建的:
c
int a = 10;
int *pa = &a; // 指向 int 的指针
int **ppa = &pa; // 指向 int* 的指针
在 int **ppa = &a 表达式中,符号 * 是右结合的,所以等价于表达式 int *(*ppa) = &pa,而符号 * 代表 ppa 是一个指针,去掉 *ppa 之后剩下的 int* 就是指针 ppa 指向的类型,所以 ppa 是一个指向 int* 的指针,也就是指向指针的指针,叫作二级指针。
关系图如下:
2. 使用二级指针
如下代码:
c
int a = 10;
int *pa = &a; // 指向 int 的指针
int **ppa = &pa; // 指向 int* 的指针
我们知道 *pa 等价于 a,那么 *ppa 就等价于 pa,从而 **pa 就应该等价于 *pa,也就是 a。下面通过代码来进行验证:
通过上述代码的运行结果,可以看到我们的分析是正确的。
3. 指针数组
数组是一组相同类型的元素的集合,整型数组是存放整数的数组,浮点型数组是存放浮点数的数组,那么指针数组就应该是存放指针的数组。
(1)创建指针数组
如下代码:
c
int* arr_pi[10]; // int* 数组
首先,下标运算符的优先级高于解引用运算符,所以 arr_pi 先和下标运算符([])结合,那么 arr_pi 就是一个数组,该数组有 10 个元素,然后去掉 arr_pi[10] 剩下的就是数组元素的类型 int*,所以上面的代码创建了一个数组,该数组有 10 个元素,每个元素都是指向 int 的指针。
(2)理解指针数组的数组名
指针数组也是一个数组,那么它的数组名也是其首元素的地址,而它的首元素又是一个指针,所以数组名是一个指针的地址,也就是二级指针。
4. 通过指针数组来模拟二维数组
假设我们有三个数组:
c
// 创建 3 个数组
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 6,7,8,9,0 };
int arr3[5] = { 0,0,0,0,0 };
然后使用一个指针数组把这三个数组的首元素地址存起来:
c
// 使用指针数组存储三个数组的首元素地址
int* arr_pi[3] = { arr1, arr2, arr3 };
然后现在的关系就如下图:
那么 arr_pi[0] 就是数组 arr1 的首元素地址,arr_pi[0][0] 就是数组 arr1 的首元素。这样,我们就可以模拟实现二维数组了:
为什么是模拟实现?因为数组在内存中的存储是连续的,我们只是使用了 3 个指针来把这 3 个数组个联系了起来,而实际上在内存中它们的地址并不是连续的,如下代码:
通过程序的运行结果发现,这三个数组的地址之间相差 48 个字节,如果它们是连续的应该相差 20 个字节,也就是 5 个 int 的大小,所以这三个数组在内存中的存储是不连续的。