带你重新认识指针(下)
真的猛士,敢于直面惨淡的人生,敢于正视淋漓的鲜血。这是怎样的哀痛者和幸福者?然而造化又常常为庸人设计,以时间的流驶,来洗涤旧迹,仅使留下淡红的血色和微漠的悲哀。在这淡红的血色和微漠的悲哀中,又给人暂得偷生,维持着这似人非人的世界。我不知道这样的世界何时是一个尽头!我们还在这样的世上活着;我也早觉得有写一点东西的必要了。 ——鲁迅
本文已经收录至我的GitHub,欢迎大家踊跃star 和 issues。
https://github.com/midou-tech/articles
点关注,不迷路!!!
看完我上一篇指针的讲解之后很多同学反馈很不错,有网友给私信说之前在大学里面一直搞不懂指针的问题,说到指针都是云里雾里,老师讲的也是很难听懂 ,点击即可进入 指针(上)。也有很多网友表示非常期待指针下的文章,所以我就马不停蹄的继续写,下 主要讲解指针的特性以及指针安全问题。
指针的特性
指针和常量
先说下什么是常量,常量就是不可变的量,一旦定义该常量,其值在整个程序生命周期都是不可变的,常量存放在虚拟地址空间的常量区。
在C语言里面有两种定义常量的方法。
使用const关键字 ,const 定义的是变量不是常量,只是这个变量的值不允许改变是常变量,带有类型。编译运行的时候起作用存在类型检查。
使用#define预处理器, define 定义的是不带类型的常数,只进行简单的字符替换。在预编译的时候起作用,不存在类型检查。
其实很多时候我们错误的以为常量就是const 修饰的变量,这个说法其实是有瑕疵的。
指针常量
很多网友在学习指针和指针的特性等问题上总是会绕进去,其实不要绕进去最重要的一点是 要把握住核心本质。
本质上是一个常量,指针用来说明常量的类型,表示该常量是一个指针类型的常量。在指针常量中,指针自身的值是一个常量,不可改变,始终指向同一个地址。在定义的同时必须初始化
1int num = 5;
2int *const p = # // p为一个常量,拥有常量的属性。
3*p = 70;
4int snum = 100;
5int *sp = &snum;
6p = sp;
聪明的你一定看出上面代码有个地方会报错,是的 p 被我们声明为一个指针常量,此时指针p具有了常量的属性,其不能在改变指向,但是其指向的值是可以改变的。所以报错的代码是p = sp
这句。
常量指针
常量指针本质上是一个指针,常量表示指针指向的内容,说明该指针指向一个“常量”。在常量指针中,指针指向的内容是不可改变的,指针看起来好像指向了一个常量。
1int num = 5;
2int const *p = # //常量指针
3const int *sp = # //常量指针
4*p = 20;
5int snum = 100;
6p = &snum; //改变指向
7sp = &snum;
是不是又发现上面的代码有一处报错,你太聪明了,基本搞懂了常量和指针的本质。指针p和sp只是申明格式不同,本质完全一样。p被声明为一个指针,指向一个常量。换句话说就是一个常量的地址存放在指针p中。此时报错的就是*p = 20
,因为常量是不可变的。
到这里你基本掌握了常量和指针的关系,其实还是很简单的,也没大家在学校学的那么绕。接下来给大家在介绍一个进阶的关系。
常量指针常量
本质上是一个常量,该常量被一个常量指针指向。也就是说一个常量指针里面放置一个常量的地址,千万不要多看一眼这句话,你会被绕进去。
1const int num = 5; //一个不可变的常量
2const int * const p = # //一个存放常量地址的常量指针
千万不要绕进去了,其实认真理解了上面的指针常量和常量指针的问题,这个问题看起来会简单很多,就是一个常量,和一个常量指针。num是一个不可改变的常量,p只一个指针,该指针也是不可改变指向的。
指针和常量这个问题在面试中会被问到,好好理解下,同时有助于你更好的理解指针。
指针和函数
函数指针
什么是函数指针
如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。
函数指针的定义和普通指针不太一样。函数返回值类型 (* 指针变量名) (函数参数列表);
1bool(*p)(char, int);
还是很简单的,这就知道怎么定义一个函数指针变量了,当然也有很复杂的函数指针变量,面试的时候面试官可能会问一些变态的面试题,比如:
1int (*(void (*)())0)();
2void (*signal(int , void(*)(int)))(int
不过还是那句话,要把握核心本质,函数指针的核心本质是:函数返回值类型 (* 指针变量名) (函数参数列表);
函数指针使用
很多人会说,搞这么难干嘛,平时有使用么?哈哈,还真的经常用到,尤其是标准库中用的那叫一个多,比如sort中的比较函数就是一个函数指针。
指针作为函数参数
用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。
这不得不使我想起一个经典案例,大学老师一定会讲的,而且当时也是很多同学一直半解的。
1void swap(int a,int b){
2 int tmp = a;
3 a = b;
4 b = tmp;
5}
6int main(){
7 int x = 10;
8 int x = 20;
9 printf("swap before:%d,%d",x,y);
10 swap(a,b);
11 printf("swap after:%d,%d",x,y);
12 return 0;
13}
是不是历历在目。。。
这个简单的问题,要搞明白可以学到好几个知识点。第一个,函数栈问题;第二个,函数的参数传递是值传递还是地址传递;第三个,指针作为函数参数。不过我这里就不讲前面两个了,相信大家能来看指针问题说明前面基础知识都差不多了,要是你真的不会的话,你可以找龙叔我,我一定把你整明白,微信搜索公众号 龙跃十二 即可找到龙叔微信,同时有机会加入龙叔技术交流群,千万别错过喔。
求点赞👍 求关注❤️
交换两个数值问题,使用指针传递可以很轻松实现交换,原理如图。
1void swap(int *pa,int *pb){
2 int tmp = *pa;
3 *pa = *pb;
4 *pb = tmp;
5}
6int main(){
7 int x = 10;
8 int x = 20;
9 printf("swap before:%d,%d",x,y);
10 swap(&a,&b);
11 printf("swap after:%d,%d",x,y);
12 return 0;
13}
指针作为函数参数并不简单是这点用处,更大的用处在于传递复杂的结构体或者大容量的数组,减少数据拷贝产生的零时变量。举个例子
1struct Person{
2 string name;
3 string addr;
4 string number;
5 int age;
6 string hobby;
7 ...
8};
9//方案一
10int Fun(struct Person person){
11 //TODO
12}
13//方案二
14int Fun(struct Person *person){
15 //TODO
16}
此时足以见得用指针的好处,可以减少零时变量的产生。有一个问题必须说一下 指针作为函数参数依然是值传递。
指针作为函数返回值
函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。举个例子
1char *strlong(char *str1, char *str2){
2 if(strlen(str1) >= strlen(str2)){
3 return str1;
4 }else{
5 return str2;
6 }
7}
用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。总结一句话 不要让返回的指针指向一个局部性的对象
指针和C语言的内存管理
C语言的动态内存分配使用的是malloc系列函数,看下库函数的声明。
1void *malloc(size_t __size) __result_use_check __alloc_size(1);
2void *calloc(size_t __count, size_t __size) __result_use_check __alloc_size(1,2);
malloc系列函数返回值都是一个指针,而且是void*类型的,所以用malloc系列函数分配的内存必须用一个指针指向该内存,而且指针类型自己一定要强制转换。分配的内存是一个内存块,返回的是内存的首地址,指针存储的也是首地址。这一点内容较为简单,主要还是把握住指针的核心本质。
求点赞👍 求关注❤️
指针安全问题
说到这里指针的问题基本告一段落了,当然还有一个最重要的问题,那就是指针的安全问题。不可忽略,必须学懂,否则就不要把指针用在工程代码里面。
数组越界访问
1int main(){
2 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
3 int *p = arr;
4 printf("value:%d,%d,%d,%d\n",p[0],p[-2],p[16],p[100]);
5}
把数组转为指针访问的时候很容易出现这样的错误,但是你要是拿着数组下表访问,这段代码编译会报warning。这个错误也是天知道结果会是怎样,反正程序可以正常跑着,结果就是不多。
不要随便强转指针的类型
先看段简单的代码
1int main(){
2 char c = 'a';
3 int *p = (int *)&c;
4 *p = 1314;
5 printf("value:%d\n",*p);
6}
这段代码有多恐怖,我真的难以想象他的恐怖程度。
如果你在工程里面这样写了这样的代码,天知道会出现什么样的结果。p指针指向了一个不属于自己的空间地址,那片地址有可能是别的程序或者其他代码正在使用,你就这样改了别人的地址上的内容,天知道会出现什么。。。
重点来了 不要随便强制转换指针的数据类型,一定要清楚转类型之后会不会越界访问到其他内容。
迷途指针
1int *p = (int *) malloc(sizeof(int));
2*p = 100;
3free(p);
4*p = 200;
从内存中删除一个对象或者返回时删除栈帧后,并不会改变相关的指针的值。该指针仍然指向原来的内存地址,即使引用已经删除,现在也可能已经被其它进程使用了。
解决迷途指针的方法就是,我们释放指针对应的内存之后切记一定要把指针置为NULL,置空之后对指针使用会造成 segmentation fault error ,程序会崩溃。
解引用空指针
1int *p = (int *) malloc(sizeof(int)*1000);
2*p = 100;
3free(p);
4p = NULL;
这段代码看起来没啥问题,仔细看看也没啥问题。但是这段代码不知道会在线上崩溃到那一天,malloc返回的地址不是一定保证正确的,万一内存分配不出来或者分配失败了,你的程序瞬间就崩掉了。
总结
指针有很多好处,同时也有很多坏处。怎样去平衡好处与坏处,我们一定要规范我们使用指针的姿势,防止因为我们使用姿势的问题导致线上崩溃。把握指针的本质,了解内存的原理,掌握这两个重要的点能减少你平时在工作中的很多错误。
求点赞👍 求关注❤️
「转发」是明目张胆的喜欢,「在看」是偷偷摸摸的爱。
如果有人想发文章,我这里提供
有偿征文(具体细则微信联系),欢迎投稿或推荐你的项目。提供以下几种投稿方式:
去我的github提交 issue:
https://github.com/midou-tech/articles发送到邮箱: 2507367760@qq.com 或者 longyueshier@163.com 或者 longyueshier@gmail.com
微信发送: 扫描下面二维码,公众号里面有作者微信号。
精选文章都同步在公众号里面,公众号看起会更方便,随时随地想看就看。微信搜索 龙跃十二 或者扫码即可订阅。