C语言深入了解指针(2):const 修饰指针、野指针、assert 断言、传址调用
指针是 C 语言的灵魂也是很多初学者的拦路虎。上一篇我们认识了指针基础本篇继续深挖指针搞懂const修饰指针的两种写法、什么是野指针、如何规避野指针、assert断言工具以及最重要的传值调用与传址调用彻底理解为什么交换两个变量要传地址。一、const修饰指针1.1 const 修饰普通变量const的本意是限定变量不能直接被修改。constintn0;n20;// 编译报错不能直接修改n注意const修饰的本质还是变量不是常量。只是语法层面禁止直接赋值修改。还有一个小bug可以通过取地址用指针绕过语法限制去修改它的值。#includestdio.hintmain(){constintn0;printf(n %d\n,n);int*pn;*p20;// 绕过const限制修改nprintf(n %d\n,n);return0;}输出结果n 0n 20这就带来问题n被 const 保护但是指针拿到地址就可以篡改。所以我们需要用const来修饰指针限制指针的行为。1.2 const 修饰指针的两种核心写法const写在*左边和*右边含义完全不同在考题里面容易出现的知识题const int *p;const 在 * 左边保护指针指向的数据不能通过指针修改内容但是指针 p 本身可以改变指向别的变量。voidtest2(){intn10;intm20;constint*pn;*p20;// 报错不能修改指向的内容pm;// 指针p可以改指向}两边都加 constconst int * const p;既不能改指向内容也不能改变指针指向完全锁死。记忆口诀星号左边保护内容星号右边保护指针本身。二、野指针野指针指针指向的位置是随机、无效、未知的内存地址。野指针不是 NULL 指针NULL 是人为置 0 的有效标识野指针是乱码随机地址访问野指针会造成程序崩溃。2.1 野指针三大成因指针未初始化局部指针默认随机垃圾值intmain(){int*p;// 局部变量没有赋值存随机地址*p20;// 直接解引用野指针程序崩溃return0;}指针越界访问数组只有 10 个元素指针跑到数组内存外面。intmain(){intarr[10]{0};int*parr[0];inti0;for(i0;i11;i){*(p)i;// i超过9之后p越界变成野指针}return0;}返回局部变量的地址函数执行结束函数栈帧销毁局部变量内存被回收返回的地址变成无效地址。int*test(){intn100;returnn;// 返回局部变量地址高危}intmain(){int*ptest();printf(%d\n,*p);// p是野指针结果不可预测return0;}2.2 如何规避野指针指针一定要初始化不知道指向谁就赋值NULL知道指向谁就存合法变量地址。intnum10;int*p1num;int*p2NULL;// NULL代表不指向任何有效空间NULL本质#define NULL ((void*)0)地址 0操作系统保护禁止读写。杜绝指针越界严格控制访问范围指针不用的时候及时置 NULL使用前先判断是否为 NULLif(p!NULL){// 安全的解引用操作}严禁返回局部变量的地址通常口诀野指针 没人拴的野狗置 NULL 相当于把狗拴树上使用前判断 NULL 先看狗有没有拴不要乱碰。三、assert 断言调试工具头文件#include assert.hassert(表达式)运行时检测表达式如果表达式为假 (0)程序直接崩溃打印出错文件名 行号方便调试排错。//例子assert(p!NULL);如果p是 NULL程序直接终止提示哪一行出错assert 的开启与关闭如果发布正式版本不想保留断言带来的性能开销可以在包含头文件之前定义NDEBUG宏全部 assert 直接失效。#defineNDEBUG// 关闭所有assert#includeassert.h使用场景Debug 调试版本大量使用Release 发布版本禁用。注意assert 只做调试检查不能替代业务逻辑的if判断。示例模拟实现 strlen结合 constassert库函数原型size_t strlen ( const char * str );const char*保证函数内部不会修改传入字符串内容assert(str)保证传入指针不是 NULL#includestdio.h#includeassert.hintmy_strlen(constchar*str){assert(str);// 如果传NULL直接报错intcount0;while(*str)// *str不等于\0就循环{count;str;}returncount;}intmain(){intlenmy_strlen(abcdef);printf(%d\n,len);return0;}四、传值调用 vs 传址调用指针核心应用传值调用需求写函数交换两个 int 变量的值。//传值调用失败#includestdio.hvoidwap1(intx,inty){inttmpx;xy;ytmp;}intmain(){inta10;intb20;wap1(a,b);printf(a%d b%d,a,b);//输出 a10 b20交换失败return0;}失败的原因传值调用实参a、b把值拷贝一份给形参x、y。x、y 是函数内全新临时变量有自己独立内存。函数内部交换的只是副本和 main 里面 a、b 毫无关系。函数结束临时变量销毁main 的 a、b 纹丝不动。所有核心结论传值调用函数内部修改参数不影响外部实参。//传址调用成功//把变量的地址传给函数函数拿到地址间接操作 main 函数原始变量。#includestdio.hvoidwap2(int*px,int*py){inttmp0;tmp*px;*px*py;*pytmp;}intmain(){inta10;intb20;wap2(a,b);// 传入地址printf(a%d b%d,a,b);//输出 a20 b10交换成功return0;}传值调用和传址调用区别总结传值调用传递变量的值拷贝副本。函数只能读取数据不能修改外部实参。适合只拿数据做计算。传址调用传递变量地址。函数通过指针解引用可以修改主调函数里面原始变量什么时候用传址需要函数修改外面变量的值交换、输出多个结果传递大块数组避免整块拷贝提高效率五、总结const 修饰指针*左边保护指向内容*右边保护指针本身。野指针产生三原因未初始化、越界、返回局部变量地址4 种规避手段。assert调试断言Debug 开启Release 用NDEBUG关闭。传值调用拷贝副本改不了外部变量传址调用传地址可以修改外部变量。