指针的深入理解(二)
文章目录
前言
哈喽,各位小伙伴!今天给大家带来指针深入理解的第二期。通过上期内容,大家对指针的基本概念有了认识,今天小编带大家继续深入学习指针内容。话不多说,咱们向着大厂前进!
一.野指针
1.1野指针概念
什么是野指针?野指针就是指向位置是不可知的(随机的,不正确的,没有明确限制的)。
野指针三大成因:
-
指针未初始化
以这个代码为例,代码运行时编译器报错,说局部变量p没有初始化。p是个指针,但是里面我们没有存有地址,也就是没有对初始化,局部变量未初始化时,它里面的值是随机值(0xcccccc,涉及函数栈帧的创建和销毁),这时对p进行解引用操作,由于p里面存的地址是随机值,我们解引用访问是不知到他访问的空间的,这时候的p指针就像一条不知去向的野狗,是非常危险的,也就是野指针。 -
指针越界访问
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a[10] = { 0 };
int* p = a;
for (int i = 0; i < 10; i++)
{
*(p++) = i;//循环10次,p由首元素跳到10个整型,第十次循环后指向第十个元素后的空间
}
return 0;
}
我们创建一个十个元素的数组,把数组首元素(数组加粗样式名)地址赋给p指针,第一次循环先使用p,后p+1跳过一个整型指向第二个元素,以此类推,第十次循环完后p会指向第11个元素,但数组是十个元素,这时p越界,指向不属于他的空间,此时p为野指针。
- 指针指向的空间释放
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int* test()//开辟空间,进入test函数
{
int n = 100;
return &n;//返回n地址,test函数空间销毁,n也销毁。空间还给操作系统。
}
int main()
{
int* p = test();
printf("%d", *p);//野指针
return 0;
}
为了创建test函数,内存开辟空间。之后空间内创建变量n,将n地址返回给指针p,但注意,返回后,函数空间也销毁了,n也随之销毁,空间还给操作系统。此时在通过解引用访问n时,指针指向的不一定是n,指向哪里我们也不得而知,此时p就是野指针。
- 结论一:指针指向不输入他的空间时,这时的指针就是野指针。
1.2如何规避野指针
- 指针初始化。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int* p = NULL;//空指针,地址为0,但不可访问。
printf("%d", *p);
return 0;
}
我们知道未初始化是导致野指针的一个原因。所以我们创建指针时想要指针指向哪里就直接赋值,如果不知道指向哪里就赋值为空指针。也就是NULL。空指针就相当于把野狗(野指针)用绳子拴住,不给他乱跑。其实空指针也并非是空的,它里面存的地址为0,但是不可访问。如果访问就会产生访问权限冲突。这就相当于野狗被拴住,你就站在旁边挑逗他,这也是很危险的。
- 检查指针有效性
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 20;
int* p = NULL;//空指针,地址为0,但不可访问。
printf("%d", *p);
if (p != NULL)
{
*p = 100;
}
return 0;
}
我们在使用指针前可以检查指针的有效性,判断是否为空指针。这样就可以避免野指针。但是不代表这样我们就可以不初始化了。因为前面说过指针不初始化是是随机值,随机值不一定是0,也就是不一定为空指针。但是你仍然使用这个指针,这时也是野指针。所以好习惯要相辅相成。
- 小心数组越界
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a[10] = {0};
int* p = a;
for (int i = 0; i < 10; i++)
{
*(p++)=i;//循环后数组越界
}
p = NULL;//置空
p = a//置空后在使用
if (p != NULL)//判断后再使用
{
*p = 100;
}
return 0;
}
这个代码前面讲过了,循环完后指针已经越界。但是我们前面讲过两个避免野指针的方法,我们在使用指针后置空,再去使用,每次使用之前都进行判断是否为空指针即可。
- 避免返回局部变量的地址
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int* test()//开辟空间,进入test函数
{
int n = 100;
return &n;//返回n地址,test函数空间销毁,n也销毁。空间还给操作系统。
}
int main()
{
int* p = test();
printf("%d", *p);//野指针
return 0;
}
这也是导致野指针的原因,返回局部变量的地址后使用指针访问空间。但空间在地址返回后已经销毁,被操作系统回收了,此时p就是野指针,我们以后要对这种特殊情况多加小心,养成置空和判断的好习惯。
二.assert断言
assert.h头文件中定义了宏assert(),用于确保程序在运行时符合指定条件,如果不符合就直接报错终止运行,这个宏通常被称为“断言”。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
int main()
{
int a = 10;
int* p = NULL;//置空
assert( p != NULL);//断言如果表达式为真这不影响程序,如果为终止程序。
*p = 100;
printf("%d", *p);
return 0;
}
可以发现assert可以接收一个表达式作为参数,如果表达式为真,则不影响程序运行结果,如果为假则终止程序,并报出错误文件位置。这对我们程序员是非常友好的,它就相当于无需更改代码就能确认程序是否正确的开关。那这个开关如何关闭呢?这是我们就可以在assert头文件前定义一个宏NDEBUG即可。
定义宏后,程序运行起来并没有终止,说明此时assert()被关闭了。但是注意assert()只能在Degug版本中使用,在Release版本(用户使用版本)中assert是被禁用的,直接被优化掉了。因为assert发挥作用时,也引入额外的检查,增加运行时间。所以这样可以方便程序员检查程序的同时,不影响用户使用体验。
三.指针的使用和传址调用
1.1strlen的模拟实现(优化版)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#define NDEBUG
#include<assert.h>
size_t my_strlen(const char* p)//统计次数,无需修改字符串,const修饰防止更改字符串
{//字符串长度不可能为负数,所以返回类型改为size_t(unsigned int也可),防止返回负数。
int count = 0;
assert(p != NULL);//断言,防止使用空指针
while(*p!='\0')
{
count++;//统计次数
p++;//指针移动
}
return count;//返回结果
}
int main()
{
char a[] = "abcdef";
int n = my_strlen(a);
printf("%d", n);
return 0;
}
我们用指针遍历字符数组,每次循环统计次数,遇到**\0结束,返回长度即可。我们这里对其进行优化。使用指针前,用assert断言判断是否为空指针**,再用const修饰形参,防止更改字符串。字符长度不为负数,返回类型改为size_t,防止返回负数。我们再来看看strlen库函数的介绍。()
可以发现strlen库函数形参就是和我们优化的版本是一样的。
1.2传值调用和传址调用
我们学到这里,大家想想指针到底有什么用?如果没用的话,指针存在是不是多余了?接下来小编通过一道题目带大家体会一下指针的作用。
题目:写一个函数,实现两个整形变量交换。
相信有部分小伙伴觉得这有啥难。直接就写出来典型的反面教材代码(哈哈!)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int swap(int x, int y)
{
int z = 0;
x = y;
y = z;
}
int main()
{
int a = 10;
int b = 20;
printf("a=%d\nb=%d\n", a,b);//交换前
swap(a,b);
printf("a=%d\nb=%d", a, b);//交换后
return 0;
}
发现结果是错误的,这是为什么呢?这就涉及一个很重要的知识点了,大家可以当成结论来记。
- 结论二:调用函数时,形参是实参的一份临时拷贝,形参有自己独立的空间,修改形参,不会影响实参。
这里我们在swap函数中的x和y有自己独立的空间,对x和y交换是不会交换a和b的。这就是传值调用。那怎样才能交换呢?我们之前学习的解引用也可以修改变量的值,但是指针需要地址,那我们就给他地址。这就是传址调用。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int swap(int* x, int* y)
{
int z = *x;
*x = *y;
*y = z;
}
int main()
{
int a = 10;
int b = 20;
printf("a=%d\nb=%d\n", a,b);//交换前
swap(&a,&b);
printf("a=%d\nb=%d", a, b);//交换后
return 0;
}
大家可以发现我们使用传址调用成功实现交换。这里大家可以发现指针也是有自己的作用的。指针能多给我们提供解决问题的方法,具体问题选择具体方法。
四.指针与数组
1.1数组名的理解
经过前面的学习我们知道数组名就是数组首元素的地址。但是,有两个特殊情况。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a[10] = { 0 };
printf("%d", sizeof(a));//表示整个数组,计算的是整个数组的长度,单位是字节。
return 0;
}
按照之前的理解的话,应该打印int的4个字节大小。
但是结果是40,为什么是40呢?40=4*10。40=int类型大小*有多少个int。所以sizeof(a)这里的数组名表示的是整个数组。这是一种特殊情况。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a[10] = { 0 };
printf("%p\n", a);
printf("%p\n", a + 1);
printf("%p\n", &a[0] );
printf("%p\n", &a[0] + 1);
printf("%p\n", &a );
printf("%p\n", &a + 1);
return 0;
}
对比三种加一情况,发现a+1和&a[0]+1均是跳过4个字节****内存加4。因为他们都表示数组首元素加一跳过一个整型。而**&a加一加了40,说明这里的a表示整个数组**,加一跳过一个数组。这是另一种情况。
结论三:数组名表示数组首元素地址,但sizeof(数组名)和&数组名表示整个数组的地址。
1.2指针访问数组
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void printf(int arr[],int b1)//int arr[]本质就是指针,所以也可以是int* p,只是为了方便理解写成数组的形式
{
int* p = arr;
//int b = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < b1; i++)
{
printf("%d ", *(p+i));//刚开始指针指向首元素,加上下标就是跳过多少个整型后的位置
}
}
int main()
{
int arr[] = { 1,3254,4,67,576,34,6,98 };
int b = sizeof(arr) / sizeof(arr[0]);//统计数组长度
printf(arr,b);//数组名是数组首元素的地址,所以传过去本质是地址,不是数组
return 0;
}
这里我们用指针打印数组。我们观察一下指针打印和数组打印就能推出一些结论。
其实数组的本质就是指针,即使写成数组,编译器也会转化为指针的形式。所以其实i[p]的形式和上面四种形式也是等价的。因为**[]其实是个操作符**,左右两个数是它的两个操作数,他们的位置不影响结果。
后言
今天带大家学习了指针的更深层次的内容,相信也对大家对指针的理解有所提升。今天就分享到这,感谢各位小伙伴的耐心阅读,咱们下期!