编程精粹—— Microsoft 编写优质无错 C 程序秘诀 07:编码中的假象

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


不记录,等于没读。本文记录书中第七章内容:编码中的假象。


有些编程实践非常危险,永远不应使用。它们中的大多数明显具有风险,但也有些看似相当安全,甚至令人向往,因为它们满足需求而没有明显的危险。这些危险的编码实践其实是披着羊皮的狼。为什么不应该引用刚刚释放的内存?为什么在全局或静态存储中传递数据是有风险的?为什么应该避免寄生函数?为什么依赖 ANSI 标准中列出的每一个细枝末节是不明智的?

这里先解释 寄生函数。在编程领域,“parasitic functions”(寄生函数)通常指那些依赖外部状态或副作用而工作的函数。这些函数不具备独立性,因为它们的行为依赖于外部环境,而不是纯粹的输入参数。这样的函数往往难以预测、测试和维护。以下是寄生函数的一些典型特征:

  • 依赖全局变量:函数依赖于全局变量的状态,这使得它们在不同的上下文中表现不一致。
  • 修改外部状态:函数在运行时改变了外部变量或状态,而不仅仅是返回一个结果。
  • 副作用:函数除了返回值外,还对程序的其他部分产生影响,如打印输出、修改文件等。
  • 依赖环境:函数的行为依赖于外部环境或系统状态,如系统时间、配置文件等。

寄生函数的存在会增加代码的复杂性和错误率,因为它们不遵循“单一职责原则”(SRP)和“函数纯度”(pure function)的理念。为了提高代码的可维护性和可靠性,编程时应尽量避免使用寄生函数,而应设计独立、可预测和易于测试的函数。

注意到底引用了什么

memchr 函数的作用是在内存块中查找第一次出现的特定字符。如果在内存块中找到了该字符,则返回指向该字符的指针,否则,返回 NULL 。一个正确的实现代码如下所示:

void *memchr(void *pv, unsigned char ch, size_t size) {unsigned char *pch = (unsigned char *)pv;while(size-- > 0){if(*pch == ch)return(pch);pch++;}return(NULL);
}

如果有程序员想要追求更快的速度,那么他可以使用一些奇技淫巧来去除范围检查:只要在内存块之后的第一个位置存放 ch 字符,这样总是可以找到 ch 字符。只要能保证总是可以找打指定字符,那么就可以不用检查内存范围(这个内存范围内一定有待查字符)。或许你会有疑问,在内存块之后放置一个字符,不是破坏其它内存数据了吗?是的,但是有办法补救,我们会先将这个位置数据存储下来,在函数返回前,再将数据放回原位置,堪称完美。代码如下:

void *memchr(void *pv, unsigned char ch, size_t size) {unsigned char *pch = (unsigned char *)pv;unsigned char *pchPlant;unsigned char chSave;			pchPlant = pch + size;	//pchPlant 指向要被查寻的内存块后面的第一个字节chSave = *pchPlant;		//保存这个区域的数据*pchPlant = ch;			//设置数据位 ch ,确保函数一定能找到 chwhile(*pch != ch)pch++;*pchPlant = chSave;		//恢复数据return((pch == pchPlant)? NULL : pch);
}

巧妙吗?通过保证 memchr 总能找到 ch,这样就可以删除范围检查,使循环速度加倍。但是这样可靠吗?

并不可靠。这少有以下问题:

  • 如果 pv 指向的是只读存储器,那么在 pchPlant 处存放字符 ch 就不起作用。
  • 如果 pchPlant 指向映射到 I/O 的存储器,那么向该位置写操作的后果就不可预测。
  • 如果待查找的内存块恰好位于合法内存的最后位置,那么 pchPlant 指向非法区域,向这个位置写操作会引起存储故障,可能会终止整个程序。
  • 如果 pchPlant 指向并发进程共享的数据区域,则可能造成其它进程数据错误。

不要引用不属于你自己的存储空间

再看一个有些微妙的错误的例子:释放链表的子窗口。代码简化如下:

