深入理解指针(2)–新手小白都能明白的指针解析
文章目录
前言
OK啊,我们也是在五一假期结束之前,又见了一面,我挺开心的,不知道你们开心吗
好了,回归正题,我相信你们在深入理解指针(1)中已经多多少少了解到了指针,接下来,我们再认识指针吧
在这里,你会学习到 对数组名的解释
用指针访问数组
… 还有很多好玩的知识
那么正片开始喽
1. 数组名的理解
在深入理解指针(1)中我们在使用指针访问数组的内容时,有这样的代码:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
在这个代码里我们是通过对数组的第一个元素进行取地址操作(&),来获得了数组第一个元素在内存中的地址
我们再来做个测试
#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;
}
我们发现数组名和数组首元素的地址打印出的结果⼀模⼀样,数组名就是数组首元素(第⼀个元素)的地址
那我们说如果数组名是数组首元素的地址,那么它是不是应该和数组首元素地址一样也是4个字节呢
数组名如果是数组首元素的地址,那下面的代码怎么理解呢:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%zd\n", sizeof(&arr));
printf("%zd\n", sizeof(arr));
return 0;
}
哎,为什么两个数好像没有按照我们的想法来输出啊,这是为什么,那是不是数组名根本就不是数组首元素的地址啊
其实数组名就是数组首元素(第⼀个元素)的地址是对的,但是有两个例外:
- sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
- &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)
除此之外,任何地方使用数组名,数组名都表示首元素的地址
这时有困惑的同学,再试⼀下这个代码:
#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[0]
arr
&arr
指向的都是首元素的地址
画个图就能更好理解
三个打印结果⼀模⼀样,这时候我们又纳闷了,那arr和&arr有啥区别呢
接下来让我们看一下下面的一段代码:
#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;
}
在X86的输出结果如下:
我们说 &arr[0]
arr
&arr
指向的都是首元素的地址
为什么 +1 之后又会不一样呢
通过输出结果,我们会发现
- &arr[0]和&arr[0]+1相差4个字节
- arr和arr+1 相差4个字节
是因为 &arr[0] 和 arr 都是首元素的地址,+1就是跳过⼀个元素
- &arr 和 &arr+1相差40个字节
因为&arr是数组的地址,+1 操作是跳过整个数组的
哦,原来如此,这下我们就应该清楚数组名的意义了吧,嗯很棒
这个时候又有小伙伴会有疑问,前面我们有提过arr是 int* 的类型的,所以它跳过了4个字节,&arr[0]也是 int* 类型的,所以它也跳过4个字节,那&arr一下跳过40个字节,它是什么类型的呢?这个问题我们会在后面给大家解答的,大家不要着急哦
2. 使用指针访问数组
我们之前访问数组都是通过下标的方式来访问的
就像这样一样
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
那现在我们已经学习了指针,对指针有了一定的认识,我们可以通过指针来访问数组
像这样
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//输出
for (i = 0; i < sz; i++)
{
printf("%d ", *p);
p++;
}
return 0;
}
这个代码搞明白后,如果我们再分析一下,能不能使用指针给数组输入10个值呢,当然可以了
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//输入10个值
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);
//scanf("%d", &arr[i]);//使用下标的方式
}
//输出
for (i = 0; i < sz; i++)
{
printf("%d ", *p);
//也可以写成 *(p+i) *(arr+i) 或者写成p[i]
}
return 0;
}
我们可以看到数组名arr是数组首元素的地址,可以赋值给p,其实数组名arr和p在这里是等价的,可以使用arr[i]可以访问数组的元素,也可以用p[i]访问数组
总结:
arr[i]
== *(arr+i)
== *(p+i)
== p[i]
这四种写法是完全等价的
3. 一维数组传参的本质
数组我们学过了,之前也讲了,数组是可以传递给函数的,这个小节我们讨论一下数组传参的本质
我们想想,之前都是都是在函数外部进行计算数组元素的个数,那么能不能在函数内部计算数组元素的个数呢
来看看代码
#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;
}
看看结果:
我们会发现在函数内部是没有正确获得数组的元素个数,那为什么, se2 为什么会是1呢
这就要学习数组传参的本质了,上个小节我们学习了:数组名是数组⾸元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组首元素的地址
所以我们在接收的时候就应该用一个指针 int* arr
来接收,而不是用数组来接收
//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;
}
总结:
- 一维数组传参的时候,传过去的是数组首元素的地址
- 形参的部分可以写成指针的形式,也可以写成数组的形式,但本是上都是指针,写成数组的形式是为了方便理解
4. 冒泡排序
假设我们有一个无序的数组,要求把这个数组排成有序的,我们该怎么实现呢,大家知道排序有很多方法,今天我们主要给大家介绍一下冒泡排序
冒泡排序的核心思想就是:两两相邻的元素进行比较
我们来画个图帮助我们理解
我们就这样两两进行比较,一直走到和最后一个元素进行比较,我们将这个就叫一趟冒泡排序,当完成第一趟冒泡排序时,我们就可以看到已经将所有元素中最大的那个数字排到了最后,所以一趟冒泡排序就搞定了一个元素的顺序
那我来看看代码的实现:
//冒泡排序
#include<stdio.h>
void bubble_sort(int arr[], int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)//冒泡排序的趟数
{
//一趟冒泡排序
for (j = 0; j < sz - 1 - i; j++)
{
//如果满足前一个数小于后一个数就交换
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]);
}
printf("\n");
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };//现在我们给定的是一个升序的数组,我们来给它排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);//打印排序前
bubble_sort(arr, sz);//冒泡排序实现
print(arr, sz);//打印排序后
return 0;
}
运行结果所示:
我们发现已经完成了一个数组的排序
我们在没有学习指针前这样写,没什么问题,那现在我们学习了指针,是不是能用一下指针呢,让我们来实现一下吧
//冒泡排序(指针)
#include<stdio.h>
void bubble_sort(int* arr, int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)//冒泡排序的趟数
{
//一趟冒泡排序
for (j = 0; j < sz - 1 - i; j++)
{
//如果满足前一个数小于后一个数就交换
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]);
}
printf("\n");
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };//现在我们给定的是一个升序的数组,我们来给它排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);//打印排序前
bubble_sort(arr, sz);//冒泡排序实现
print(arr, sz);//打印排序后
return 0;
}
嗯,这下我们就完成了冒泡排序
但是我们会发现,这样写代码的效率是不是有点太低了,要是我们要排序的数组已经是有序的,是不是还要一个一个比,这样的话效率就显得有些低了,那我们是不是还可以把我们的代码给优化一下
那怎么优化呢
是不是可以定义一个 “1” , 要是 “1” 没变就说明是有序的,要是 “1” 变了就是无序的
怎么定义呢,请看以下代码:
//冒泡排序(优化)
#include<stdio.h>
void bubble_sort(int* arr, int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)//冒泡排序的趟数
{
//一趟冒泡排序
int flag = 1;//我们规定,有序为1,无序为0;
for (j = 0; j < sz - 1 - i; j++)
{
//如果满足前一个数小于后一个数就交换
if (*(arr + j) > *(arr + j + 1))
{
flag = 0;//一旦发生交换我们就让flag为0
int tmp = *(arr + j);
*(arr + j) = *(arr + j + 1);
*(arr + j + 1) = tmp;
}
}
//如果进行了一趟冒泡排序之后,flag仍然为1,
//就证明这个数组已经是有序的了,我们就跳出循环。
if (flag == 1)
{
break;
}
}
}
void print(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };//现在我们给定的是一个升序的数组,我们来给它排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);//打印排序前
bubble_sort(arr, sz);//冒泡排序实现
print(arr, sz);//打印排序后
return 0;
}
这样就优化好了
怎么样,冒泡排序是不是非常地简单
5. 二级指针
那接下来一级指针讲的差不多了,就讲讲让人头大的二级指针
我们说,之前在深入理解指针(1)中一级指针指向的是元素的地址
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里
这就是 二级指针
,二级指针就是用来存放一级指针变量的地址的
int a = 10;
int * pa = &a;
int ** ppa = &pa;
什么意思,还是画个图帮助理解
这个 int ** ppa
我们就可以理解为指向 int * pa
的指针
那我们能不能通过二级指针 int ** ppa
来找到a呢,答案肯定是可以
来看看代码实现:
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;
int** pp = &p;
**pp = 20;
printf("%d\n", a);
return 0;
}
运行结果所示:
嗯,没什么问题
6. 指针数组
大家看到这个名字是不是就开始疑惑了,指针数组到底是指针呢,还是数组呢,我们来类比一下.
整型数组 int arr[10]
就是存放整型的数组
字符数组 char arr[10]
就是存放字符的数组
指针数组 是不是就是存放指针的数组呢,对的
指针数组的每个元素都是用来存放地址(指针)的
指针数组的每个元素是地址,又可以指向一块区域
所以啊,大家遇到自己不熟悉的知识的时候,很多时候都可以用类比的方式给推出七七八八
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数组中
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
用来存放三个数组的地址,然后我们通过arr[i]来访问指针数组,假设此时i=0,从图中我们可以看到 parr[0]
指向的就是数组 arr1
,然后我们再通过访问 arr1
的下标来获取 arr1
数组中的元素
结语
好了,感谢您能看到这里,今天的学习到这里也就结束了,希望这篇文章对您有用
一起期待**深入理解指针(3)**吧,溜了溜了,我们下个星期再见吧