指针篇(2)- const修饰,野指针,assert断言,指针的使用和传址调用
目录
- 一、const修饰指针
- 1.1 const修饰变量
- 1.2 const修饰指针变量
- 二、野指针
- 2.1 野指针成因
- 2.1.1 指针未初始化
- 2.1.2 指针越界访问
- 2.1.3 指针指向的空间释放
- 2.2 如何规避野指针
- 2.2.1 指针初始化
- 2.2.2 小心指针越界
- 2.2.3 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
- 2.2.4 避免返回局部变量的地址
- 三、assert断言
- 四、指针的使用和传址调用
- 4.1 strlen的模拟实现
- 4.2 传值调用和传址调用
- 总结
一、const修饰指针
1.1 const修饰变量
在之前我们想让一个变量不能被修改时,会使用const:
这时候想要想要强行修改被const修饰的变量,就会出现报错
const是常属性的意思,就是不能发生改变的意思
这里的a本质上还是变量,只是在语法层面上限制了a的修改,在C语言中,a被称为常变量
那这里怎么证明它依旧是变量呢,这里可以放到数组里证明:
常量放在数组里是不会出现报错的
在C++中,const修饰的就不是变量了。而直接是常量
const int n = 10;//n直接是常量
如果我们在指针里加入const会摩擦出怎样的火花呢?
正常情况下,被const修饰的变量就不能修改了,但是如果我们绕过a,使用a的地址,去修改a就能做到了。
这里也确实是修改了,但还要再想一下,既然a被const修饰了,就是为了不能被修改,如果pa拿到a的地址就能修改a,这样就打破了const的限制,这是不合理的。
就比如说你的房子之前都是大门敞开,过了一段时间你发现家里进贼了,这时候你每次出门都把门锁住了,但贼又从窗户翻进去了,那肯定就不行了。
1.2 const修饰指针变量
所以应该让pa拿到a的地址也不能修改a,那接下来该怎么做呢,即用const修饰指针变量即可:
用const修饰之后,指针变量就不能修改了。
在细讲这里之前,先看下面这张图:
a变量里放的是10,0x0012ff40是a变量的地址
pa是指针变量
pa中目前存放的是a的地址
pa中也可以存放其他变量的地址,即也可以把b的地址放到pa里去
*pa就是pa指向的对象
一般来说const修饰指针变量,可以放在* 的左边,也可以放在 *的右边,意义是不一样的,用下面的代码具体分析一下
const放在 *左边的时候,const限制的是pa指向的对象,也就是 *pa不能被修改,但是pa不受限制,也就是指针变量可以改变指向。
const可以放在 *的右边,const限制的是pa,也就是pa的指向不能改变了,但是 * pa不受限制也就是说pa指向的内容,是可以通过pa来改变的。
下面举一个吃凉皮例子方便记忆:
现在有两个男孩,第一个男孩a有10块,第二个男孩b有100块,有一个女孩叫p,男孩a把地址都给了p了,这俩建立了男女朋友关系,即*p指向了a。有一天女孩想吃凉皮了,男孩囊中羞涩,一想吃完就没钱了,相当于 *p-=10,a=0,男孩于是就拒绝了,也就是不发生 *p-=10,a=0,这一步,相当于const int *p=&a,限制了 *p。女孩于是生气了,10块都不舍得给我花,行,既然换不了凉皮,那我换个男朋友,也就是p=&b,这是可发生的,因为const放在左边无法限制,这就是const放在 *左边的意义,他可以不请女孩吃凉皮,但是女孩有权利换男朋友。
这时候男孩慌了呀,有对女孩说,我可以请你吃凉皮,但是你不能换男朋友,相当于int* const p=&a,*p-=10。女孩吃了凉皮不生气了,p=&b这个动作就不发生了。
如果有const int* const p,恭喜你,遇到渣男了,*p-=0不能发生,p=&b也不能发生,既不给你花钱,也不让你换男朋友,这就是白嫖。
二、野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),再总结就是指针指向的空间,是不属于当前程序的。
2.1 野指针成因
2.1.1 指针未初始化
正常指针的初始化是这样的:
int main()
{int a = 10;int* pa = &a;*pa = 20;//a=20return 0;
}
未初始化的指针:
int main()
{int* pa;*pa = 20;return 0;
}
这里的pa是局部变量,局部变量不初始,它里面存放的是一个随机值。
因为pa里面存放的并不是一个有效的地址(随机的),*pa想通过pa里的地址访问一个空间,并把20放入,这时候的访问就非常危险,很可能访问一个根本不属于当前程序的内存空间,这时候就有可能非法访问。
2.1.2 指针越界访问
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]);int* p = arr;//int* p=&arr[0];for (i = 0;i <= sz;i++){printf("%d ", *p);p++;}return 0;
}
这串代码其实是有问题的,虽然目的是为了打印数组中的10个元素,但该循环是从0-11,意味着会循环11次,当第11次时,p访问了元素10后面的空间,这时候p就是野指针了。
2.1.3 指针指向的空间释放
这个标题是什么意思呢?先看下面的代码:
int* test()
{int n = 100;//...return &n;
}int main()
{int* p = test();printf("%d\n", *p);return 0;
}
乍一看这串代码是不是觉得没有问题?但我既然写出来了,那他肯定是不对的
这串代码在创建n变量的时候,变量的生命周期开始,出函数后生命周期结束,地址传出主函数后,p就有能力找到n了,但是这时n创建时申请的4个字节空间已经被销毁了,还给操作系统了,相当于你去图书馆借书,在借阅期限达到之前的这段时间,这本书属于你,但当还了书之后,这本书就不属于你了,int* p=test()结束后,n的空间已经不属于当前程序了,此时*p找到n的时候,这时候找到的空间就不确定放的是不是100了,即你还了书之后,另一个人借走了你刚刚还的书,这时候记的这个地址还能找到n吗?n里面还能是100吗?有可能是,也有可能不是,如果n的空间没有被篡改,那有可能是100,被别的篡改或使用了,就不是100了。
这里还能正确打印是一种巧合,该片空间被还给操作系统但没有被使用。
我这里如果在打印之前随便打印一句话,就不是100了,值就被破坏了:
这里也涉及函数针栈的创建与销毁的内容,我后面会写一篇文章来解释这里的底层原理,链接到这里
n是在第一个函数调用里的栈帧创建的,当我调用结束,函数栈帧销毁,也就是把空间还给操作系统了。
但如果我之后调用printf,在刚刚还回的空间的基础上,给printf创建了一个函数的栈帧,那内部空间就被覆盖了,所以接下来printf去访问这个空间的时候,访问的就不是它了。
2.2 如何规避野指针
2.2.1 指针初始化
如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL(空指针),NULL是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是被规定死的,不允许访问的,读写该地址会报错。
这里我们可以右击转到定义
227行这里的意思是C++里NULL是0
230行就是在C里,NULL是0,被强制类型为void*,本质上还是0,带了个类型,为什么要给它空指针呢?
这是为了避免它称为野指针,我们可以把野指针比喻成野狗,也够是很危险的,而NULL就像一个铁链,拴住了野狗,免得它出去咬人。
int* p=NULL;意思是p没有指向有效的空间,暂时不要使用它。
2.2.2 小心指针越界
一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
2.2.3 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置NULL。因为约定俗成的一个规则就是:只要时NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL。
用一串代码来实现:
//把前5个元素置为5
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = arr;int i = 0;for (i = 0;i < 5;i++){*p = 5;p++;}p = NULL;//现在又想使用pp = arr;if (p == NULL)//这里NULL也可以直接写0{printf("p 是空指针\n");}else{//...}return 0;
}
在5次循环走完,p指向了元素6,这时候不用了置为空指针,一定程度上避免了野指针,且每次使用时都判断它是不是空指针。
2.2.4 避免返回局部变量的地址
如造成了野指针的3个例子,不要返回局部变量的地址,那为什么返回局部变量没有问题呢?
return利用了中间的一个空间,该空间为寄存去(不是一个变量,集成在CPU上的一个存储器,会把n的值暂时放在存储器上,销毁了局部变量n不印象寄存器上的100,r获得的100是从寄存器上来的。
返回局部变量的地址还有一种叫法,返回栈空间的地址,因为局部变量是放在栈空间的,本质上是一个意思。
三、assert断言
之前在判断一个指针的有效性,用if语句判断,还有另一种方法,即assert
assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行,这个宏常常被称为”断言“
上面代码在程序运行到assert这一语句时,验证变量p是否等于NULL。如果确实不等于NULL,程序继续运行,否则就会终止运行,并且给出报错信息提示。
assert()宏接受一个表达式作为参数。如果该表达式为真(返回值非零),assert()不会产生任何作用,程序继续运行,如果该表达式为假(返回值为零),assert()就会报错,在标准错误流stderr(就是在屏幕上打印一串错误信息),显示没有通过的表达式,以及包含这个表达式的文件名和行号。
assert()的使用对程序员是非常友好的,本质上是一种防御型编程,提前把可能的错误条件预设出来。
使用assert()有几个好处:
- 它不仅能自动标识文件和出错误的行号。
- 还有一种无需更改代码技能开启或关闭assert()的机制。如果以及确认程序没有问题,不需要再做断言,就在#include<assert.h>语句的前面,定义一个宏NDEBUG
#define NDEBUG
#include<assert.h>
之后,重新编译程序,编译器就会禁用文件中所有assert()语句。如果程序又出现问题,可以溢出这条#define NDEBUG指令(或者把它注释掉),再次编译,这一就重新启用了assert()语句。
assert()的缺点是,因为引入了额外的检查,增加了程序的运行时间。
一般我们可以在Dubug中使用,在Release版本中选择禁用assert就行,在VS这样的集成开发环境中,在Release版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在Release版本不影响用户使用时程序的效率。
assert相当于条件判断
四、指针的使用和传址调用
4.1 strlen的模拟实现
之前已经写过这样的函数实现strlen的功能了,这里就用今天写的东西对代码内容加固一下:
在设计时肯定不希望字符串的内容被改变,只需要统计数据,这样在char* p前加const,即*p不能被改,循环判断条件前加assert(),防止指针变量是空指针,就不会出现危险情况,这样的代码更加完善。
4.2 传值调用和传址调用
既然学指针的目的是使用指针来解决问题,那么有什么问题是非使用指针不可的呢?
写一个函数,交换两个整型变量的值
不使用指针的时候,根据以往逻辑我们是这么写的
虽然x和y确实接收了a和b的值,但是a,b的地址和x,y的地址并不是一一对应一样的,相当于x和y是独立的空间,那么在Swap1函数内部交换x和y的值,自然不会影响a和b,当Swap1函数调用结束后返回main函数,a和b没法交换。Swap1函数在使用的时候,是把变量本身直接传递给了函数,这种函数调用方式叫传值调用。
结论:实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参,所以Swap1失败了。
那么怎么办呢?
现在要解决的是当调用Swap函数的时候,在main函数中将a和b的地址传递给Swap函数,Swap函数里边通过地址简介的操作main函数中的a和b,并达到交换的效果就好了。
可以看到Swap2的方式顺利的达到了我们想要的效果,这里调用Swap2函数的时候是将变量的地址传递给了函数,这种函数调用方式叫:传址调用。
传址调用,可以让函数和主函数之间建立真正的联系,在函数内部可以修改主调函数中的变量,所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用。
总结
不得不说兴趣确实是第一动力,虽然今天写了一天的代码和博客,也没有感觉很累,但是真要让主播期末复习个一上午,那真是难受得要死。喜欢主播文章内容的靓仔靓女们不要忘记一键三连支持~