1. 数组名的理解
在上一个章节我们在使用指针访问数组的内容时,有这样的代码:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
这里我们使用&arr[0] 的方式拿到了数组第一个元素的地址,但是其实数组名本来就是地址,而且是数组首元素的地址,我们来做个测试。
#include <stdio.h>
int main()
{
int arr[10] = {0};
printf("arr = %p\\n",arr);
printf("&arr[0]= %p\\n",&arr[0]);
return 0;
}
我们发现数组名和数组首元素的地址打印出的结果一模一样,数组名就是数组首元素(第一个元素)的地址。
这时候还有两个例外:数组名如果是数组首元素的地址,那下面的代码怎么理解呢?
#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才对。
这就是两个例外:
1.sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)
除此之外,任何地方使用数组名,数组名都表示首元素的地址。
#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]和&arr[0]+1相差4个字节,arr和arr+1相差4个字节,是因为&arr[0]和arr都是首元素的地址,+1就是跳过一个元素。
但是&arr和&arr+1相差40个字节,这就是因为&arr是数组的地址,+1操作是跳过整个数组的。
到这里大家应该搞清楚数组名的意义了吧。数组名是数组首元素的地址,但是有2个例外。
2. 使用指针访问数组
有了前面知识的支持,再结合数组的特点,我们就可以很方便的使用指针访问数组了。
1. 数组在内存在中是连续存放的
2. 指针的加减整数运算方便获得每一个元素的地址
3. 数组是一块连续的空间其大小和数组元素个数和类型有关
4. 指针变量是一个变量(4/8)个字节
5. 数组名是地址是首元素的地址
6. 可使用指针访问数组
#include <stdio.h>
int main()
{
int arr[10]={0};
//使用指针访问数组
//输入10个值
int* p = arr;//p指向数组的第一个元素
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0;i<sz;i++)
{
//一次循环输入一个值
scanf("%d",p+i);//p+i == &arr[i]
}
//输出10个值
for(i=0;i<sz;i++)
{
printf("%d ",*(p+i));//p+i得到的是地址需要解引用
}
return 0;
}

数组名arr是数组首元素的地址,可以赋值给p,其实数组名arr和p在这里是等价的。那我们可以使用arr[i]可以访问数组的元素,那p[i]是否也可以访问数组呢?
#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;
}
在代码printf("%d ", p[i]);处将p[i] 换成 * (p+i)也是能够正常打印的,所以本质上p[i]是等价于 * (p+i)。
同理arr[i] 应该等价于 * (arr+i),数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的。
3. 一维数组传参的本质
首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给⼀个函数后,函数内部求数组的元素个数吗?
#include <stdio.h>
void Print(int arr[10])
//数组传参的时候形参可以写成数组的形式但本质上还是指针
//也就是int arr[10] == int *arr
//所以这时候sizeof(arr)不是求一个数组的大小求的是指针变量的大小X86环境下为4字节
//sizeof(arr[0]表示一个元素的大小也是4字节
//所以4/4==1
{
int sz = sizeof(arr)/sizeof(arr[0]);//错误
int i =0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
Print(arr);
return 0;
}

调试看到sz计算为1因为i=0 ,sz为1,所以循环只进行了一次也就只打印了1,这是因为sz算错了为什么会算错呢?
这就要学习数组传参的本质了,上个小节我们学习了:数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组首元素的地址。
所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写sizeof(arr) 计算的是一个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
结论:
1. 数组传参的本质是传递了数组首元素的地址,所以形参访问的数组和实参访问的数组是同一个数组
2. 一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
3. 形参的数组是不会单独再创建数组空间的,所以形参的数组是可以省略数组大小的:int arr[10] == int arr[ ]
4.冒泡排序
冒泡排序是一种基础的比较排序算法,其核心思想是反复交换相邻的逆序元素,将较大的元素逐渐“浮”到数组末端。
算法步骤:
1. 从第一个元素开始,比较相邻的两个元素。
2. 如果顺序错误(前一个大于后一个,以升序为例),则交换它们。
3. 对每一对相邻元素重复此操作,直到数组末尾。这样一轮结束后,最大元素会“冒泡”到最后一个位置。
4. 重复上述步骤,每次忽略已经排序好的末尾部分,直到整个数组有序。
#include <stdio.h>
void bubble_sort(int arr[], int sz)
{
//先确定趟数
int i = 0;
for(i=0;i<sz–1;i++)//如果有n个元素那么需要进行n-1次排序
{
//一趟排序的过程
int j = 0;//表示下标
for(j=0;j<sz–1–i ;j++)//j<sz-1-i:每完成一轮,末尾的 i 个元素已经是有序的最大值,无需再比较,进一步减少循环次数;
{
if(arr[j] > arr[j+1])//如果前一个元素大于后一个元素就交换
{
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
void Print(int arr[],int sz)
{
int i =0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
}
int main()
{
int arr[] = {9,8,7,6,5,4,3,2,1,0};
//排序
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr,sz);
//打印
Print(arr,sz);
return 0;
}
优化版本:如果数组本身就是接近有序的情况下比如:9,0,1,2,3,4,5,6,7,8, 这样只需一趟冒泡排序即可但上面的代码不支持所以下面给出优化版本。
#include <stdio.h>
void bubble_sort(int arr[], int sz)
{
//先确定趟数
int i = 0;
for(i=0;i<sz–1;i++)//如果有n个元素那么需要进行n-1次排序
{
//每次进入假设已经有序
int flag = 1;
//一趟排序的过程
int j = 0;//表示下标
for(j=0;j<sz–1–i ;j++)//j<sz-1-i:每完成一轮,末尾的 i 个元素已经是有序的最大值,无需再比较,进一步减少循环次数;
{
if(arr[j] > arr[j+1])//如果前一个元素大于后一个元素就交换
{
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
flag = 0;//只要发生一次交换,说明数组还没完全有序
}
}
if(flag == 1)//如果一趟下来没有交换,说明数组已经完全有序,提前结束
{
break;
}
}
}
void Print(int arr[],int sz)
{
int i =0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
}
int main()
{
int arr[] = {9,8,7,6,5,4,3,2,1,0};
//排序
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr,sz);
//打印
Print(arr,sz);
return 0;
}
第二份代码是优化版本的冒泡排序,相比第一份代码:
1. 增加了提前结束机制,提高了效率
2. 代码结构更清晰
3. 在实际应用中性能更好
5. 二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?这其实就是二级指针
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;//p一级指针
int** pp = &p;//pp就是二级指针
printf("%p\\n",*pp);
printf("%p\\n",&a);
}
6.指针数组
指针数组是指针还是数组?
我们类比一下,整型数组,是存放整型的数组,字符数组是存放字符的数组。
那指针数组呢?是存放指针的数组。


指针数组的每个元素都是用来存放地址(指针)的。指针数组的每个元素是地址,又可以指向一块区域
7. 指针数组模拟二维数组
#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数组中,此时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]就是整型一维数组中的元素。
上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的


