C语言基础复盘:指针的深入理解(2)

发布时间:2026/7/16 10:09:08

C语言基础复盘:指针的深入理解(2) 目录引言一、const修饰指针二、野指针野指针的成因1.指针未初始化2.指针越界访问3.指针指向的空间释放如何规避野指针1.指针变量初始化2.小心指针越界3.指针变量不再使用时及时置为NULL指针使用前检查有效性4.避免返回局部变量的地址三、assert断言四、指针的使用和传址调用模拟实现strlen函数传值调用和传址调用总结引言近期复盘指针体系知识梳理传值、传址、野指针、const 与 assert 相关底层逻辑梳理完后收获良多。清楚自身技术储备仍有欠缺会持续深耕基础稳步夯实内存相关能力。本文将从传值调用与传址调用核心区别、野指针成因与规避方案、const 与 assert 的防护实战三个模块完整梳理指针相关核心知识点夯实底层基础。一、const修饰指针当我们创建了一个变量 a 时我们可以对 a 的值进行修改。但是只要在int a 0;前面加上const1修饰就不能改变 a 的值了。这里的a本质上还是变量只是在语法层面上限制了a的修改因此 a 是常变量本身是变量又具有常属性在C语言中是常量。当我们将变量 a 进行const修饰时我们还想修改 a 的值或者说我们想检查 a 的值是否还能通过其他方式修改我们能否钻个“空子”呢能而且很容易#includestdio.hintmain(){constinta0;int*pa;*p20;printf(%d,a);return0;}我们运行这段程序发现 a 的值真的改为20了。这就像是我们将一个房子的们锁住但是有人翻窗户进去了为了防止这种情况出现让这个房子从哪都进不去我们要对指针变量p 进行const修饰。constint*pa;这样就不能通过解引用操作对变量 a 的值进行修改了。此时我们又会发现一个新的问题const在修饰指针变量的时候会有两种位置即const 可以放在*的左边const 可以放在*的右边。分别讨论这两种情况①const在*左侧即const int* p或int const * pp是指针变量,p中目前存放的是a的地址,p中也可以存放其他变量的地址*p就是p指向的对象#includestdio.hintmain(){constinta0;constint*pa;intb20;pb;printf(%d,a);return0;}const在*左边时修饰*p,我们不能通过解引用对a值进行修改此时我们创建一个新的变量b将b的地址存在指针变量p中这样就可以更改a值了。我们得到结论const 在修饰指针变量的时候const 可以放在 * 的左边const 限制的是 p 指向的对象也就是 *p 不能被修改但是 p 不受限制也就是指针变量可以改变指向2②const在*右侧即int* const p#includestdio.hintmain(){constinta0;int*constpa;*p20;printf(%d,a);return0;}const在*右边时修饰p指针变量p就不能指向别的变量了此时我们通过解引用对a值进行修改。我们得到结论const 在修饰指针变量的时候const 可以放在 * 的右边const 限制的是 p也就是 p 的指向不能改变了但是 *p 不受限制也就是说 p 指向的内容是可以通过 *p 来改变的总结const 在修饰指针变量的时候①const 可以放在 * 的左边const 限制的是 p 指向的对象也就是 *p 不能被修改但是 p 不受限制也就是指针变量可以改变指向限制的是指针变量指向内容表示指针指变量向内容不能改变②const 可以放在 * 的右边const 限制的是 p也就是 p 的指向不能改变了但是 *p 不受限制也就是说 p 指向的内容是可以通过 *p 来改变的限制的是指针变量本身表示指针变量本身不能改变二、野指针概念野指针就是指针指向的位置是不可知的随机的、不正确的、没有明确限制的简单来说野指针就是指针指向的空间是不属于当前程序的。野指针的成因1.指针未初始化我们来看这个例子:intmain(){int*pa;*pa10;return0;}运行结果如下我们看到程序报错pa是变量也是局部变量局部变量不初始化它里边存放的是一个随机值。用*pa去接引用pa的时候就会访问一个随机值对程序来说就会非常危险。这个指针变量就成了一个野指针。2.指针越界访问我们来看这个例子#includestdio.hintmain(){intarr[10]{1,2,3,4,5,6,7,8,9,10};/*int* p arr[0];*/int*parr;//arr是数组名数组名是首元素的地址intszsizeof(arr)/sizeof(arr[0]);inti0;for(i0;isz;i){printf(%d,*p);p;}return0;}运行结果如下这里的sz大小是10也就是所for循环进行了11次循环到第11次的时候p就是野指针了。所以说我们向内存申请了多大空间我们能够使用的空间范围就是多大一但超出范围去访问这本身就是非法的。3.指针指向的空间释放再看这个例子#includestdio.hint*test(){intn100;//此处省略部分代码returnn;}intmain(){int*ptest();printf(%d,*p);return0;}这里的p就是野指针解释一下进入程序执行main函数进入main函数后首先调用test函数并将test函数的返回值放到p里面此时创建一个指针变量p接收返回值接下来开始调用test函数进入函数后创建变量n并赋值为100这里的n是一个局部变量我们假设n的地址是0x00ff1012接着往下执行直至将n的地址传给p所以现在p有能力找到n但是函数结束后局部变量n就销毁了所以现在通过p去找它所指向的空间时所指向的空间已经不属于当前程序了因此p就变成了一个野指针。造成野指针的原因可能性有很多种但是归根结底会发现只要指针变量指向的空间不属于当前程序了这个时候的变量就是野指针。如何规避野指针1.指针变量初始化明确知道指针变量要初始化到什么值不知道指针变量应该给什么值32.小心指针越界一个程序向内存申请了哪些空间通过指针也就只能访问哪些空间不能超出范围访问超出了就是越界访问。3.指针变量不再使用时及时置为NULL指针使用前检查有效性指针变量指向一块区域时通过指针访问该区域后期不再使用这个指针访问空间时把该指针置为NULL。只要是NULL指针就不去访问同时使用指针之前可以判断指针是否为NULL。判断就像这样#includestdio.hintmain(){intarr[10]{1,2,3,4,5,6,7,8,9,10};inti0;int*parr;for(i0;i10;i){*pi;p;//这两步就等同于*(p)}//此时指针已经越界了应该把指针置为NULLpNULL;//下次在使用时判断p不为NULL在使用//... ...parr;//重新让p获得地址if(pNULL)//判断{printf(指针变量为NULL);}else{printf(%d\n,arr[i]);}return0;}像这样运用这种处理方式可以很大程度上防止野指针产生。4.避免返回局部变量的地址那这里局部变量也被销毁了为什么还可以返回局部变量呢简单说明就是计算机中有一块空间即集成到cpu上的一个存储器叫寄存器n的值会存在寄存器中当n的值销毁时寄存器中的值会返回到r中,所以局部变量的销毁不影响返回局部变量。三、assert断言assert()是一种宏在运行时确保程序符合指定条件如果不符合就报错终止运行。这个宏称为 “断言”,即assert()断言。在使用assert()断言时必须包含头文件assert.h。#includestdio.h#includeassert.hintmain(){inta100;int*pNULL;assert(p!NULL);printf(%d,*p);return0;}运行结果如下assert()宏将一个表达式作为参数4。如果该表达式为真返回值非零assert()不会产生任何作用程序继续运行。如果该表达式为假返回值为零assert()就会报错在标准错误流stderr5中写入一条错误信息显示没有通过的表达式以及包含这个表达式的文件名和行号。使用assert()有几个好处自动标识文件和出问题的行号还有一种无需更改代码就能开启或关闭assert()的机制如果已经确认程序没有问题不需要再做断言就在#include assert.h语句的前面定义一个宏NDEBUG定义NDEBUG重新编译程序编译器就会禁用文件中所有的assert()语句。如果程序又出现问题可以移除这条#define NDEBUG指令或者把它注释掉再次编译这样就重新启用了assert()语句。#includestdio.h#defineNDEBUG#includeassert.hintmain(){inta100;int*pa;assert(p!NULL);printf(%d,*p);return0;}运行结果如下assert()的缺点引入了额外的检查增加了程序的运行时间在VS这样的集成环境中assert()直接被禁用了一般我们可以在Debug中使用在Release版本中选择禁用assert()。在VS这样的集成开发环境中Release版本直接优化掉了assert()。这样在Debug版本写有利于程序员排查问题在Release版本不影响用户使用时程序的效率。四、指针的使用和传址调用库函数strlen是求字符串的长度的只需要将字符串的起始地址传递给strlen就行统计的是字符串中\0之前的字符的个数。#includestdio.h#includestring.hintmain(){chararr[]abcdef;size_tlenstrlen(arr);printf(%zu,len);return0;}运行结果如下模拟实现strlen函数刚开始学习写代码时我们也许会这样写#includestdio.hsize_tmy_strlen(char*p){size_tcount0;while(*p!\0){count;p;}returncount;}intmain(){chararr[]abcdef;intlenmy_strlen(arr);printf(%zu,len);return0;}运行结果如下但是当我们掌握了const修饰与assert断言时我们发现这个程序并不是很安全可以进行一下改进#includestdio.h#includeassert.hsize_tmy_strlen(constchar*p)//只能统计字符个数不能认为修改加const修饰后代码更加健壮{size_tcount0;assert(p!NULL);//万一指针为空指针需要assert断言一下while(*p!\0){count;p;}returncount;}intmain(){chararr[]abcdef;intlenmy_strlen(arr);printf(%zu,len);return0;}修改后不能直接修改字符数组程序能更好地作用于统计字符串的长度同时能检验所传递值是否为空指针使代码更加健壮。传值调用和传址调用#includestdio.hvoidSwap1(intx,inty){intz0;zx;xy;yz;}intmain(){inta10;intb20;printf(交换前a %d b %d\n,a,b);//Swap1(a,b);//传值调用printf(交换后a %d b %d\n,a,b);return0;}这是一个想要将输入的两个数值交换顺序的程序但是运行过后发现没有达到预期效果。这是因为这里我们进行的是传值调用形参单独创建一的临时空间接收实参而改变形参后并不会影响实参实参还是原来的值。我们来看传值调用的运行结果想要改变实参的值就要用到传址调用#includestdio.hvoidSwap2(int*pa,int*pb){intz0;z*pa;//z a*pa*pb;//a b*pbz;//b z}intmain(){inta10;int*paa;intb20;int*pbb;printf(交换前a %d b %d\n,a,b);//Swap2(a,b);//传址调用printf(交换后a %d b %d\n,a,b);return0;}运行结果如下调用Swap2函数的时将变量的地址传递给了函数这种函数调用方式叫传址调用。传址调用可以让函数和主函数之间建立真正的联系在函数内部可以修改主函数中的变量所以在未来函数中只需要主函数中的变量值计算的问题就可以采用传值调用。如果函数内部要修改主函数中的变量的值就需要传址调用。总结在复盘知识点时我将其总结为三个词“隐患”、“排查风险”和“独立与共享”。温故而知新令我受益匪浅。const 常属性的意思,就是不能改变的意思。 ↩︎也就是指针变量 p指向 a 改变不了 a 的值那么我们就创建一个变量 b 将想要让 a 改成的值存入 b 中然后让 p 指向 b 。 ↩︎如果明确知道指针指向哪里就直接赋值地址如果不知道指针应该指向哪里可以给指针赋值NULLNULL是C语言中定义的一个标识符常量值是00也是地址这个地址是无法使用的读写该地址会报错。int* p NULL意思是p没有指向有效的空间暂时不要使用它。 ↩︎在上述的例子中这个参数表达式就是 p ! NULL 在不同情况下也可以换成其他的表达式如 p ! 20 等等。 ↩︎在C语言中stderr标准错误流是标准I/O库提供的三个标准输入输出流之一。stderr通常用于输出错误消息和诊断信息无缓冲即时输出故障也能反馈报错。stderr设计为始终输出到终端或者屏幕允许用户及时接收到错误和调试信息。 ↩︎

相关新闻