编程精粹—— Microsoft 编写优质无错 C 程序秘诀 03:强化你的子系统

这是一本老书,作者 Steve Maguire 在微软工作期间写了这本书,英文版于 1993 年发布。2013 年推出了 20 周年纪念第二版。我们看到的标题是中译版名字,英文版的名字是《Writing Clean Code ─── Microsoft’s Techniques for Developing》,这本书主要讨论如何编写健壮、高质量的代码。作者在书中分享了许多实际编程的技巧和经验,旨在帮助开发人员避免常见的编程错误,提高代码的可靠性和可维护性。


不记录,等于没读。本文记录书中第三章内容:强化你的子系统。


上一章我们看到了 断言 的威力,相比编译器,它能够检查出更多的错误。断言的好处是:用户在错误发生时,可以自动地把它们检查出来。这同时揭示了断言的一个弱点:断言 静静地等待,直到错误出现。
断言无疑是强有力的工具,但只有断言还不够。更强大的是 子系统 完整性检查,它能 主动验证 子系统,在错误影响程序之前发现错误。针对标准 C 内存管理器的完整性检查能够检测空指针、内存泄漏以及非法使用未初始化或已释放的内存。完整性检查还可用于消除罕见行为,并迫使子系统重现错误,以便追踪和修复。

首先要了解的是,什么是 子系统 (subsystem) ?

子系统 指的是一个较大系统中的独立功能单元或组件。它具有独立的功能和接口,可以单独开发、测试和维护,但同时又与其他子系统协同工作,共同实现整个系统的功能。

操作系统 是一个大的系统,它包含许多子系统,比如:

  • 文件系统子系统:负责文件的存储、检索和管理。
  • 网络子系统:负责处理网络协议和数据传输。
  • 内存管理子系统:负责内存的分配和管理。

每个子系统都执行特定的任务,并通过系统调用接口与其他子系统和应用程序交互。在小型嵌入式系统中,模块化代码 也可以看做是一个子系统。

通常 子系统隐藏实现细节,只对外提供一些简单的接口,并且隐藏的实现细节可能相当复杂。比如文件系统一般只提供 5 个基本接口函数:打开、关闭、读、写和创建文件,但这些操作通常需要大量复杂的代码作支撑。

程序员在调用这些接口函数时,可以增加调试检查,这样就能毫不费力的进行许多错误检查。这正是本章的核心理念,即强化你的子系统。设想一下这个场景:

一场足球比赛可能有 5 万名球迷现场观赛,但只需要几个人就能完成检票。当然我们规定这些观众要从入口进入。程序也有这样的门,它们是进入子系统的入口。

要构建这个关键入口,我们可以将子系统提供的接口函数 再次封装。一方面可以在封装函数内部增加调试或断言,用来捕捉错误;另一方面在更换另一家供应商提供的子系统时,可以将 更改 限制到封装函数层面,而不必修改应用层代码

下面,我们以内存管理子系统为例,看看标准库给出的接口( mallocfreerealloc) 有哪些容易犯错的地方,然后我们再次封装这些标准接口,在其中添加断言和调试代码,然后再提供给上层应用使用。

要消除随机特性 ─── 使错误可再现

malloc 函数存在以下未定义行为:

  1. 根据 ANSI 标准,请求 malloc 分配长度为零的内存块时,其结果未定义;
  2. 如果 malloc 分配成功,那么它返回的内存块的内容未定义,可以是零,也可以是内容随机的无用信息。

malloc 函数封装时,要将上述的未定义行为消除,或者利用断言确保不会使用到:

#define bGarbage  0xCCbool fNewMemory(void **ppv, size_t size) { byte **ppb = (byte **)ppv; ASSERT(ppv != NULL && size != 0); *ppb = (byte*)malloc(size); if(*ppb == NULL)return false;#ifdef DEBUG memset(*ppb, bGarbage, size);	//填充特定内容if(fCreatBlockInfo(*ppb, size) == false) {free(*ppb);					//无法创建日志信息,模拟内存分配错误*ppb = NULL;return false;}     
#endif return true; 
} 

