编程精粹—— Microsoft 编写优质无错 C 程序秘诀 05:糖果机接口

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


不记录,等于没读。本文记录书中第五章内容:糖果机接口。


我们在第一章介绍了一些 方法,以便检测到更多的错误,比如启用所有可选的编译器警告、使用语法和可移植性检查工具等。但是这些措施只能查出所有错误的一小部分。
所以我们在第二章看到了 断言 的威力,相比编译器等工具,它能够检查出更多的错误。但断言也有弱点,它只能静静地等待,直到错误出现。
所以我们在第三章介绍了 子系统完整性检查,不再被动的等错误发生,而是“挨门挨户”的搜查错误。但这对研发人员的要求比较高,你必须知道哪里可能会发生错误,为了主动抓到错误,你得知道错误长什么样子。这可需要经验和艰苦的思考。
然后我们在第四章介绍了每个人都能轻松实践的、发现错误的最佳方法:使用调试器逐步执行所有新代码。
上面所有的内容都在帮助我们写出无错的函数。但是函数只是无错还不够,函数还必须易于使用,且不会引入意外的 BUG

本章将详细阐述如何设计 接口 (Interfaces),通常我们所提及的 API 接口,就是本章所聚焦的核心内容。在编程世界中,接口 扮演着至关重要的角色,它们定义了不同模块、组件或系统之间交互的规范。对于 C 代码而言,函数名及其参数列表实质上构成了我们所说的接口,它们定义了函数的 行为期望的输入

如果要降低 函数调用 带来的 BUG,每个函数需要有一个明确单一的目的,具有明确单一用途的输入和输出,具有可读性,并且理想情况下从不返回错误状态。具有这些属性的函数易于使用断言和调试代码进行验证,并且最大限度地减少必须编写的错误处理代码量

好的设计会引导人们去做正确的事情。不好的设计则会让事情一团糟。

有一次作者想在自动贩卖机购买黄油饼干。购买的步骤是:

  1. 确认商品金额
  2. 向贩卖机投入硬币
  3. 按下表示商品的编码
  4. 取走商品

黄油饼干售价 45 美分,编码为 21。作者向贩卖机投入硬币时满脑子都是数字 45,在输入商品编码时,他本应该按下 2 和 1,但却阴差阳错的按下了 4 和 5。售货机不会出错,它吐出了一个泡泡糖。
这里的关键在于,自动售货机的商品编码和金额都用数字表示是不好的设计,因为容易将编码和金额混淆。如果商品编码用字母表示,金额用数字表示就是一种好的设计方式,因为这样可以 防止 顾客出错。

程序员设计的函数接口也要符合好的设计原则:函数既要无错,还要使用起来安全。

不要在返回值中隐藏错误

getchar

getchar 是标准 C 库提供的函数, 用于从某个设备上读取一个字符或返回 EOF。单看函数名,我们会很自然的认为它返回一个 char 类型数据,就像下面所示的代码:

char c;
c = getchar();
if( c == EOF )// 处理内容

这段代码是有问题的:在不进行符号扩展的机器上,char 类型变量 c 总是整数,无法表示 EOF ,因为 EOF 定义为 int 类型,值为 -1,表示文件结束或出错。为了避免这一点,必须使用 int 类型变量来保存 getchar 函数的返回值

即便是有经验的程序员也容易在这里栽跟头。

molloc

molloc 也是标准 C 库提供的函数,用于返回分配的内存地址或 NULL(表示内存耗尽或出错);这类函数返回的值都不精确:有时返回有效数据,但另一些时候却返回不可思议的错误值。程序员可能会忘记对相应的错误进行处理,因为这类函数将错误隐藏在程序员极易忽视的正常返回值中。这是不好的设计。

更麻烦的是这类函数能写出虽然有缺陷,但表面上仍能工作的代码。直到碰到一连串不易发生的事件而导致这些代码失败。

接口的返回值规则

设计函数接口时,不要使用引起混淆的返回值:每个输出应该只代表一种数据类型。具体到 getchar 函数,我们可以设计一个封装函数:

