目录
1 数组名的理解
2 指针访问数组
3 一维数组传参的本质
4 冒泡排序
5 二级指针
6 指针数组
7 指针数组模拟实现二维数组
1 数组名的理解
上一篇文章提及,数组名是首元素的地址,可是,不管什么情况都是这样的吗?
先看一串代码。
int main(){int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p1 = arr;int* p2 = &arr[0];printf("%p\n", p1);printf("%p\n", p2);return 0;}
打印地址的时候,数组名确实是首元素地址,结果一样。
那么既然数组名是首元素地址,是不是说sizeof(arr)的大小是4 / 8呢?
结果是整个数组的大小,那么就说明了sizeof()里面的数组名不是首元素地址的含义,代表的是整个数组。
数组名是首元素地址的说法也是对的,只是有两个特例。
一个是sizeof(数组名),代表的是整个数组的大小。
一个是&数组名,代表取出整个数组的地址。
需要区分的是,取出整个数组的地址与取出首元素地址是不一样的,具体差别看这段代码。
int main(){int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };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 + 1打印的是数组最后一个元素的地址的后一块空间,arr + 1是跳过了一个整型,&arr + 1是跳过了整个数组,这就是取出了首元素地址和取出了整个数组地址的区别。
总结一下,数组名不是首元素地址的情况只有两种,遇到sizeof(数组名)和&数组名 的时候,这两个情况应该多加注意。
2 指针访问数组
有了前面知识的铺垫,我们就可以通过指针来访问数组了。比如实现输入,实现打印等操作。
int main(){int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = arr;for (int i = 0; i < 10; i++){printf("%d ", *(p + i));}return 0;}
p存首元素的地址,利用解引用操作,得以访问数组。
那么这里我就提问了,平常的写法是
printf("%d ",arr[i]);
arr是数组名,现在也不是两种特殊情况,那么可不可以理解为指针[i]呢?毕竟arr代表的就是首元素地址,那么我们可以写成p[i]吗?答案是肯定的。
注意一点就是这里就不需要解引用操作符了。
那么我又又又提问了,刚才的写法是*(p + i),可以代替为p[i],那我可以写成*(I + p)吗?肯定可以,那么我可以写成i[p]吗?试试。
哦吼,看来可以,那么i[arr]可以吗?当然也是可以的。
但是实际上,i[p],i[arr],都是比较少见的写法,我们权当了解一下就行了,平时还是写*(p + i),或者p[i],这两个写法是常见的写法。
3 一维数组传参的本质
实际上在扫雷的篇章的时候,我们就已经了解了一维数组传参传的是首元素地址,这里就不再多解释了,来两段代码,看看能不能找出差异,如果能,说明你就了解了。
void test(int arr[]){for (int i = 0; i < sizeof(arr); i++){printf("%d ", arr[i]);}}int main(){int arr[] = { 1,2,3,4,5,6,7,8,9,10 };test(arr);return 0;}
void test(int arr[],int sz){for (int i = 0; i < sz; i++){printf("%d ", arr[i]);}}int main(){int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);test(arr,sz);return 0;}
4 冒泡排序
了解了一维数组传参的本质,我们现在来了解一下冒泡排序。
冒泡嘛,泡都是一个一个冒的,排序也是一个一个排的,冒泡排序的本质思想就是,两两相邻的元素进行比较。从左到右依次比完为1趟,一趟下来就能确定某个元素的相对位置,冒泡要么就是升序排列要么就是降序排列,所以每趟下来能暂时确定相对位置。
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;}
我们写一个冒泡排序的函数,通过一维数组传参的方式,使用地址修改该数组,传过去的是首元素地址,所以我们选在主函数的位置把数组的元素个数传过去。
在第一个for循环里面,我们确定的是总共需要多少趟就能排列完成,假定一个数组有10个元素,我们只需要排列9次就可以,因为冒泡排序是两两比较的。
在第二个for循环里面,我们确定的是在该趟数下,需要比较多少次,可能你会好奇为什么是j < sz - 1 - i,实际上,你也可以不用选择- i,这样你比较的话就是老老实实的从第一个元素开始,每个都进行一次比较,但是实际上,如前面所说,每趟都能确定某些元素的相对位置,也就是说,随着趟数的增加,实际需要比较,换位置的元素是越来越少的。所以我们需要比较的次数就随之递减,因为每次能至少确定一个元素的相对位置,所以每次比较的次数 - 1,所以代码是j < sz - 1 -i。
if里面就是两个变量交换的步骤,不用过多介绍。
但是实际上,某些极端情况确实就需要满满的比较上9趟,大多数情况下,可能在第5趟就比较完了,可能第一趟就比较完了,甚至可能都不需要比较,那么我们如何优化呢?
优化的点就是,如何确定在一趟中有没有进入到交换变量的if里面去,所以这里有一个常用的方法,标志变量,这样使用的。
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;}
可以看到,我们多加了一个变量,flag,假定这一趟已经有序了,那么flag就不变,在判断flag的if里面就break,跳出循环了,如果进入的交换变量的循环,那么flag就改变,第二趟的时候flag的值重新变为1,进入第二趟的循环,判断。
当然,这里有个易错点就是flag的位置,容易放在最外层那个for循环的外面,这样flag就不能重新回到1了,即便某一趟有序,还是要老老实实的比较,就比较浪费时间。
5 二级指针
我们说一个变量,在内存中存储的时候就会有自己的“房间”,有自己的“门牌号”,那么指针变量也是变量,它也会有自己的地址,我们用另一个指针变量来存储这个指针变量,新的存储指针变量的指针变量就被称为二级指针。
int main(){int num = 10;int* pa = #int** pb = &pa;return 0;}
这里的pb就是二级指针,就会有人问了,会不会有三级指针,四级指针,以至于100级指针,当然是有点,但是实际生活中,三级指针都很少用到了,二级指针是比较常见的,所以包括三级往上,我们称为多级指针。
你看,二级指针有两个*,一级指针有一个*,所以不完全归纳法可以知道,多少级指针就有多少个*。
我们现在就先简单介绍一下二级指针的概念,具体使用放到后面介绍.。
这里来一段代码过过瘾就行
int main(){int num = 10;const int* const p1 = #int** p2 = &p1;**p2 = 20;printf("%d ", num);return 0;}
6 指针数组
首先提个问题,指针数组是数组还是指针。
想不清楚没关系,看这个。
int arr[10] = { 0 };
这是数组吧?是什么数组?整型数组吧?那你说指针数组是指针还是数组?
当然是数组了,只是里面存放的是指针而已。
那这个就厉害了,我们调用这个数组的时候,可以通过访问这个数组来使用里面存放的指针,这么说你可能不大明白,但是剧透一下,函数名也是地址,多的就不透露了,后面会提到,
先看一下指针数组,模拟实现二维数组吧。
#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;}
先看看效果图。
我们定义三个整型数组,一个指针数组,指针数组里面存放的是三个整型数组的数组名,第一个for循环用来确定调用几次指针数组,第二个for循环用来确定调用几次整数数组的数组名。
可能唯一比较绕的点就是parr[i][j],不妨这样来看,parr[1]就是arr1,然后就是arr1[j],这样够明了的吧,当然这里用解引用操作的话也行,但是我认为没有方括号看着顺眼,
*(*(parr + i) + j),这就是解引用的写法,看起来可能比较绕,*(parr + i)就是arri,再解引用一下就是arri[j]了,不理解的话,再看几遍!
感谢阅读!