数组名的理解
前面,我们讲过,数组名就是数组首元素的地址。今天,我们就来进行详细的讲解。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int* pp = &arr[0];
printf("%p\n", p);
printf("%p\n", pp);
return 0;
}
结果运行图:
我们从结果运行图中,可以看出来,数组名的地址和数组首元素的地址相同。所以,数组名就是首元素的地址(数组名就是地址)。接下来,我们再写一个代码(能颠覆你三观的代码),进行代码展示:
// 写这样的代码:计算 sizeof(arr)的值。
// 这个代码咱们前面都见过,比如,int sz = sizeof(arr) / sizeof(arr[0])
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%zd\n", sizeof(arr));
return 0;
}
结果运行图:
我们之前讲过这个代码,sizeof(arr)
测的是整个数组的大小,这样想很合情理。但是,我们讲过了,数组名就是地址,地址的空间大小跟数据类型无关,只与计算机平台有关,32位平台小为4个字节,64位平台下为8个字节。而sizeof(arr)
不就是在测地址的字节吗?结果不就是4或者8吗?为什么会40呢?自相矛盾?数组名就是首元素的地址,这个说法没有错,只不过凡事都有例外——在sizeof
和&
这两个操作符是不适用的。除了这俩货,数组名就是首元素的地址这个规则在其它地方都能用(都成立)。sizeof咱们已经讲过了,接下来,咱们讲讲&
这个例外。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf(" arr=%p\n", arr);
printf("&arr[0]=%p\n", &arr[0]);
printf(" &arr=%p\n", &arr);
return 0;
}
结果运行图:
对于arr
和&arr[0]
的地址相同,我们没什么可惊讶的。但是,&arr
的地址竟然也和它们俩相同,这就很惊讶。我们都知道&取地址操作符
,而**&arr就是取的整个数组的地址,难道整个数组的地址=数组名=数组首元素的地址吗?当然不是,虽然&arr
的数值和它们俩相同,但是它和它们俩本质上是不同。 ** arr
和&arr[0]
是可以互用的,但是&arr
不能与它们俩互用。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf(" arr=%p\n", arr);
printf(" arr+1=%p\n", arr+1);
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);
return 0;
}
结果运行图:
我们可以发现,arr+1
和&arr[0]+1
跳过四个字节(原因:在前面的指针运算讲过)。而 &arr+1跳过了40个字节,就是跳过了整个数组。 在指针运算的那个章节里面,咱们讲过指针运算跨过多少个字节取决于数据类型,而 &arr+1的数据类型肯定不同(因为它跨过了整个数组),具体是什么类型,咱们后面讲。这就是它和它们的区别,虽然它和它们在数值上相同,但本质上是不同。
使用指针访问数组
有了前面的知识铺垫,我们就可以使用指针对数组进行访问了。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
printf("更改前");
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
for (i = 0; i < sz; i++)
{
*(p + i) = -1;
}
printf("\n");
printf("更改后");
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
结果运行图:
我们知道p和arr是等价的(本身p是由arr赋值而来的,所以肯定相等)。前面,我们访问数组的时候,都是用arr[i]来进行访问。既然,p和arr是等价的,那么我们能否写成p[i]来进行访问呢?答案是肯定的,进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", p[i]);
}
return 0;
}
结果运行图:
大家能看的出来,结果是一样的。看到这里,我们再来进行拓展一下。前面,我们用过arr[i] ,p[i]和*(p+i)对数组进行访问,我们会发现访问的结果是一样的。那么它们本质上是不是一回事呢?答案:本质上是一样的。 在访问数组时,编译器会把arr[i]转换成 *(p+i)进行编译。因为arr[i]这种格式很符合人们的认知,所以C语言中就规定了以这种方式对数组进行访问 . *(p+i)才是本质。总结:arr[i] = *(p+i)= *(arr+i)。这里的p只是举个例子,换成什么都OK。
一维数组传参的本质
前面,我们访问数组的时候都是在main函数里面进行访问。那么,今天我们就创建个子函数,在这个子函数里面进行访问。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void Printf(int arr[])
{
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz;i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
Printf(arr);
return 0;
}
结果运行图:
我们把给函数传的是数组,我们理所应当就该给Printf函数的参数类型为数组形的。我们既然把数组给传过去了,那么我们就应该能访问整个数组呀,为什么只访问了1个元素呢?原因很简单,前面咱们讲过——数组传参,传的是数组首元素的地址。也就是说,arr传过去的并不是整个数组,而是传过去一个地址。前面,我们讲过,地址的空间大小只与计算机平台有关。在32位平台下为4个字节,又因为int 也是4个字节。所以,sz的结果为1,即arr[0]就只访问了一个元素。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void Printf(int arr[])
{
int sz = sizeof(arr) / sizeof(arr[0]);
printf("sz=%d ", sz);
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
Printf(arr);
return 0;
}
结果运行图:
结果与我们分析的是一样的。接下来,我们对代码进行更改:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void Printf(int arr[],int sz)
{
int i = 0;
for (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]);
Printf(arr,sz);
return 0;
}
结果运行图:
在前面,我们讲了,数组传参传的是数组首元素的地址。那么,我们可以把子函数的参数写成指针(因为传过去的是地址)。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void Printf(int*p,int sz)
{
int i = 0;
for (i = 0; i < sz;i++)
{
printf("%d ", p[i]);
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
Printf(arr,sz);
return 0;
}
结果运行图:
我们发现效果是一样的,也就是说 int arr []
和int *p
是一样的。确实是一样的,int *p
是int arr[]
的本质。因为当我们写成int arr []
时,是很自然而然地就能想到的(因为我们传过去的是数组,自然而然就用数组来接收)。其实,编译器也会把int arr []
转换成int *p
来进行识别和处理的。因为我们传过去的是地址,不是数组,所以子函数的形参部分是不会创建数组的,所以```[ ]``里面的数字可省略不写。
冒泡排序
我们来写个程序,对数据进行排序。对数据进行排序的算法有很多,比如,1.冒泡排序 2.插入排序 3.选择排序 4.快速排序 5.归并排序。今天,我们来用冒泡排序。冒泡排序的核心思想:两两相邻的数据进行比较。
// 我们随机输入5个数字,而且还是乱序的。最后我们得到从小到大的排序。
// 比如,输入:1 ,4, 6 ,5 ,9
// 输出 :1 ,4 ,5 ,6 ,9
进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void change(int arr[], int sz) //写成指针也是可以
{
int x = 0; //趟数
int y = 0; //排序对数
for (x = 0; x < sz - 1; x++) //趟数
{
for (y = 0; y < sz - 1 - x; y++) //排序对数
{
if (arr[y] > arr[y + 1])
{
int c = arr[y + 1];
arr[y + 1] = arr[y];
arr[y] = c;
}
}
}
}
int main()
{
int arr[5] = {1,2,3,4,5};
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
scanf("%d", &arr[i]);
}
printf("排序前:");
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
printf("排序后:");
change(arr, sz);
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
结果运行图:
这个代码不够完善,需要优化。因为当我们输入9 ,1 ,2 ,3 ,4
时,只需要一趟就能排完,但是按照上面的代码就要重复排序,代码执行效率不高。优化后的代码如下所示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void change(int arr[], int sz) //写成指针也是可以
{
int flage = 1; //假设一趟排完
int x = 0; //趟数
int y = 0; //排序对数
for (x = 0; x < sz - 1; x++) //趟数
{
for (y = 0; y < sz - 1 - x; y++) //排序对数
{
if (arr[y] > arr[y + 1])
{
flage = 0;
int c = arr[y + 1];
arr[y + 1] = arr[y];
arr[y] = c;
}
}
if (flage == 1)
break;
}
}
int main()
{
int arr[5] = { 1,2,3,4,5 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
scanf("%d", &arr[i]);
}
printf("排序前:");
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
printf("排序后:");
change(arr, sz);
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
二级指针
前面,咱们讲过,指针变量也是变量。创建变量的本质就是向内存申请空间,既然申请了空间,那就有这个空间的地址。那么,我们就可以取出来这个指针变量的地址。如图所示:
我们创建了int a =10
,然后我们又创建了指针变量int *p=&a
。前面,咱们已经讲过了int *
的意义(对它的解读)。我们创建了一个指针变量(二级指针),用来存放指针变量的地址,而指针变量的类型为int *
,又因为我们创建的变量来存放指针变量的地址,所以它也是一个指针变量,即int **ppa=&pa
。按照这个思路,我们还可以创建三级,四级,……N级指针。二级指针往上,我们就不常用了。当我们对其指针所指的变量进行数据访问时,我们要进行多次*
操作。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;
int* *ppa = &p;
printf("更改前:a=%d\n", a);
*(*ppa) = 30; //要加()
printf("更改后:a=%d\n", a);
return 0;
}
结果运行图:
指针数组
指针数组是指针呢?还是数组呢?在回答这个问题之前,我们先类比一下。
// 整形数组:存放的元素是整形类型的数据
// 字符数组:存放的元素是字符类型的数据
| |
| |
| |
// 指针数组:存放的元素是指针(地址)类型的数据
所以指针数组是存放指针(地址)的数组。如图所示:
指针数组模拟二维数组
我们学了指针数组,那么我们就用指针数组模拟二维数组。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 5,4,3,2,1 };
int arr3[] = { 0,0,0,0,0 };
int* arr[] = { arr1,arr2,arr3 };
int i = 0;
int j = 0;
int sz = sizeof(arr) / sizeof(arr[0]); //指针数组的元素个数
int sz1 = sizeof(arr1) / sizeof(arr1[0]); //整形数组的元素个数
for (i = 0; i < sz; i++)
{
for (j = 0; j < sz1; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
结果运行图:
我们在这里只是模拟二维数组,并不是真正的而数组。因为真正的二维数组是在同一个连续的空间的。我们这三个数组分明不在同一个空间,所以只是模拟。
数组名的理解返场:
开篇咱们讲了数组名的理解,里面有个遗留的问题——&arr+1
为什么能跳过整个数组呢?它的数据类型到底是什么?今天我们就讲解一下。
// int a=10;
// int * p=&a; //因为a是整形,所以就用 int *
// char b='w';
// char *pp=&b; //因为b是字符形,所以就用char *
对于数组,同理得到:
// int arr[10];
// int *arr1[10] =&arr; //因为arr是数组类型,所以就用数组类型——int arr [10]
// 然后再加个*,代表存放地址。
? ? ? ? ? ?
但事实就是这样的吗? 没有什么疑问吗?
int *arr1[10] =&arr
并不是整个数组的地址,这不就是咱们上面讲的指针数组吗,指针数组是用来存放数组的地址的(也就是数组名----数组首元素的地址),并不是存放&arr整个数组的地址。我们只需要这样改一下就OK了——int( *arr1)[10] =&arr
。(*arr1)
说明arr1
是个指针变量,然后(*arr1)
和[10]
结合,表明所指的类型为数组。最后,(*arr1)
和[10]
与int
结合,表明所指的数组为整形数组。所以arr1的数据类型为int( *)[10]
,这也就是为什么&arr+1
跳过40个字节了。总结:当我们接收整个数组的地址时,数据类型为type (* name) [元素个数]
。
彩蛋时刻!!!
https://www.bilibili.com/video/BV15b42177rL/?spm_id_from=333.337.search-card.all.click&vd_source=7d0d6d43e38f977d947fffdf92c1dfad
每章一句:学习也是如图片里的跑步一样。
感谢你能看到这里,点赞+关注+收藏+转发是对我最大的鼓励,咱们下期见!!!