bool new_getchar(char *pch)
{int ch;ch = getchar();if(ch == EOF)return false*pch = ch;return ture;
}

这个封装函数的返回值强调错误情况。

realloc

realloc 同样是标准 C 库提供的函数,用于重新分配内存块的大小。观察下面的代码:

pbBuf = (byte *)realloc(pbBuf, sizeNew);

这句代码可能有个严重的错误:如果 pbBuf 是指向要改变大小的内存块的唯一指针,那么当 realloc 函数调用失败,会把 NULL 填入 pbBuf,然后这个内存块就再也找不到了。

我们有多少次在要改变一个内存块的大小时,想到要把指向新内存块的指针存储到另一个变量中

所以realloc函数同样有引起混淆的返回值。我们在第 3 章介绍了 realloc 的封装函数,去掉其中的调试代码后,它的形式如下:

bool fResizeMemory(void **ppv, size_t sizeNew)
{byte **ppb = (byte **)ppv;byte *pbResize;pbRsize = (byte *)realloc(*ppb, sizeNew);if(pbResize == NULL)return false;*ppb = pbResize;return true;
}

使用这个封装函数,绝不会破坏原有指针。

再谈接口的返回值规则

将函数的返回值设计成单一功能,在很大程度上让我们设计出避免隐藏陷阱的接口。找出这些暗藏陷阱的唯办法是停下来思考所做的设计。检查输入和输出的各种可能组合,寻找可能引起问题的副作用。

要不遗余力地寻找并消除函数接口中的缺陷

编写功能单一的函数

realloc 函数的原型为:

void *realloc(void *pv, size_t size)

它的另一个问题是集多种功能于一身:

  • malloc 函数做的事情(当 pvNULL 时)
  • free 函数做的事情(当 size 为 0 时)
  • 实现内存块的缩小或扩大,扩大时,指针可能发生移动。
  • 扩大内存块时可能返回 NULL,缩小内存块总是会成功,返回的指针与 pv 相同。
  • pvNULLsize 为 0 ,结果未定义。

它在一个函数中完成了所有的内存管理工作,真不知道还要 mallocfree 干什么。这样的函数缺点在于:

  • 因为复杂,程序员难以掌握;它包含了如此多的细节。
  • 功能重复、多余
  • 为测试参数带来了困难(size 为 0 合法、pvNULL 也合法)。

不管出于什么样的理由编写了多功能的函数,都要把它分解为不同的功能。编写功能单一的函数:一个函数只做一件事。

输入参数值不要模棱两可

前面谈过了 函数的返回值 应具有单一功能,将这一建议应用于 函数的输入 也是成立的。比如下面的代码,是改变内存块大小,还是分配内存块或者释放内存块?

pbNew = realloc(pb, size);

都有可能。这取决于 pbsize 的值。但如果我们知道 pb 指向一个有效的内存块,size 是个非零的合法块长,我们立刻就知道该函数是改变内存块的大小。

明确的输入使人容易理解函数的行为,这对于提高程序的可维护性非常重要

再来看个例子,如下所示的代码实现字符串抽取,即从一个大字符串 strFrom 中抽取一个子字符串,放到 strTo 中:

char* CopySubStr( char* strTo, char* strFrom, size_t size ) {char* strStart = strTo;while(size-- > 0)strTo++ = strFrom++;*strTo='\0';return(strStart);
}

一个使用该函数的例子是:从一个组合串中抽出星期几:

static char* strDayNames = "SunMonTueWedThuFriSat";
char strDay[4];
// ……
ASSERT(day>=0 && day<=6);
CopySubStr(strDay, strDayNames+day*3, 3);

现在我们明白了 CopySubStr 的工作方式,但你看得出该函数的输入有问题吗?至少有以下问题:

  • 参数 strTo 和 strFrom 应该使用断言确保非 NULL
  • 参数 size 值不应大于 strFrom 指向的字符串长度

修改过的代码如下所示:

char* CopySubStr(char strTo, charstrFrom, size_t size) {char* strStart = strTo;ASSERT( strTo != NULL && strFrom != NULL );ASSERT( size <= strlen(strFrom) );while( size-- > 0 )strTo++ = strFrom++;*strTo='\0';reurn( strStart );
}