void FreeWindowsTree(windows *pwndRoot) {if(pwndRoot != NULL) {window *pwnd;for(pwnd = pwndRoot->pwndChild; pwnd != NULL; pwnd = pwnd->pwndSibling)FreeWindowTree(pwnd);	//释放子窗口...}
}

让我们看一下 for 循环体,它按照以下步骤执行:

  1. 初始化 pwnd :pwnd = pwndRoot->pwndChild;
  2. 判断条件: pwnd 是否为 NULL 。如果是,执行步骤 3;否则循环结束。
  3. 执行函数 FreeWindowTree(pwnd) :释放 pwnd 指向的存储块。
  4. 更新 pwnd :pwnd = pwnd->pwndSibling,然后执行步骤 2。

问题出现在步骤 4 上。更新 pwnd 时,表达式 pwnd->pwndSibling 引用了已经释放的内存数据。有些程序员并不认为这样有什么问题,刚刚存储区还好好的,也没做什么影响它的事,而且在机器上运行这个程序,并有任何的异常。

关键的是,一旦释放 pwnd 指向的内存块,那么 pwnd->pwndSibling 的值是什么呢?是一堆垃圾。引用已经释放的存储区是非法的,在释放过程中,存储管理程序可能以任何方式使用这些内存,而你并不能控制存储管理程序,因为它是操作系统提供的。如果上述程序能正常运行,也只是凑巧而已。

仅取所需

编写一个无符号数转字符串的函数,一般步骤是:

  1. 获取数字的个位数,转换成字符
  2. 将数字缩小 10 倍
  3. 判断数字是否大于 0 ,如果是,执行步骤1,否则转换完成。

唯一的问题是,这样转换出来的字符串是倒置的,比如数字 123,通过上述算法得到的字符串是 “321”。所以,为了获取正确的顺序,转换结束后要进行字符串反转操作。有些程序员觉得这样做效率低下,他们给出了新的算法,反向生成字符串,以便正确表示数字,代码如下:

char *UnsToStr(unsigned u, char *str)  { char *pch; ASSERT(u <= 65536); pch = &str[5]; 		//这里假设 str 指向的内存足够大,能存储 u 的最大值*pch = '\0'; do *--pch = u % 10 + '0'; while((u /= 10) > 0); return pch; 
}

这个函数的问题是,str 指向的存储区有多大,你并不知道。但是,函数却假设它足够大。调用者并不一定知道这个函数基于的假设。比如调用者确定自己的数据在 0-255 以内,就可能只申请 4 个字节的内存空间:

char strScore[4]; 
UnsToStr(UserScore, strScore); 	

这样 UnsToStr 函数会破坏 strScore 数组后面的内存数据。一个编程经验是:尽一切可能避免依赖。你的每一个依赖都可能是将来问题的原因。

不要在全局或静态存储中传递数据

还是以编写一个无符号数转字符串的函数为例。在上一节中,我们说不能假设 str 指向的存储区足够大。所以这次,我们在函数内部定义一个足够大的静态数组:

char *strFromUns(unsigned u) {static char strDigits[] = "?????" ; 	//5个字符 + '\0'char *pch;ASSERT(u <= 65535);pch = &strDigits[5];ASSERT(*pch == '\0');do*--pch = u % 10 + '0' ;while((u /= 10) > 0);return(pch);
}

一旦使用全局或静态存储区传递数据,就意味着这个函数不具备可重入性。比如连续将两个无符号数转换成在字符串:

strHighScore = strFromUns(HighScore);
strThisScore = strFromUns(Score);

第二次调用会将第一次转换的结果覆盖掉!

一些观点

  • 任何时候,只要不编写直观代码,就是自找麻烦!

  • 用一把螺丝刀撬开油漆罐的盖子,然后又用这把螺丝刀搅拌油漆,这是家庭维护中最熟悉的举动之一。人们知道这样做会损坏螺丝刀,不应该如此,为什么还要用螺丝刀来搅拌油漆呢?原因在于,这样做在当时很方便,而且能够解决问题。

  • 使用过某个工具后,你有把它物归原位的习惯吗?据我观察,基本上没有人这么做。所以等到再次用到这个工具的时候,他们会费时间到处找。为什么不放回原位呢,因为用完一扔最方便。
    我很警惕那些怎么方便就怎么来的人。他们常常会以牺牲他人的方式方便自己。

  • 在 Microsoft,那些理解产品内部工作原理的人,会更多的编写新代码。对项目了解很少的人则把时间花在阅读别人的代码、修改别人的BUG以及对已有功能做少量的局部性增补。这种安排很有意义。如果你不理解系统,就不能给系统增加主要功能。

  • 如果你发现自己编写的代码用了较多技巧,那么停止编写代码并寻找别的解决方法。如果一个算法不直观,却产生了正确的结果,那么这个算法的错误同样也会不明显。因此,编写直观代码才是真正的聪明人。

小结

  • 如果你在处理不属于你的数据,哪怕是临时的,也不要写入它。虽然你可能认为读取数据总是安全的,但请记住,读取内存映射的 I/O 可能会对硬件造成危害。
  • 一旦释放了内存,不要再引用它。引用已释放的内存会导致许多种错误。
  • 为了效率,你可能会想在全局或静态缓冲区中传递数据,但这是一个充满危险的捷径。如果你编写了一个函数,它所创建的数据只对调用者有用,请将数据返回给调用者,或者保证你不会意外地更改这些数据。
  • 不要编写依赖于其他函数具体实现的函数。
  • 编程时,按照程序设计语言原来的本意,编写清晰、准确的代码。避免使用可疑的编程习惯,即使语言标准保证它们能工作。记住,标准是会改变的。
  • 从逻辑上看,用 C 语言高效地表达一个概念似乎也会生成同样高效的机器代码,但事实并非如此。在将一段清晰的多行 C 代码压缩成单行代码之前,请确保你因此得到了更好的机器代码。即便如此,请记住局部效率的提升通常难以察觉,而且通常不值得破坏代码的可读性。
  • 最后,不要像律师写合同那样编写代码。如果一个普通程序员不能阅读和理解你的代码,那它就太复杂了;请使用更简单的语言。






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

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

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

相关文章

二进制炸弹的fp是什么?

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

【算法】优先级队列-基础与应用

优先级队列&#xff08;Priority Queue&#xff09;是一种特殊的队列类型&#xff0c;它允许在其元素中分配优先级。与传统的先进先出&#xff08;FIFO&#xff09;队列不同&#xff0c;优先级队列中元素的出队顺序取决于它们的优先级。优先级较高的元素会被优先处理&#xff0…

【Qt笔记①】帮助文档、窗口、按钮、信号和槽、lambda表达式

学习第一天&#xff1a;2024-3-9 文章目录 Qt creator 快捷键帮助文档默认生成的main.cpp逐行解释核心类帮助文档的查阅方法-①代码创建按钮第一个第二个对窗口的其他设置 对象树窗口坐标系信号和槽&#xff08;优点&#xff1a;松散耦合&#xff09;帮助文档的查阅方法-②找信…

轨迹规划系列之S型速度曲线绝对值定位功能块(Codesys源代码)

1、轨迹规划的作用 轨迹规划的作用(前馈速度+位置插补)-CSDN博客文章浏览阅读13次。轨迹规划可以产生运动控制系统位置闭环控制所需要的前馈速度和插补位置值。前馈速度+PID位置闭环控制典型应用可以参考下面文章链接:1、S7-1200PLC和V90伺服通过工艺对象实现定位控制S7-1200…

表驱动法 -优化逻辑分支

表驱动法 -优化逻辑分支 定义 表驱动法&#xff08;Table-Driven Approach&#xff09;是一种编程模式&#xff0c;可以将输入变量作为直接或间接索引在表里查找所需的结果或处理函数&#xff0c;而不使用逻辑语句&#xff08;if-else 和 switch-case&#xff09;。索引表可以…

VMware与windows的共享文件夹没找到怎么办?

如果这样子添加&#xff0c;在ubuntu中还是没能找到。开机后有的时候仍然未发现共享文件夹。 二、解决办法 使用如下指令&#xff1a; sudo mount -t fuse.vmhgfs-fuse .host:/ /mnt/hgfs -o allow_other /mnt/hgfs/ 是挂载点&#xff0c;也可以指定其它挂载点 -o allow_other…

DHCP原理1-单个局域网出现多个DHCP服务器会发生什么

1. 背景 DHCP全称是Dynamic Host Configuration Protocol。其协议标准是RFC1541&#xff08;已被RFC2131取代&#xff09;&#xff0c;主要实现服务器向客户端动态分配IP地址&#xff08;如IP地址、子网掩码、网关、DNS&#xff09;和配置信息。其系统架构是标准的C/S架构。RFC…

自学SAP是学习ECC版本还是S4版本?

很多人想学SAP&#xff0c;问我应该学ECC版本还是S4版本&#xff0c;我的建议如果你是自学的话&#xff0c;我个人建议使用ECC版本就行&#xff0c;因为这两个版本前台业务和后台配置的操作差异并不大&#xff0c;主要差异在于数据库的差异&#xff0c;前台业务操作和后台系统配…

OS复习笔记ch12-1

文件系统 概述 文件是大多数应用程序的核心要素&#xff0c;文件系统是操作系统对用户来说最重要的部分之一。 本章的主要内容见下图&#xff1a; 文件&#xff0c;大家耳熟能详的就是的docx、pdf、jpg、MP4等各种后缀文件&#xff0c;根据任务需要文件又分成了文本、图片、…

超越YOLOv8,飞桨推出精度最高的实时检测器RT-DETR!

众所周知&#xff0c;实时目标检测( Real-Time Object Detection )一直由 YOLO 系列模型主导。 飞桨在去年 3 月份推出了高精度通用目标检测模型 PP-YOLOE &#xff0c;同年在 PP-YOLOE 的基础上提出了 PP-YOLOE 。后者在训练收敛速度、下游任务泛化能力以及高性能部署能力方面…

我国目前常用的卫星影像星座有哪些(高分二号、高分七号、吉林一号、高景一号······)

​ 点击下方全系列课程学习 点击学习—>ArcGIS全系列实战视频教程——9个单一课程组合系列直播回放 点击学习——>遥感影像综合处理4大遥感软件ArcGISENVIErdaseCognition 中国目前的遥感卫星在数量、种类和应用领域上都取得了显著进展&#xff0c;覆盖了陆地、气象、海…

ViT:5 Knowledge Distillation

实时了解业内动态&#xff0c;论文是最好的桥梁&#xff0c;专栏精选论文重点解读热点论文&#xff0c;围绕着行业实践和工程量产。若在某个环节出现卡点&#xff0c;可以回到大模型必备腔调或者LLM背后的基础模型重新阅读。而最新科技&#xff08;Mamba,xLSTM,KAN&#xff09;…

Java17 --- SpringSecurity之前后端分离处理

目录 一、实现前后端分离 1.1、导入pom依赖 1.2、认证成功处理 1.3、认证失败处理 1.4、用户注销处理 1.5、请求未认证处理 1.6、跨域处理 1.7、用户认证信息处理 1.8、会话并发处理 一、实现前后端分离 1.1、导入pom依赖 <dependency><groupId>co…

【python】python股票量化交易策略分析可视化(源码+数据集+论文)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

【fastapi+mongodb】使用motor操作mongodb(三)

本篇文章介绍mongodb的删和改&#xff0c;下面是前两篇文章的链接&#xff1a; 【fastapimongodb】使用motor操作mongodb 【fastapimongodb】使用motor操作mongodb&#xff08;二&#xff09; delete delete 的用法基本和查找一致&#xff0c;包括delete_one&#xff08;删除…

借助AI快速提高英语听力:如何获得适合自己的听力材料?

英语听力是英语学习中的一个重要组成部分&#xff0c;它对于提高语言理解和交流能力至关重要。可理解性学习&#xff08;comprehensible input&#xff09;是语言习得理论中的一个概念&#xff0c;由语言学家Stephen Krashen提出&#xff0c;指的是学习者在理解语言输入的同时&…

时空预测 | 基于深度学习的碳排放时空预测模型

时空预测 模型描述 数据收集和准备&#xff1a;收集与碳排放相关的数据&#xff0c;包括历史碳排放数据、气象数据、人口密度数据等。确保数据的质量和完整性&#xff0c;并进行必要的数据清洗和预处理。 特征工程&#xff1a;根据问题的需求和领域知识&#xff0c;对数据进行…

Canvas绘制图片和区域

如何使用Canvas在图片上绘制区域&#xff1f; 一. 首先&#xff0c;我们需要初始化三个canvas画布&#xff08;初始化Canvas&#xff09; initCanvas() {// 初始化canvas画布let canvasWrap document.getElementsByClassName("canvas-wrap");this.wrapWidth canva…

Android中如何动态的调整Dialog的背景深暗

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 在 Android 开发中&#xff0c;当你使用 Dialog 或 DialogFragment 时&#xff0c;可以通过设置 Window 的背景变暗来突出它的可见性。这个效果…

【密码学】分组密码

文章目录 分组密码的模式分组密码与流密码模式明文分组与密文分组 ECB模式ECB定义ECB特点对ECB模式的攻击改变分组顺序攻击 CBC模式CBC定义初始化向量IVCBC特点对CBC模式的攻击对初始向量进行反转攻击填充提示攻击 CFB模式CFB定义对CFB模式的攻击重放攻击 OFB模式OFB定义CFB模式…