三、野指针
概念: 野指针就是指针指向的位置是==不可知==的(随机的、不正确的、没有明确限制的)
1、野指针成因
对于野指针相信大家在使用指针的时候都会遇到,可能也有同学听说过它是一个很危险的东西,而且在写代码的时候一不小心就使一个指针变成了野指针,接下去我将出现野指针的情况做一个罗列👇
① 指针未初始化
- 首先第一种就是这个指针未初始化的情况,也就是你定义了整型指针,但是呢并没有在系统中为其分配一块空间,此时这个指针p就指向了内存中一块随机的地址,此时这个指针就叫做【野指针】
- 然后在这个时候又使用
*p
访问到了这块地址中的内容,并对其做一个修改,那么此时就会出现问题
int main(void) { int* p; *p = 20; return 0; }
② 指针越界访问
- 第二种情况就是指针越界访问,用我们刚才那个关于初始化数组的小练习,此时我在遍历这个数组的时候在边界多访问了一次,那此时就会造成一个越界访问
- 若是指针p访问arr数组内的地址是没有问题的,因为这些地址是操作系统已经分配给我们的,但若是多访问一个位置的话其实这块地址就是一个随机的地址,那这个指针也就成了【野指针】
int main(void) { int arr[10] = { 0 }; int* p = arr; int sz = sizeof(arr) / sizeof(arr[0]); for (int i = 0; i <= sz; ++i) { *(p + i) = i + 1; } }
- 可以看到编译器报出了错误❌
③ 指针指向的空间释放
- 其实这可以联系我们的生活实际,比如说我在酒店里开了一间房,房间的房号叫302,此时就想要叫我的好兄弟明天也一起来住(doge),于是就告诉了他在XX酒店XX房间号。但是呢我只付了一个晚上的钱,到了第二天早上便只好退房了。
- 但是到了第二天张三却真的拿了他的行李箱过来住了,可以呢酒店前天说这间房已经退了不可以住了,不过张三执意要住,可是这间房呢已经退换回去了,还给酒店了,张三没有了使用权,此时他的这个行为就可以被称为是非法访问
- 还有另外一种空间释放指的是在堆区动态申请内容后释放,要使用到
free()
,这一块就不在这里讲了,后面介绍到动态内存规划的章节再做细讲,如果想了解的可以看看我的这篇【链表】文章 ——> 带你从浅入深真正搞懂链表
2、如何规避野指针
知道了会产生野指针的情况,那我们就要针对这些情况去做一些风险规避
1. 指针初始化
- 这一块很简单,只要是定义了一个指针,那就千万别忘了对它进行一个初始化,无论是让其保存一个地址或者是置为空都可以
- 其实可以把野指针看作是一条野狗,若是让这个指针保存一个地址也就是有个主人管住它了,那也就不会产生危险;将其置为空其实就使用链子把它拴起来了,也不会有问题
int a = 10; int* pa = &a; char* pc = NULL;
2. 小心指针越界
- 这一块的话自己小心和注意一点就行
3. 指针指向空间释放,及时置NULL
- 这个我在前面说起过,对于从堆区中动态申请的一块空间现在要将其释放了,也就是还给操作系统,但是呢你初始化后的指针还是指向堆区中的这块地址,只是它被释放了而已,此时就要让这个指针指向空(也就是NULL),这样就可以防止随意操作一块随机地址的风险了
4. 避免返回局部变量的地址
- 这一点我们上面也看到过了,若是返回一个在函数中创建的局部变量,此时外界虽然是接受到了这个变量的地址,但是这个变量的作用域只是在这个函数内部,除了作用域就销毁了,若是外界有一个指针接受了这个随机的地址,然后再去操作它就非常危险了🈲
- 这一点可以看C++引用章节的传引用返回部分 ,有细说到为何不能返回局部变量
5. 指针使用之前检查有效性
- 如果你还是担心自己的程序会出现野指针的问题,那么就要在操作一个指针的时候检查一下这个指针是否合法,也就是像下面这样在操作这个指针pa的时候判断一个它是否为空
int a = 10; int* pa = &a; if (pa != NULL) { printf("%d\n", *pa); }
- 可以看到,若是不对其进行一个判断,然后这个指针又是一个空指针的话,就会造成一个很大的问题
- 当然,如果你觉得这样写条件判断比较麻烦的话也是有其他简便的办法的,就是使用
assert()
进行一个断言。这种方式的话就比较粗暴一些了,直接给你弹出一个警告框
int main(void) { int a = 10; int* pa = NULL; assert(pa); printf("%d\n", *pa); return 0; }
- 也就是像下面这样,还会告诉具体哪行出现了错误,当然也就是你写断言的那样
四、指针运算
在了解了许多有关指针的基本知识和指针的使用技巧后,我们就要使用指针去做一些运算的工作,一起来看看
1、指针与整数的运算
- 首先通过下面这段程序来看看指针和整数之间的运算
- 首先是定义了一个float类型的数组,然后定义了一个
float
类型的指针。不过在一开始定义出来的时候没有进行一个初始化。我们循环内部对其进行了一个初始化,首先让其指向这个数组的首元素地址,再通过for循环去遍历这个数组 - 主要来看的就是就是循环的内部这个指针是如何变化的,
*vp++
这个表达式有两个操作符,一个是[*]解引用
操作符,一个则是[++]递增
操作符,如果你对操作符优先级了解的话可以知道【++】是比【*】的优先级来得高的,所以它会先进行一个运算,可是呢可以看出这是一个后置++,所以这个表达式所操作的还是==vp当前所指向的这块地址==。那么解引用取到的就是当前这块地址所存放的内容,可以看到右边是将其修改为0
#define N_VALUES 5 int main(void) { float values[N_VALUES]; float* vp; //指针+-整数;指针的关系运算 for (vp = &values[0]; vp < &values[N_VALUES];) { *vp++ = 0; } }
- 程序的思路和我们上面那个小练习初始化数组是一样的,这是这里的运算表达式稍微复杂一些而已
- 到最后初始化完成后也就是指向这一块地址
- 接着通过打印这个数组来看看是否初始化完了这个数组中的所有元素
2、指针的关系运算
- 好,我们继续来看指针的关系运算。与上一段类似,所以一些初始化的代码就不给出了
#define N_VALUES 5 for (vp = &values[N_VALUES]; vp > &values[0];) { *--vp = 0; }
- 可以看到,在这个for循环中,指针vp首先是指向数组的最后一个元素的后一个位置,那有同学问到这不是指针访问越界了吗?【野指针】!!!!
- 不要激动:metal:其实这不算是越界访问,而只能说是【越界指向】,这个指针就是指向了一下这块地址,但是呢并没有对这块地址进行任何的操作,那也就不会有危险。
- 其实对于野指针来说最大的危险就是一个指针指向了一块没有被操作系统分配过的、随机的地址,而且还去访问、修改这块地址中的内容
举个生活中的小案例:若是某一天你在银行:bank:前面溜达、经过一下,但是呢银行的保安说你是来抢银行的,那这个时候你一定不乐意了。那此时就可以将自己想作是那个指针,然后银行就是那个随机的地址,你就是看了看这块随机的地址,但是并没有去动它,是不会存在危害滴!
好,题外话,我们回归代码
- 来解释一下
*--vp
是什么意思,前置- -代表的就是让这个指针先前移一个位置,也就是让它从越界的那个位置回到存放最后一个元素所在的位置,此时也会不会造成越界访问了,然后再使用*
解引用操作符访问到这个地址的内容,同样进行一个初始化 - 此时数组就被初始化好了👉可是呢,还是有同学会觉得这样去写代码不是很直观,毕竟前置- -的这个代码阅读性并不是很高,因此就将数组的初始化修改成了下面这样
for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--) { *vp = 0; }
- 这么看起来的话其实就非常直观了,大家应该是都可以很轻松地看懂,指针从数组的最后一个位置开始遍历,直到遍历到第一个元素的地址为止,指针的偏移也放到了for循环中,而不是放在循环体的表达式里
- 但是呢这样的判断会使得指针vp最后偏移到了
数组的最前端
,也会产生一个越界的情况
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行
【标准规定】:
- 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
- OK,我知道你已经晕了,来解释一下,其实很好理解,就是对我们上面讲过的有关指针运算的两道题目做一个总结罢了。一句话来说就是【
指针可以越界指向数组最后一个元素后面的那个位置,但是不可以指向第一个元素前面的那个位置
】,不要问我为什么,因为人家标准就是这么规定的,你就不要越界访问那个位置就可以了
3、指针与指针的运算
接下去我们来说说有关指针和指针之间的运算,题目的情景我们之前在讲函数递归的时候有说起过
- 首先我们来做个引入,请你算算下面这段代码最后输出的结果为多少
int main(void) { int arr[10] = { 0 }; printf("%d\n", &arr[9] - &arr[0]); return 0; }
- 答案是9,你算对了吗❓开始我们有讲到过【地址】其实就是【指针】,那么对于两个地址之间的差值其实就是两个指针之间的距离,简单点说那也就是
&arr[9]
自&arr[0]
偏移了9个位置,所以它们之间的元素个数就是【9】
- 不过两个指针的相减,需要它们指向同一块连续内存空间,像下面这种情况就是不对的,因为int类型的变量和char类型的变量在内存中不是连续存放的,它们在内存中的距离是不确定的,是随机的
int a = 10; char ch = 'c'; printf("%d\n", &a - &ch);
好,接下去来看看指针与指针之间的运算
- 这里是要去求解一个字符串的长度,我们可以使用自带的库函数
strlen()
、自定义函数变量累加、递归,在本文中,我还要再介绍一种方法,也就是使用【指针】 - 思路很简单,函数形参接受了一个数组的首元素地址,在内部拿一个字符型指针接受一下,然后通过这个字符型指针去遍历这个字符串,我们知道对于一个字符串来说以
\0
作为结束的标志,因此只需要每次解引用判断是否遍历到\0
即可 - 最后当遍历到字符串结尾的时候将末尾的指针与形参接受的首元素地址,也就是指向首元素地址的指针,进行一个相减,就可以获取到这个字符串的长度了
int my_strlen(char* str) { char* pc = str; while (*pc != '\0') { pc++; } return pc - str; }