一开始就要为函数的输入参数选择严格的定义,并最大限度的利用断言

尽可能不返回失败

如果函数返回错误代码,那么每个调用该函数的地方都必须对错误值进行处理。这也许不是这个函数的最佳实现方式。如果发现自己在设计函数时要返回一个错误代码,那么要先停下来问自己:是否还有其它的设计方法可以不用返回该错误情况。

努力编写在给定有效输入的情况下不会失败的函数

考虑一个小问题,给定一个 ASCII 表,写一个函数,把其中的大写字母转换成对应的小写字母。我知道 C 库函数可以很好的完成这个需求,但请自己来设计一个。在解决这个问题时,需要注意给定的输入范围是整个 ASCII 表,所以输入不一定是大写字母,也可能是个逗号,因此需要考虑异常处理。

一个不好的实现方法是:当输入参数不是大写字母时,返回一个错误代码,比如 NULL、空字符或者 -1 :

char tolower(char ch) {if( ch >= 'A' && ch <= 'Z')return( ch + 'a'-'A');elsereturn(-1);
}

这个函数把错误信息和真正的数据混在了一起,而且更重要的事,这个函数大可不必返回错误代码:如果输入参数不是大写字母时,返回该参数 (不做任何改变)。

使程序在调用点明了易懂

设计一个将无符号数转为字符串的函数,转换后的字符串可以是十进制样式也可以是十六进制样式,比如无符号数 16,可以转换为字符串 "16" 或者 "0x10"

第一种方法:

#define BASE10 1 
#define BASE16 0 /* UnsignedToStr 这一函数将一个无符号的值转换成对应的字符串表示,
* 如果 fDecimal 为 TRUE,u 被转换成十进制表示;
* 否则,它被转换成十六进制表示。 
*/ 
void UnsignedToStr(unsigned u, char *strResult, flag fDecimal) 
{}

这种方法通过传递布尔参数fDecimal来表示十进制还是十六进制表示。

这是一种不合理的设计。

布尔参数常常表明设计者在设计这个函数时并没有深思熟虑。如果真的只需要布尔量的两种情况,就应该把函数拆成两个。

第二种方法:

void UnsignedToDecStr(unsigned u, char* str); 
void UnsignedToHexStr(unsigned u, char* str); 

拆成两个函数,一个函数只做一件事件。

通用的方法:

void UnsignedToStr(unsigned u, char* str, unsigned base);
{ASSERT(base == 2 || base == 8 || base == 10 || base == 16);...
}

把布尔参数改成通用参数,从而使 UnsignedToStr 函数更灵活,因为可能还需要转换为二进制、八进制的字符格式。我们需要在函数中增加断言来约束参数的范围

strcmp 函数对两个字符串进行比较,如果两个字符串相等返回0、如果第一个字符串小于第二个返回负数、如果第一个字符串大于第二个返回正数。

尽管这样设计函数接口可以完成字符串比较功能,但对于不熟悉strcmp函数的人来说毫无意义。如何设计新的接口,使得调用者更能抵御错误、更加可读?

一种解决办法用命名良好的宏包装strcmp函数,提高可读性的同时,在空间和速度方面也没有损失:

#define fStrLess(strLeft, strRight)  	( strcmp(strLeft, strRgiht) < 0 )
#define fStrGreater(strLeft, strRight)  ( strcmp(strLeft, strRight) > 0 )
#define fStrEqual(strLeft, strRight) 	( strcmp(strLeft, strRgiht)== 0 ) 

小结:

  • 易于使用和理解的函数:每个输入参数和输出都精确表示一种类型的数据;将错误和其它专用值混入输入参数和输出中只会使函数接口混乱。
  • 设计函数接口的原则:应迫使使用接口的程序员考虑所有重要细节(如处理错误条件)。不要让他们很容易忽视或忘记细节。
  • 考虑程序员必须如何调用你的函数。寻找函数接口中的缺陷,这些缺陷可能导致程序员无意间引入错误。尤其重要的是:努力编写永远成功的函数,这样调用者就不必做任何的错误处理。
  • 函数接口要具有可读性,程序员容易理解这些函数的调用,可以减少BUG。魔法数和布尔量参数都与这一目标背道而驰。
  • 将多功能函数拆分成单独功能的多个函数,不仅可以通过合理的函数命名来增加程序的可读性,而且可以用更严格的断言自动地检查不合理的参数。






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

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

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

