为什么 C 语言的 free 函数最后不自己把指针指向 NULL?
56 个回答
不可以,因为传给 free 的是一个值。
二级指针这个设计也不行,因为这意味着只能传一个可写变量(左值)进去。这样的话,如果我们需要释放一个右值或者const指针就不行了。
最后,其实就算你传了左值进去,也解决不了你要解决的问题。因为这个值可能不止一份。
1、“内存泄漏”和free后指针是否重置为null无关。
2、free后的无效指针不应该被重复使用;如果重复使用了,这种指针就叫做“悬挂指针”。
悬挂指针(Dangling Pointers)、野指针(Wild Pointers,未初始化、内容随机的指针)都是病态的;使用它都会给程序带来灾难。
显然,病态指针并不限于“free后重新使用的指针”,未初始化的指针、指向错误位置的指针、指向已失效对象的指针……这些都可能引起程序崩溃(最轻微的问题)、程序状态紊乱(严重的问题)、数据破坏(严重的问题)、状态/数据随机性破坏(最严重也最令人头痛的问题)。
指针相关的病害还不仅于此。
更可怕也更隐蔽的,内存越界,比如你申请一个10字节的char数组,在里面放了个“0123456789”,那么就出现了内存越界问题。
这可能看起来正常、测起来正常、调试版正常,但发行后却引起了程序崩溃(真幸运!)甚至数据破坏。
类似的,内存申请、使用之后,忘了归还(free),这才是内存泄漏。这会导致程序无法长时间使用,否则内存占用就会无限增长、直至崩溃。
此外,使用不同方式申请的、归还时却用了另一种方式,这都会引起小至程序崩溃、大到数据破坏的问题。
相比之下,悬挂指针只算是疥癣之疮了。
从C语言的设计层面来说,从一开始,就是有很多事情是它做不到的。
事实上,理想的、可以自动发现任何逻辑错误的编程语言本身就是不存在的。
尤其是,很多看起来像是逻辑错误的写法,它可能是语言使用者有意为之——这就是为何各种编程语言都有海量的warning、且允许带着warning的代码通过编译、发行的原因。
但是,对追求质量的程序员来说——™编译器都警告你了!那么大个警告啊!你丫就这样熟视无睹的放它到发行版祸害用户、祸害公司了?
这种状况,也使得这样一句对傻X程序员的调侃深入人心——程序员都™看不见warning。
以及,评论区引用的笑话:悬崖边竖着一块大牌子,上面写着WARNING,只有程序员掉下去了。
但是,随着代码量激增,越来越多的逻辑错误不可避免的混入了软件,这必将带来越来越糟的软件质量。
怎么办呢?
没错。增加更多的自动检查。这就是lint类工具。
lint类工具把过去习以为常的、轻易不出错误但逻辑上存在缺憾的写法统统标记出来、发出warning。
能够使得lint类工具不报告任何warning的代码叫做lint-free;追求lint-free的人写出的代码质量相对较高。
lint类工具是如此的成功,以至于新版本的c/c++以及其它各种编程语言直接集成了lint工具,可以报比过去多得多的warning。
但即便如此,代码中的逻辑错误仍然无可避免。
这是因为,“逻辑错误”往往和需求挂钩;机器不知道需求,甚至也无法理解人类语言表述的需求,那么它自然无法对所有的逻辑错误报错——如果它有这个能力,让它自动生成代码好了,还要程序员做什么?
free后自动把指针置为null就是C语言做不到的事情之一。
当然,free后、要求编译器自动检查,发行程序员忘了把指针赋值为null,这是能做到的——lint类工具能做出比这个详细得多得检查。
但这个检查并没有加入lint。你并不能看到类似“指针free后需要置空”这样的警告。
这是因为,“free后置空”并不是灵丹妙药、更不可能解决所有问题;事实上,某些情况下,它还是有害无益的。
比如说,在某个程序里,我们在过程A中分配内存、过程B使用内存、过程C释放内存;理想情况下,代码总应该是A->B->C的顺序。
但实际上呢?B总是会莫名其妙的得到一个无效的指针。
怎么办呢?
一种看似聪明的做法是:强制过程C释放内存后把指针置零;然后在B中检查指针是否为null,是,则直接返回。
你看,程序再也不会崩溃了——win-win!
然而,在有经验的程序员看来,你这个蠢操作把错误隐藏的更深、导致程序逻辑更复杂、更难排除错误了。
这是因为,为什么B会得到无效指针的根本原因并没有真的被找出来——这,才是最可怕最危险的地方。
比如,这个错误可能是某个马大哈新同事搞的;他本应该每次都先调用过程A初始化环境、再调用过程B执行需要的操作、最后调用过程C释放内存的。但他却想当然的以为“框架只需要初始化一次”,所以,他的代码是这样的:
init_env(); //call A
while (pData!= null) {
B(pData++);
C();
};
你在C中强制把指针赋为null,然后在B中检查、发现指针为空就跳过逻辑时,他的程序的确不再崩溃了——但你有没有注意到,B和C里面的逻辑其实也被跳过去了?
这就使得pData指向的数据未经处理即被丢弃;而这可能是非常非常严重、造成极大经济损失甚至生产事故、人身伤亡的。
你看,这么一改,本来会立即崩溃的、不那么严重的问题(因为这种问题容易定位和修复),现在变成了隐蔽的、难以发行的“丢订单”“丢告警”“丢信号”问题。
此时,可能的故障表现是,当用户只购买一份商品时,一切正常;但当他一次购买两份商品时,第二份丢失了……
或者,当设备发生轻微故障时,程序反应灵敏、运行良好;可设备被严重破坏、整个厂房即将被摧毁时,你的破程序却不紧不慢“安啦安啦,就是一个温度探测器离线的小问题,备份传感器还有很多呢。忽略就行了,明天找人换一下就好。没事没事”……然后,楼塌了,死了几百号人。
这种故障,在如今动辄几万几十万几百万行代码的系统里面,你如何定位?
相反,保持指针的“悬挂”状态,那么这个指针哪怕不在B过程引起崩溃,也会在C过程的free调用时立即崩掉——换句话说,现在,这个逻辑错误就被自动发现了。
这就避免了严重的软件设计缺陷被掩盖、被发布到资金相关甚至生死攸关的用户环境。
正因此,我在前面反复强调——程序崩溃是最理想、最优越、最轻微的故障;把它弄的不会崩溃了,反而是最可怕、最严重的故障。
当然了,请注意,我并没有说“free(p); p=null;”这种写法本身是不良设计——如果你像我一样,从来都是通盘考虑程序状态以及每个状态如何识别、确保自己不需要借助“悬挂指针”指示程序当前状态不正常,那么令p=null是可行的。
再强调一次,你可以把free后的指针置空;但可以这样做的前提是,不要通过“检查p==null”之类动作隐藏错误;该崩溃时,确保程序能够第一时间干净利落的崩掉。
否则,就会把本可以轻易揪出来、解决掉的小问题变成极难发现和定位的大问题的。
换句话说,别说C语言的free本来就做不到“自动置空”;就是能做到(比如C++里面可以把参数设计成引用类型),这事也不能做——就好像lint可以轻易发现free后没有置空指针、但它却故意不报告一样。
这是我们对同行的某种善意——或者说,对那些垃圾程序员的一种恶意: 我就是不想让你立即发现“这个指针是null是无效的”、然后用一行代码糊弄过去 。
因为,“指针不合法”这个东西太表面化了,绝不能用更表面化的“置null、绕过”处理掉——正规公司里,这是绝对过不去审核关的,明显没有发现root cause嘛。
换句话说,在实际的项目中,不该出现无效指针时出现了无效指针,其中隐含的错误几乎从不会如此的表面、可以不假思索的掩盖掉——允许你检查、绕开,这本身就是对程序质量的不负责任。
因此,重复一遍:哪怕可以做到“free后置null”,这件事也不能做。因为它可能掩盖错误。