这个函数比直接调用 malloc 函数要复杂多了,下面来解析这个函数:

  • 多了一个 void **ppv 指针参数,返回值变成了 bool 型。这样的改写有两个好处:
    • malloc 函数的返回值有两种含义:内存申请失败 (返回 NULL) 或者指向已分配内存块的指针(返回 非 NULL)。现代的编程习惯不建议这样做,因为它违反了单一职责原则。 fNewMemory 函数则不同,它的返回值表示内存申请是否成功,如果内存申请成功,已分配的内存块由参数 *ppv 指向,如果内存申请失败,它负责将 *ppv 设置为 NULL
    • 使用起来, fNewMemory 函数更清晰。如果使用 malloc 函数,形式如下:
    char *pbBlock;
    bpBlock = (char *)malloc(32);
    if(bpBlock != NULL)// 成功
    else//失败
    
    而使用 fNewMemory 函数,形式如下:
    char *pbBlock;
    if(fNewMemory(&pbBlock, 32) )//成功
    else//失败
    
  • malloc 分配长度为零的内存块时,其结果未定义。fNewMemory 函数使用 断言 对这种情况进行检查,如果请求分配长度为零的内存块,则会触发断言。
  • 如果 malloc 分配成功,那么它返回的内存块的内容未定义,可以是零,也可以是内容随机的无用信息。fNewMemory 函数通过额外的调试代码,对新申请的内存块填充已知的数据。注意,函数中填充的已知数据是 0xCC(由宏 bGarbage 定义),而不是 0 ,这样做的目的是增加暴露错误的可能性,你可以根据自己的系统特性选择一个数值,让这个数值尽可能看起来离奇而且无用,这样你的程序就不会错误的使用它,而是会崩溃或异常,让你不得不去处理。
  • 额外的调试代码调用了 fCreatBlockInfo 函数,这是 内存跟踪接口 中的一个函数,它记录申请到的内存地址和大小,用来辅助完整性检查。后面还会介绍更多内存跟踪接口。

冲掉无用的信息,以免被错误地使用

free 函数的问题是:

  1. 如果给 free 函数传递无效的指针,其结果未定义
  2. 已经被释放的内存仍包含着对软件而言有效的数据,如果因为软件错误,程序误用了已经释放的内存,可能不会立即出错。

为了解决上面的问题,我们重新封装 free 函数:

void FreeMemory(void *pv) {ASSERT(pv != NULL)#ifdef DEBUGmemset(pv, bGarbage, sizeofBlock(pv));FreeBlockInfo(pv);
#endiffree(pv);
}

让我来解释下这个函数:

  • 首先使用 断言 捕获参数为 NULL 的情况,应用程序将 NULL 传递给 free 函数是无意义的。
  • 将要释放的内存区域用特定的数值填充 (数值由宏 bGarbage 定义),这块区域的内容会变得无用。完成这一步,只需要调用 memset 函数,但问题是,这需要知道被释放的内存大小。为此我们调用 sizeofBlock 函数。这是第 2 个 内存跟踪接口 提供的函数,调用这个函数可以获取被释放内存的大小的原理是:当使用 fNewMemory 函数分配内存时,已经记录下申请到的内存地址和大小,sizeofBlock 函数利用内存地址(已知量) 来获取该块内存大小。另外, sizeofBlock 函数还顺便对 pv 指针进行了检查,确认它是由 fNewMemory 函数分配的。这当然是可以做到的,因为 内存跟踪接口 知道每个内存分配块的细节。
  • 函数 FreeBlockInfo 是第 3 个内存跟踪接口 提供的函数,用于释放跟踪数据。

realloc 函数的问题是:

  1. realloc 函数传递无效的指针,其结果未定义。
  2. realloc 函数调用失败,则返回 NULL 。如果程序员没有意识到这一点,可能会写类似 my_ptr = realloc(my_ptr, NEW_SIZE) 的错误代码。当 realloc 调用失败时,my_ptr 就将指向 NULL,之前申请的内存块再也无法访问。
  3. 若缩小内存,释放的内存中仍包含着对软件而言有效的数据;若扩大内存,新增的内存数据是随机的。
bool fResizeMemory(void **ppv, size_t sizeNew)
{byte **ppb = (byte **)ppv;byte *pbResize;
#ifdef DEBUGsize_t sizeOld;
#endifASSERT(ppb != NULL && sizeNew != 0);#ifdef DEBUG     sizeOld = sizeofBlock(*ppb);if(sizeNew < sizeOld) {			//内存缩小,冲掉块尾释放的内容*memset(*ppb + sizeNew, bGarbage, sizeOld - sizeNew);} else if(sizeNew > sizeOld) {	//内存扩大,强迫realloc不能在原位置扩展空间byte *pbNew;if(fNewMemory(&pbNew, sizeNew)) {memcpy(pbNew, *ppb, sizeOld);FreeMemory(*ppb);		//冲刷掉原来的内容*ppb = pbNew;}}
#endifpbRsize = (byte *)realloc(*ppb, sizeNew);if(pbResize == NULL)return false;
#ifdef DEBUGUpdateBlockInfo(*ppb, pbResize, sizeNew);/*如果扩大,对尾部增加的内容进行初始化*/if(sizeNew > sizeOld)memset(pbResize + sizeOld, bGarbage, sizeNew - sizeOld);
#endif*ppb = pbResize;return true;
}