相关文章

Word 文本框技巧2则

1 调整大小 一种方法是&#xff0c;选中文本框&#xff0c;周围出现锚点&#xff0c;然后用鼠标拖动来调整大小&#xff1b; 精确按数值调整&#xff0c;在 格式 菜单下有多个分栏&#xff0c;一般最后一个分栏是 大小 &#xff1b;在此输入高度和宽度的数值&#xff0c;来调整…

MySQL的数据存储一定是基于硬盘吗?

一、典型回答 不是的&#xff0c;MySQL也可以基于内存的&#xff0c;即MySQL的内存表技术。它允许将数据和索引存储在内存中&#xff0c;从而提高了检验速度和修改数据的效率。优点包括具有快速响应的查询性能和节约硬盘存储空间。此外&#xff0c;使用内存表还可以实现更高的复…

【C++】类和对象(三)构造与析构

文章目录 一、类的6个默认成员函数二、 构造函数干嘛的&#xff1f;语法定义特性综上总结什么是默认构造函数&#xff1f; 三、析构函数干嘛的 &#xff1f;语法定义析构顺序 一、类的6个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。空类中并不是真的什么…

Mac数据如何恢复?3 款最佳 Mac 恢复软件

如果您认为 Mac 上已删除的文件永远丢失了&#xff0c;那您就大错特错了&#xff01;实际上&#xff0c;即使您清空了 Mac 上的垃圾箱&#xff0c;也有许多解决方案可以帮助您恢复已删除的文件。最好的解决方案之一是 Mac 恢复删除软件。最好的Mac 恢复删除应用程序可以轻松准确…

反射机制详解

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a;Java从入门到精通 ✨特色专栏&#xff…

SM9加密算法:安全、高效的国产密码技术

随着信息技术的飞速发展&#xff0c;网络安全问题日益凸显。加密算法作为保障信息安全的核心技术&#xff0c;受到了广泛关注。在我国&#xff0c;一种名为SM9的加密算法逐渐崭露头角&#xff0c;凭借其卓越的安全性能和高效计算能力&#xff0c;成为了新一代国产密码技术的代表…

常用的Java日志框架:Log4j、SLF4J和Logback

日志是软件开发中不可或缺的一部分&#xff0c;它有助于记录应用程序的运行状态、调试问题和监控系统。Java中有多个流行的日志框架&#xff0c;如Log4j、SLF4J和Logback。 一、Log4j 1.1 什么是Log4j&#xff1f; Log4j是Apache基金会开发的一个开源日志框架&#xff0c;它…

Milvus跨集群数据迁移

将 Milvus 数据从 A 集群&#xff08;K8S集群&#xff09;迁到 B 集群&#xff08;K8S集群&#xff09;&#xff0c;解决方案很多&#xff0c;这里提供一个使用官方 milvus-backup 工具进行数据迁移的方案。 注意&#xff1a;此方案为非实时同步方案&#xff0c;但借助 MinIO 客…

C++基础std::bind

目录 说明 举例子&#xff1a; 说明 std::bind是一个函数模板&#xff0c;用于创建一个可调用对象&#xff0c;该对象可以在稍后的时候被调用。bind的作用是将函数与参数绑定在一起&#xff0c;在调用时可以自动传入预定的参数值。 std::bind的基本语法如下&#xff1a; templ…

1. zabbix监控服务器部署

zabbix监控服务器部署 一、监控的作用1、监控的方式2、zabbix监控获取数据的方式 二、zabbix server部署1、确保时间同步2、添加epel源3、添加zabbix仓库4、安装zabbix服务端软件5、在数据库创建zabbix需要的表、授权用户6、编辑zabbix server配置文件&#xff0c;指定数据库连…

在WordPress中使用AI的实用方法:入门级

