数组名的理解
在上⼀个章节我们在使⽤指针访问数组的内容时,有这样的代码:
cs
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
这⾥我们使⽤ &arr[0] 的⽅式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,⽽且
是数组⾸元素的地址,我们来做个测试。
cs
#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 = %p\n", arr);
return 0;
}
运行后你会发现,数组名和数组首元素的地址打印出来的结果是一模一样的呀,这就再次印证了数组名就是数组首元素(也就是第一个元素)的地址这一说法。
不过,这时候可能有的同学就会产生疑问啦。要是数组名是数组首元素的地址,那下面这段代码又该怎么理解呢?
cs
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));
return 0;
}
这段代码输出的结果是 40 呢。按之前说的,如果 arr
仅仅是数组首元素的地址,那输出的应该是 4 或者 8 才对啊。
其实数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:
- sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,单位是字节。
- 当用
&数组名
时,这里的数组名同样表示的是整个数组,取出的是整个数组的地址(要知道整个数组的地址和数组首元素的地址可是有区别的哟)。
除了这两个例外情况之外,在其他任何地方使用数组名时,数组名所表示的就是首元素的地址啦。
这时候呀,可能又有好奇的同学会进一步尝试下面这段代码呢。
cs
#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 = %p\n", arr);
printf("&arr = %p\n", &arr);
return 0;
}
你看,这三个打印结果又是一模一样的,这下可能又让人纳闷了,那 arr
和 &arr
到底有啥区别呀?咱们再来看下面这段代码以及它的输出结果哦
cs
#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;
}
输出结果如下
&arr[0] = 0077F820 &arr[0]+1 = 0077F824
arr = 0077F820
arr+1 = 0077F824
&arr = 0077F820
&arr+1 = 0077F848
从这里我们可以发现,&arr[0]
和 &arr[0]+1
相差 4 个字节,arr
和 arr+1
也相差 4 个字节,这是因为 &arr[0]
和 arr
都是首元素的地址呀,当进行 +1
操作时,就是跳过一个元素哦。而 &arr
和 &arr+1
相差 40 个字节呢,这就是因为 &arr
表示的是整个数组的地址,这里的 +1
操作意味着跳过整个数组哟。
讲到这儿,大家应该对数组名的意义比较清楚了吧。简单来说,数组名通常是数组首元素的地址,但有那两个特殊的例外情况哦。
使用指针访问数组
有了前面关于数组名相关知识的支撑,再结合数组自身的特点,我们就能很方便地使用指针来访问数组啦。来看看下面这段代码吧。
cs
#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;
}
把这段代码弄明白后,咱们再思考一个问题哈。既然数组名 arr
是数组首元素的地址,还可以赋值给 p
,在这里它们其实是等价的。我们知道可以用 arr[i]
来访问数组的元素,那 p[i]
是不是也可以访问数组呢?咱们再来看看下面这段代码哦。
cs
#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;
}
你瞧,在代码的第 18 行那里,把 *(p+i)
换成 p[i]
也是能够正常打印输出的哦。所以呀,本质上 p[i]
是等价于 *(p+i)
的呢。同理,arr[i]
也应该等价于 *(arr+i)
哦。在编译器处理数组元素访问的时候,实际上就是将其转换成首元素的地址加上偏移量求出元素的地址,然后再通过解引用的方式来访问元素的哟。
一维数组传参的本质
咱们已经学习过数组了,也知道数组是可以传递给函数的呀。那在这个小节呢,咱们就来探讨一下数组传参的本质到底是什么。
咱们先从一个问题入手哈,之前我们都是在函数外部去计算数组的元素个数,那能不能把数组传给一个函数后,在函数内部去求数组的元素个数呢?咱们看看下面这段代码以及它的运行情况哦。
cs
#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)
时,计算的其实是一个地址的大小(单位是字节),而不是数组的大小(单位字节)啦。正是因为函数的参数部分本质上是指针,所以在函数内部是没办法求得数组元素个数的哟。咱们再看看下面这两种函数定义形式哦。
cs
void test(int arr[]) //参数写成数组形式,本质上还是指针
{
printf("%d\n", sizeof(arr));
}
void test(int* arr) //参数写成指针形式
{
printf("%d\n", sizeof(arr)); //计算一个指针变量的大小
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
test(arr);
return 0;
}
总结一下哈,一维数组传参的时候,形参的部分既可以写成数组的形式,也可以写成指针的形式哟。
冒泡排序
冒泡排序的核心思想呢,就是让两两相邻的元素进行比较哦。下面给大家介绍两种实现冒泡排序的方法呢。
方法 1
cs
void bubble_sort(int arr[], int sz) //参数接收数组元素个数
{
int i = 0;
for(i=0; i<sz-1; i++)
{
int j = 0;
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;
}
}
}
}
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);
for(i=0; i<sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
方法 2 优化
cs
void bubble_sort(int arr[], int sz) //参数接收数组元素个数
{
int i = 0;
for(i=0; i<sz-1; i++)
{
int flag = 1; //假设这一趟已经有序了
int j = 0;
for(j=0; j<sz-i-1; j++)
{
if(arr[j] > arr[j+1])
{
flag = 0; //发生交换就说明,无序
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
if(flag == 1) //这一趟没交换就说明已经有序,后续无序排序了
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);
for(i=0; i<sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
二级指针
要知道呀,指针变量它本身也是变量呢,既然是变量那就有地址呀。那指针变量的地址存放在哪儿呢?
对于二级指针的运算呀,有下面这些情况哦:
-
*ppa
呢,是通过对ppa
中的地址进行解引用操作,这样就能找到pa
啦,所以*ppa
其实访问的就是pa
哦。比如下面这样的代码:csint b = 20; *ppa = &b; //等价于 pa = &b;
-
**ppa
呢,先是通过*ppa
找到pa
,然后再对pa
进行解引用操作,也就是*pa
,这样找到的就是a
啦。像下面这样:cs**ppa = 30; //等价于*pa = 30;
指针数组
那指针数组到底是指针还是数组呀?咱们可以类比一下哦,整型数组呢,是用来存放整型数据的数组,字符数组是存放字符的数组。那指针数组呀,就是存放指针的数组哦。
指针数组模拟二维数组
咱们来看看下面这段代码哈。
cs
#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;
}
在这里呀,parr[i]
是用来访问 parr
数组的元素哦,而 parr[i]
找到的数组元素指向了整型一维数组,那么 parr[i][j]
就是整型一维数组中的元素啦。不过要注意哦,上述代码虽然模拟出了二维数组的效果,但实际上它并非完全等同于二维数组呢,因为每一行的数据在内存中并非是连续存放的哟。
通过以上对数组名、指针访问数组、数组传参、冒泡排序以及二级指针、指针数组等多方面知识的详细讲解与探讨,相信大家对这些 C 语言中重要的知识点有了更深入且清晰的理解。希望大家在后续的编程学习与实践中,能够灵活运用这些知识,不断提升自己的编程能力哦。