让我来解释下这个函数:

  • 使用断言捕获不应该发生的错误
  • 如果缩小内存,用特定数据冲洗掉要释放的内存,如果扩大内存,对新增内存初始化为特定数据。
  • 对于扩大内存,还有一层需要考虑。考虑一下,realloc 在扩大内存时,可能有两种动作,第一种是紧随着当前内存块的后面扩充适当的内存,这种是最理想的情况;第二种情况是在另一个位置申请全新的、足够大的内存块,然后将扩充前的内存数据拷贝到新的内存块,再将扩充前的内存块释放掉。后一种情况可能带来问题,因为 realloc 函数释放的内存块没有用特定数据冲洗。fResizeMemory 函数使用了一个小技巧来避免这个问题,即模拟 realloc 函数的行为:用fNewMemory 申请新的内存块,然后把原来内容拷贝到新块中,最后释放掉原来内存块。
  • 当内存扩大时,既然已经模拟了 realloc 函数的行为,是否可以在模拟完成后,即 *ppb = pbNew 语句后面执行 return true 返回?这样还可以提高运行速度。答案是绝不允许的!因为这会跳过正常代码的。要记住调试代码是多余的,最终是要从系统中去除的。因此调试代码决不能改变原有代码的执行顺序或跳过正常代码
  • fResizeMemory 函数在操作失败的情况下并不返问 NULL。此时,新返回的指针仍然指向原有的内存分配块,并且块内的内容不变。

不必担心调试版本增加的额外代码。调试版本本来就不必短小精悍,不必有特别快的响应速度,只要能满足程序员和测试者的日常使用要求就够了。

有些错误的难点在于虽然它并不经常发生,但却总是发生:不要让事情很少发生。如果发现子系统中有极罕见的行为,要千方百计地设法使其重现

你有过跟踪错误跟踪到了错误处理程序中,并且感到“这段错误处理程序中的错误太多了,我敢肯定它从来都没有被执行过”这种经历吗?肯定有!错误处理程序之所以往往容易出错,正是因为它很少被执行到。

保存调试信息,以便进行更强的错误检查

从调试的角度来看,内存管理程序是有问题的。创建的内存块大小只是在第一次创建时知道,随后就失去了这一信息。除了内存块大小,如果能够知道已经分配了多少次内存,每个内存块的位置在哪里,用处会更大。这就是编写 内存跟踪接口 的意义。通过编写内存跟踪接口,我们可以保存内存分配的信息,方便调试排错。内存跟踪接口源码见本书附录 B,这是一个很有价值的接口。

如果匪徒根本没打算出城,路障就没用了。不要等待错误发生。要“挨门挨户”的搜查错误:在程序中加上能够积极地寻找这种问题的调试代码。

如果你是售货员,那么当顾客到你那里准备购买毛衣和套装时,你应该先给顾客看套装,然后给顾客看毛衣。这样做可以增加销售额。因为顾客买了一件500美元的套装后,相比之下,一件80美元的毛衣就显得不那么贵了。但是如果你给顾客先看毛衣,那么80美元一件的价格可能顾客无法接受。

​ ——Robert Cialdini博士《影响力》

任何人只要花30秒就能想明白这个道理。可是,又有多少人花时间想过这一问题呢?

一点就透,更要主动思考:仔细设计程序的测试代码,任何选择都应该经过考虑。

当测试代码将错误限制在一个局部范围之内后,就通过断言把错误抓住,打断正常的工作,明确告知程序员。努力做到测试代码对程序员是透明的,所有测试和检查自动执行