随着人工智能&#xff08;AI&#xff09;的快速发展&#xff0c;WordPress平台上引入了越来越多的工具和插件&#xff0c;为网站管理员提供了强大的功能。这些工具不仅可以提升网站的用户体验&#xff0c;还能简化网站管理过程。本文将介绍几种在WordPress中使用AI的实用方法&a…

广州化工厂可燃气体报警器检定检验:安全生产新举措显成效

随着科技的不断发展&#xff0c;可燃气体报警器的检定检验技术也在不断进步。 广州的一些化工厂开始采用先进的智能检测系统和数据分析技术&#xff0c;对报警器的性能进行更加精准和全面的评估。 这些新技术不仅能够提高检定检验的效率和准确性&#xff0c;还能够为化工厂的…

大数据的力量:推动战略决策和业务转型

在当前全球化的时代背景下&#xff0c;国际间的联系日益紧密&#xff0c;世界变得更加互联互通。面对各种危机&#xff0c;数据驱动决策和分析显得愈发重要。从医学研究到市场趋势分析&#xff0c;大数据技术在各个领域发挥着关键作用&#xff0c;推动着一场深刻的变革浪潮。 大…

打开IE自动跳转EDGE的解决方法

目录 1. 创建快捷方式的解决方案 2. 其他可以尝试但未必靠谱的方法 2.1 通过设置EDGE浏览器实现 2.2 设置internet属性 2.3 BHO拓展管理 找到Windows10中的IE浏览器的方法&#xff1a; WIN Q&#xff0c;打开搜索栏&#xff1b;键入IE&#xff0c;即可看到IE浏览器 1. …

Java Stream流应用

Stream流的核心方法 Stream流的方法主要包含如图的几种 提供部分应用场景做个思考&#xff1a; (1)从员工集合中筛选出salary大于8000的员工&#xff0c;并放置到新的集合里。 (2)统计员工的最高薪资、平均薪资、薪资之和。 (3)将员工按薪资从高到低排序&#xff0c;同样薪资…

深度理解微信小程序技术架构:从前端到后台

在当今移动互联网的时代&#xff0c;微信小程序作为一种轻量级、便捷的应用形式&#xff0c;已经成为许多用户和开发者的首选。本文将深入探讨微信小程序的技术架构&#xff0c;从前端视角到后台支撑&#xff0c;为读者全面解析这一新兴应用形式的奥秘。 #### 一、微信小程序的…

opencv中凸包运算函数convexHull()的使用

操作系统&#xff1a;ubuntu22.04OpenCV版本&#xff1a;OpenCV4.9IDE:Visual Studio Code编程语言&#xff1a;C11 1.功能描述 该函数cv::convexHull用于寻找一组二维点集的凸包&#xff0c;采用的是Sklansky算法[242]&#xff0c;当前实现中具有O(N logN)的时间复杂度。 1…

2024: 有效使用OKR的10个技巧

2023年是许多前所未有的一年。从真正意义上讲&#xff0c;这一年让我们为不可预测的事情做好了准备&#xff0c;也为不确定的事情提供了训练。在我们身边发生了这么多事情&#xff0c;而下一步的行动却依然不甚明朗的情况下&#xff0c;领导者们更应该开始制定战略&#xff0c;…

Linux服务器挖矿病毒处理

文章目录 Linux服务器挖矿病毒处理1.中毒表现2.解决办法2.1 断网并修改root密码2.2 找出隐藏的挖矿进程2.3 关闭病毒启动服务2.4 杀掉挖矿进程 3. 防止黑客再次入侵3.1 查找异常IP3.2 封禁异常IP3.3 查看是否有陌生公钥 补充知识参考 Linux服务器挖矿病毒处理 情况说明&#x…

FuTalk设计周刊-Vol.033

&#x1f525;AI漫谈 热点捕手 1、Stable Video Diffusion —— Stable Diffusion 推出的 AI 生成视频模型 Stable Video Diffusion 也是开源的&#xff0c;可以免费下载部署。支持文本/图片生成视频&#xff0c;最高支持 576*1024 分辨率 25 帧。 链接https://huggingface.…