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