小结:

  • 考察所编写的子系统,问自己“在使用这些代码时,程序员可能会犯什么错误。”在子系统中加上相应的断言和确认检查代码,以捕捉难于发现的错误和常见的错误。
  • 如果不能重现 BUG,就无法排除它们。找出程序中可能引起随机行为的因素,并将它们从程序的调试版本中清除。把“未定义”的内存单元设置成精心选择的常量值,是消除随机行为的一个例子。这样,如果某个代码引用了“未定义”内存,每次执行有问题的代码,每次都会得到相同的结果。
  • 如果所编写的子系统释放内存(或者其它资源),并因此产生了“垃圾信息”,那么要用已知的数据把它冲刷掉。否则,这些被释放了的数据就有可能仍被使用,而又不会被注意到。
  • 类似地,如果子系统中含有小概率行为,那么增加调试代码确保这些小概率行为一定发生。那些正常情况下不会执行的代码(通常是错误处理逻辑)最容易滋生BUG,这样做可以增加捕获这些BUG的概率。
  • 确保所编写的测试代码能在程序员无感的情况下起作用,最好的测试代码是不用知道其存在也能起作用。
  • 如果可能的话,把测试代码放到所编写的子系统中,而不要把它放到所编写子系统的外层。不要等到进行了系统编码后,才考虑其确认方法。在子系统设计的每一步,都要考虑如何对这一实现进行彻底地验证这一问题。如果发现这一设计难于测试或者不可能对其进行测试,那么要认真地考虑另一种不同的设计,即使这意味着用大小或速度作代价去换取系统的测试能力也要这么做。
  • 如果一个验证测试程序太慢或占用太多内存,在弃用它之前要三思而后行。切记,交付版本中并不会有验证测试代码。如果发现自己正在想“这个测试程序太慢、太大了”,那么要马上停下来问自己:怎样才能保留这个测试程序,并使它即快又小?






每一份打赏,都是对创作者劳动的肯定与回报。
千金难买知识,但可以买好多奶粉

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/352659.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

docker怎么拉取全部镜像,打包所有镜像

因为docker&#xff0c;所以我把电脑上之前的镜像全部打包出来了 你们也可以打包&#xff0c;我提供一个脚本&#xff0c;你运行即可 export_docker.sh #!/bin/bash# 导出目录 EXPORT_DIR"docker_images_backup" mkdir -p "$EXPORT_DIR"# 获取所有镜像 …

一文看懂!iThenticate查重报告全解读

在科研界&#xff0c;原创性和知识产权保护的重要性不言而喻。iThenticate作为全球领先的文献比对和不端检测工具&#xff0c;为广大科研工作者提供了一道坚实的防线保障。据统计&#xff0c;全球有超过16000家学术机构、出版社、高校与企业使用iThenticate查重系统&#xff0c…

Dubbo-使用zookeeper作为注册中心时节点的概述

本文内容很容易理解&#xff0c;会阐述当dubbo使用zookeeper作为注册中心时候&#xff0c;zookeeper节点是什么样子的 本文的代码使用的dubbo版本是2.7.x&#xff0c;几年前的版本了&#xff0c;但是不影响探究 首先我们创建一个简单的maven项目&#xff0c;然后写出一段dubb…

黑马苍穹外卖2 员工的增查改+异常处理+ThreadLocal

员工管理 新增员工 Controller&#xff1a; PostMapping//post类型的请求ApiOperation("添加员工")public Result save(RequestBody EmployeeDTO employeeDTO) {log.info("新增员工{}", employeeDTO);employeeService.save(employeeDTO);return Result.su…

有监督学习——梯度下降

1. 梯度下降 梯度下降&#xff08;Gradient Descent&#xff09;是计算机计算能力有限的条件下启用的逐步逼近、迭代求解方法&#xff0c;在理论上不保证下降求得最优解。 e.g. 假设有三维曲面表达函数空间&#xff0c;长(x)、宽(y)轴为子变量&#xff0c;高(z)是因变量&…

themleaf 页面弹层取值

themleaf 页面弹层取值 创作背景themleaf页面事件onbluronclick 页面参数提交 创作背景 个人在日常开发中&#xff0c;遇到了一个需求页面&#xff0c;页面交互较多&#xff0c;用到的事件也很丰富&#xff0c;特此记录&#xff0c;方便后续查找也方便有需要的开发者采用&…

软件测试期末复习

软件测试期末复习 Author 雨 2024年6月18日 1. 什么是软件测试 从一个通常为无限的执行域中选取合适的有限的测试用例&#xff0c;对程序所期望的行为进行动态验证的活动过程。 2. 软件测试的目的 尽早地发现软件的缺陷 3.什么是测试什么是缺陷 从软件内部看&#xff1a;软件开…

SpringCloudStream原理和深入使用

简单概述 Spring Cloud Stream是一个用于构建与共享消息传递系统连接的高度可扩展的事件驱动型微服务的框架。 应用程序通过inputs或outputs来与Spring Cloud Stream中binder对象交互&#xff0c;binder对象负责与消息中间件交互。也就是说&#xff1a;Spring Cloud Stream能…

探索Web Components

title: 探索Web Components date: 2024/6/16 updated: 2024/6/16 author: cmdragon excerpt: 这篇文章介绍了Web Components技术&#xff0c;它允许开发者创建可复用、封装良好的自定义HTML元素&#xff0c;并直接在浏览器中运行&#xff0c;无需依赖外部库。通过组合HTML模…

多尺度特征提取:原理、应用与挑战

多尺度 多尺度特征提取&#xff1a;原理、应用与挑战**原理****应用****挑战****总结** 多尺度特征提取&#xff1a;原理、应用与挑战 在计算机视觉、自然语言处理和信号处理等领域&#xff0c;有效地捕捉和解析数据的多种尺度特性是至关重要的。多尺度特征提取是一种技术&…

【机器学习】智能创意工厂:机器学习驱动的AIGC,打造未来内容新生态

&#x1f680;时空传送门 &#x1f50d;机器学习在AIGC中的核心技术&#x1f4d5;深度学习&#x1f388;生成对抗网络&#xff08;GANs&#xff09; &#x1f680;机器学习在AIGC中的具体应用&#x1f340;图像生成与编辑⭐文本生成与对话系统&#x1f320;音频生成与语音合成 …

SpringMVC01-初始SpringMVC

SpringMVC 回顾MVC 什么是MVC MVC是模型(Model)、视图(View)、控制器(Controller)的简写&#xff0c;是一种软件设计规范。是将业务逻辑、数据、显示分离的方法来组织代码。MVC主要作用是降低了视图与业务逻辑间的双向偶合。MVC不是一种设计模式&#xff0c;MVC是一种架构模…

高通Android 12 右边导航栏改成底部显示

最近同事说需要修改右边导航栏到底部&#xff0c;问怎么搞&#xff1f;然后看下源码尝试下。 1、Android 12修改代码路径 frameworks/base/services/core/java/com/android/server/wm/DisplayPolicy.java a/frameworks/base/services/core/java/com/android/server/wm/Display…

【LeetCode:2786. 访问数组中的位置使分数最大 + 递归 + 记忆化缓存 + dp】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

电感的本质是什么

什么是电感&#xff1f; 电感器件一般是指螺线圈&#xff0c;由导线圈一圈靠一圈地绕在绝缘管上&#xff0c;绝缘管可以是空心的&#xff0c;也可以包含铁芯或磁粉芯。 为什么把’线’绕成’圈’就是电感&#xff1f; 电感的工作原理非常抽象&#xff0c;为了解释什么是电感…

IntelliJ IDEA 使用 Maven 时不加载本地私服的最新版本快照(snapshot)JAR 包

IntelliJ IDEA 使用 Maven 时不加载本地私服的最新版本快照&#xff08;snapshot&#xff09;JAR 包 目录 IntelliJ IDEA 使用 Maven 时不加载本地私服的最新版本快照&#xff08;snapshot&#xff09;JAR 包1. 检查 settings.xml2. IDEA Maven 配置3. 强制更新 Snapshot4. 使用…

使用 C# 学习面向对象编程:第 8 部分

抽象方法 亲爱的读者&#xff0c;本文是 OOP 的第四大支柱&#xff0c;也是最后一大支柱。对于 OOP 初学者来说&#xff0c;这很容易让人困惑。因此&#xff0c;我们用非常简单的语言提供了一个示例。 “抽象用于管理复杂性。无法创建抽象类的对象。抽象类用于继承。” 例如…

端口映射工具下载?

天联是一款强大的端口映射工具&#xff0c;它能够帮助用户实现远程数据采集管理、异地统一管理、随时随地协同办公等多种场景的应用。无论您是医药、餐饮、商超等零售行业的企业&#xff0c;还是需要使用OA、CRM、ERP、财务进销存等系统的企业&#xff0c;甚至是使用视频监控设…

Python自动化测试面试题精选(一)

今天大家介绍一些Python自动化测试中常见的面试题&#xff0c;涵盖了Python基础、测试框架、测试工具、测试方法等方面的内容&#xff0c;希望能够帮助你提升自己的水平和信心。 项目相关 什么项目适合做自动化测试&#xff1f; 答&#xff1a;一般来说&#xff0c;适合做自…

前端菜鸡流水账日记 -- git管理工具(多版本)

哈喽哇&#xff0c;我又又又来了&#xff0c;其实之前就挺想进行一篇关于git管理工具的分享的&#xff0c;但是一直都没有来的及&#xff0c;直到今天&#xff0c;在学习的时候&#xff0c;&#xff0c;一个朋友新发现了一个vscode中的小插件&#xff0c;所以我就决定一起来分享…