C语言的缺陷/错误处理问题探讨

news2025/1/13 15:41:48

 最近遇到一个问题,先看看如下代码:

uint8_t Bcd2Dec01(uint8_t bcd)
{
	uint8_t one = (bcd & 0x0F);
	uint8_t ten = (bcd & 0xF0) >> 4;

	if ((one > 9) || (ten > 9))
	{
		printf("请输入合法的BCD码!");
		return 0;
	}

	return one + (ten * 10);
}

这是一个将单字节bcd码转成十进制数的函数,传入bcd码,并通过返回值返回转换后的十进制数据。

但是在写函数的时候发现一个问题不太好解决,就是传入的可能是一个不合法的bcd码,所以需要进行参数校验,但是因为有带数据的返回值,所以如果我在错误检查中返回一个0,这个0可能是有意义的数据,如果不返回任何值,那么最后的返回结果还是会执行,这样,调用者明明参数都传错了,还是会拿到了错误的数据。

所以,函数中应该如何合理地进行缺陷和错误处理呢? 

C语言中有没有必要做参数检查?

C语言中对函数进行参数检查能够保证软件的健壮性,同时也必然会带来程序性能的降低。这让我在一段时间内常常为是否应该执行参数检查而纠结不已。

我们在很多情况下需要用到assert,如当我们需要使用它去进行参数合法监测等,我们使用assert语句去实现,但是当软件发布时,因为性能问题等,会将assert去掉,所有的合法检测都失效了,我们又该如何监测参数的合法性呢?
如果我们将函数加上检测参数合法性的代码,使用if语言去判断去处理,这就无疑增加了程序的开销,在什么样的场景下我们需要检查,也是一个问题。
 

这里先提供一些同行的看法。

##

都说好的编程习惯是对每个函数进行入口参数检查,可是我感觉这太麻烦了,每个函数的开头都要写一串:if (NULL != fp && NULL != ...) ,有的函数就几行代码,入口检查占了大半。
请问各位,有没有更好的方法或“机制”既能保证安全又能省事些?

##

这个其实不是技术问题,而是哲学问题。
如果你一定想要一个答案,那么我告诉你,接口规范的检查是没有必要的。
第一,C/C++的宗旨之一,是程序员要对自己做的事负责,我告诉了你接口规范,你没按这个要求去做,后果要由你自己承担;
第二,莫让99%的情况为1%的情况付出代价。一个已经调试好的程序,对接口规范的检查是徒劳和无意义的。我的建议是,用断言处理接口规范检查,调试好后,去掉断言;

##

这个看你写什么代码,给自己用的话,在调用函数前确保参数的合法性,如果给客户用的话,还是在使用参数前进行检查。

##

这个要看。C运行库里很多为了性能,牺牲检查。由用户(使用库的程序员)去检查。

##

你这种担心是多余的。
你所担心的事情其实属于BUG的范畴,BUG不是调试阶段能够检查的,需要系统性的方法才能检查出来,这是软件测试部门的工作。你的担心其实是试图将不是码工应该做的工作承担起来,以求得“心之所安”,但这个“努力”换来的是软件在99.999%的情况下运行着多余的浪费的代码,这是值得的吗?另一方面,为了符合接口规范的要求,函数使用者多数情况下会对实参进行检查,于是你所做的“努力”很可能又一次成为了重复劳动。
函数返回给调用者的信息,应该主要用来做两件事,一是返回结果;二是返回函数的状态,例如搜索到尽头都没有找到符合条件的结果,就返回一个标志,这才是高效的工作,而非告诉调用者,你违反了我的接口规范!这是低效的,很多时候这种低效信息对调用者甚至一点用处都没有。

##

不应该在函数中用内嵌源码的方式处理接口规范的检查,这是低效的,如果函数使用者同时也做了检查,更增加了多余的重复劳动。应该用断言处理这个问题,调试好后把断言去掉。

##

个人感觉
面向程序调用者,底层代码少测
面向用户输入,应用层多测(尤其是客户端程序)

软件通常分两种,一种是给程序员调用的,比如C库,比如操作系统,这时候的传参是由程序员自己决定的;还有一种是发布后直接供普通用户操作的,比如用户在某APP中输入框输入不合法数据。

##

一般来说是对外提供的接口一定要检查参数的有效性;模块内部使用的接口要求稍低,但为了多人合作和方便后人接手,最好也加上参数有效性检查,既可以检查参数有效性,又可以指明参数应有的特征,方便读代码。
另外在正常的参数有效性检查之外,还可以使用assert断言检查,用作调试,但是不能简单的用assert来代替参数检查。

总结来说:

程序开发阶段,使用断言机制来检查参数合法性是很有必要的,程序发布后,断言被关闭,有些外部输入数据并且容易出错的接口可以使用if来进行参数检查,并给予用户一些有效的提示。

参数检查的两种常用方式

参数检查方法通常有两种:一种是通过条件判断语句。

实际使用时,从效率角度出发,程序员往往会在参数检查语句外面加一个宏开关,例如:

#define MOD_CHK_PARAM (1) // 0 - 关闭宏,1 - 打开宏

int foo(char *p)
{
#if MOD_CHK_PARAM > 0
    if (NULL == p)
    {
        return -1;
    }
#endif

    // do somethhing
}

这种方法的好处是,代码的使用者能灵活的配置各项参数检查的开关。

但这种方法带来的问题是使用者总是得通过函数的返回值来判断哪个参数在传递时出了问题。

参数检查的另外一种方法是通过C语言标准库提供的断言(assert)机制(断言的原理比较简单,以至于用户可以自己实现),使用方法如下:

#include <assert.h>

int foo(char *p)
{
    assert(p != NULL);

    // do something
}

用断言的好处是能直观的反应出错误在哪,缺点是没那么灵活。

因为断言要么开,要么关,不存在关一部分断言的情况。

断言详解

什么是Assert断言?

编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式。

断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真。

可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言,而在部署时禁用断言。

同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。 ---来自百度百科

C 标准库的 assert.h头文件提供了一个名为 assert 的宏(注意不是函数),它可用于验证程序做出的假设,并在假设为假时输出诊断消息。

已定义的宏 assert 指向另一个宏 NDEBUG,宏 NDEBUG 不是 <assert.h> 的一部分。如果已在引用 <assert.h> 的源文件中定义 NDEBUG 宏,则 assert 宏的定义如下:

#define assert(ignore) ((void)0)

也就是说,如果定义了NDEBUG宏,就可以关闭assert。

这一点其实可以参考STM32的HAL库中的assert_param,是一样的。

STM32的assert_param参数断言定义如下:

这里默认是没有开启的,也就是assert_param不起作用。

要想开启,HAL中stm32f1xx_hal_conf.h里:

ASSERT() 是一个调试程序时经常使用的宏,在程序运行时它计算括号内的表达式,如果表达式为 FALSE (0),程序将报告错误,并终止执行。如果表达式不为 0,则继续执行后面的语句。这个宏通常用来判断程序中是否出现了明显非法的数据,如果出现了终止程序以免导致严重后果,同时也便于查找错误。

assert中传入的可以是一个变量或任何 C 表达式。如果 expression 为 TRUE,assert() 不执行任何动作。如果 expression 为 FALSE,assert() 会在标准错误 stderr 上显示错误消息,并中止程序执行。

比如如下程序:

uint8_t Bcd2Dec01(uint8_t bcd)
{
	uint8_t one = (bcd & 0x0F);
	uint8_t ten = (bcd & 0xF0) >> 4;

	assert((one <= 9) && (ten <= 9));

	return one + (ten * 10);
}

传入的参数不对时,assert() 会在标准错误 stderr 上显示哪个文件哪一行出错了。并调用abort()函数终止程序。

C 库函数 void abort(void) 中止程序执行,直接从调用的地方跳出。停止程序执行可以让开发人员很容易马上看到哪里的代码出错,而不是过段时间以后才知道。

assert() 的用法像是一种"契约式编程",在我的理解中,其表达的意思就是,程序在我的假设条件下,能够正常良好的运作,其实就相当于一个 if 语句:

if(假设成立)
{
     程序正常运行;
}
else
{
      报错&&终止程序!(避免由程序运行引起更大的错误)  
}

但是这样写的话,就会有无数个 if 语句,甚至会出现,一个 if 语句的括号从文件头到文件尾,并且大多数情况下,我们要进行验证的假设,只是属于偶然性事件,又或者我们仅仅想测试一下,一些最坏情况是否发生,所以这里有了 assert()。所以能用assert的地方也没必要使用if+abort/exit的形式。

使用 assert 的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。

在调试结束后,可以通过在包含 #include 的语句之前插入 #define NDEBUG 来禁用 assert 调用,示例代码如下:

assert用法总结与注意事项

1) 在函数开始处检验传入参数的合法性;

2) 每个assert只检验一个条件,因为同时检验多个条件时,如果断言失败,无法直观的判断是哪个条件失败;

不好:

assert(nOffset>=0 && nOffset+nSize<=m_nInfomationSize); 

assert(nOffset >= 0); 
assert(nOffset+nSize <= m_nInfomationSize); 

3) assert不能产生副作用,比如错误做法:assert(i++ < 100);

4) 有的地方,assert不能代替条件过滤。程序一般分为Debug 版本和Release 版本,Debug 版本用于内部调试,Release 版本发行给用户使用。断言assert 是仅在Debug 版本起作用的宏,它用于检查"不应该"发生的情况。因为assert会使得程序终止,适合在调试的时候使用。如果发布后遇到一些错误就要复位重启,那绝对是不可取的。这种情况,就需要在程序中做好错误处理,使得程序能够正常运行下去,而这种错误通常是能够预见的,所以可以提前预防。

5) 使用断言捕捉不应该发生的非法情况。开发人员应该切记:断言是用于检测缺陷的,不能用于错误处理。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。错误使用断言的一个典型例子是,在试图打开一个文件用于读取时去检查文件的指针,如下图所示。

开发人员应该编写错误处理程序,而不是用断言,以便在文件不存在时,错误处理程序可以用一些默认可用数据来创建它,以便后续代码继续操作。

6) 在编写函数时,要进行反复的考查,并且自问:"我打算做哪些假定?"一旦确定了的假定,就要使用断言对假定进行检查。注意assert里检查的是正确的假设。

7) 断言也能用来验证契约式设计环境中对某个函数输出的假设。

8) 断言应该占代码的1%至3%

每个开发人员对于代码库(Code Base)中应该有多少个断言都有自己的主见。大家一致同意的一个数字是,代码库中的断言占比应该大于0。断言为开发人员提供了一种在代码库中发生缺陷的时刻发现它的好方法。调试是在开发嵌入式系统中最浪费时间并令人沮丧的事情之一。不管开发人员认可的占比是1%、3%还是5%,使用断言肯定对你有利,并会使开发嵌入式软件变得多少有些趣味。

错误处理

从以上内容可知,assert通常是在程序开发中用来调试程序的,不适合用来进行错误处理。

那么,要如何进行错误处理呢?

错误处理通常就是使用if来判断,如果有错误则返回一个状态,然后根据状态来进行相关的处理。如果没错误,则继续运行程序,总之,就是可以使得程序正常运行。

这种情况下,通常都会有一个返回值,又回到了开头的那种情况,如果返回值跟有效返回值不重叠倒还好,要是跟返回值重叠了怎么办?

在解决上面的问题之前,先了解下常见的错误处理方式。

1、没有错误。没有错误就是不用处理;;

3、返回一个负数;

4、返回一个布尔值和一个输出参数;

5、返回一个枚举和一个输出参数;

6、返回一个布尔值和两个输出参数;

7、返回一个枚举和多个输出参数;

9、返回一个共用体/结构体;

10、返回一个错误对象。

不管是返回负数、布尔值、枚举都是类似的,关键在于如何在返回这些值的时候还能有输出参数,这就用到了C语言的指针,形参传入指针作为输出型参数。

8、设置一个静态的全局变量;

设置静态的全局变量也能解决问题,但是一定要加static,如果没有static,则不推荐使用。

其实C语言中很多函数,都是返回函数状态的,而想要返回的数都是通过输出参数来承载的。

所以,现在就用这种方式解决开头提出的问题:

uint8_t Bcd2Dec01(uint8_t bcd, uint8_t *decResult)
{
	uint8_t one = (bcd & 0x0F);
	uint8_t ten = (bcd & 0xF0) >> 4;

	if ((one > 9) || (ten > 9))
	{
		return 1;
	}
	else
	{
		*decResult = one + ten * 10;
		return 0;
	}
}


int main(void)
{
	uint8_t bcdValue = 31;
	uint8_t decResult = 0;
	uint8_t sta = 0;

	sta = Bcd2Dec01(bcdValue, &decResult);

	if (sta == 1)
	{
		printf("输入的BCD码是非法的!\n");
		return 0;
	}

	printf("the result is %d\n", decResult);

	return 0;
}

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

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

相关文章

《C++内存管理》

本文主要介绍C内存管理的知识&#xff0c;主要包括new和delete&#xff0c;其实很简单&#xff0c;类比我们的C语言的内存管理malloc/free&#xff0c;就是在堆上申请内存的 小知识点&#xff1a; C构造对象的顺序&#xff1a;先构造全局&#xff0c;再构造局部静态对象&#x…

一些解决方案

文件异步下载方案 1 set QueryBussessType manually different type --> different resolving code、wherecondition 2. frontend request with the type 3. get excelHeader --> groovyUtil load from db 4. getData from db with pagination 5. saveData in an excel 6…

【测试面试】你要的宝典,软件接口测试面试题大全(总结)--附答案

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、接口测试主要目…

将war包发布到容器中的tomcat

文章目录 将war包直接发布到容器中使用数据卷将war包持久化到docker的宿主机(CentOS7) 将war包直接发布到容器中 1、将windows中的文件通过xftp程序传到centOS7中 2、创建容器&#xff0c;通过docker中的命令将web.jar复制到tomcat容器中 # 查看docker中的镜像 ~]$ docker imag…

0.8秒捕捉,速度超乎想象,小米和WPS用户太激动,office用户已用

只需0.8秒就能捕捉 距离4月18日小米13ultra发布会时间还剩一天。这几天雷军的微博已经把小米13ultra的亮点已经做了很多铺垫宣传。 除了系统和硬件之外&#xff0c;就是这次小米13ultra最大的“杀手锏”的就是“徕卡相机”。连宣传文案都改成了&#xff1a;一个伟大的瞬间&…

【UE】玩家位置存档

在上一篇博客中&#xff08;【UE】将存档的值显示在控件蓝图上&#xff09;我们介绍了如何将存档的值显示在控件蓝图上&#xff0c;本篇博客要介绍的是如何将玩家位置进行存档。 效果 可以看到重新进入游戏时&#xff0c;角色在存档点出现&#xff0c;而不是玩家出生点 步骤 …

XMU 算法分析与设计第三次上机题解

文章目录 一、BFS试炼之微博转发二、DFS试炼之不同路径数三、并查集试炼之合并集合并查集的介绍 四、堆排序堆排序的介绍 五、厦大GPA&#xff08;分组背包&#xff09;分组背包介绍 六、消防安全指挥问题&#xff08;最短路Floyd&#xff09;七、铺设光纤问题(最小生成树Prim)…

干货满满~如何解决跨域!!

1. 为什么会存在跨域 首先要知道&#xff0c;在浏览器/app中使用异步请求(ajax)发送到服务器时&#xff0c;会出现跨域问题。若是服务与服务之间通信是没有跨域这一说的 2. 浏览器为什么要设置跨域的存在&#xff1f; 为了防止恶意网页可以获取其他网站的本地数据&#xff0…

4.23、TCP状态转换(为什么四次挥手)

4.23、TCP状态转换 1.TCP状态转换图2.为什么需要四次挥手&#xff0c;状态转换 1.TCP状态转换图 2.为什么需要四次挥手&#xff0c;状态转换 2MSL&#xff08;Maximum Segment Lifetime&#xff09; 主动断开连接的一方, 最后进入一个 TIME_WAIT状态, 这个状态会持续: 2msl ms…

实例化构造方法static统统都学会

文章目录 前言一、实例化是什么&#xff1f;二、构造方法1.概念2.特性3.. 如果用户没有显式定义&#xff0c;编译器会生成一份默认的构造方法&#xff0c;生成的默认构造方法一定是无参的 四.static1.static修饰成员变量2.static修饰成员方法3.static成员变量初始化 总结 前言 …

AR实战-基于Krpano的多场景融合及热点自定义

背景 在之前的博客中&#xff0c;曾经介绍了关于Krpano的相关知识&#xff0c;原文&#xff1a;全景自动切片技术-krpano初识。简单讲解了基于krpano1.19-pr13下单张全景照片的处理与展示。随着实景中国在各地的落地生根&#xff0c;三维园区、三维景区、三维乡村等等需求的集中…

ERP系统给企业管理带来哪些改变?

企业资源计划&#xff08;ERP&#xff09;系统是一种综合性的管理工具&#xff0c;它可以集成和管理企业内部所有的业务流程和信息。自上世纪90年代以来&#xff0c;ERP系统已成为许多企业的重要工具&#xff0c;为企业管理带来了巨大的变革。 第一&#xff0c;ERP系统可以将企…

ArrayList与顺序表

目录 ​编辑 一、线性表 二、顺序表 1、接口的实现 &#xff08;1&#xff09;打印顺序表 &#xff08;2&#xff09;新增元素 &#xff08;3&#xff09;判定是否包含某个元素 &#xff08;4&#xff09;查找某个元素对应的位置下标 &#xff08;5&#xff09;获取 …

基于QTableView中的MVD代理添加总结

目录 1、Qt中MVD说明 1.1 View 1.2 Delegate 1.3 Model/View的基本原理 2、代码是现实示例 2.1 设置样式文件 2.2 set base attribute 2.3 设置model 2.4 设置表头 2.5 设置数据 2.6 添加代理控件 2.6.1 添加 QSpinBox 代理 2.6.2 添加 QComboBox 代理 2.6.…

【JS】vis.js使用之vis-timeline使用攻略,vis-timeline在vue3中实现时间轴、甘特图

vis.js使用之vis-timeline使用攻略&#xff0c;vis-timeline实现时间轴、甘特图 1、vis-timeline简介2、安装插件及依赖3、简单示例4、疑难问题集合1. 中文zh-cn本地化2. 关于自定义class样式无法被渲染3. 关于双向数据绑定 vis.js是一个基于浏览器的可视化库&#xff0c;它提供…

深度探索vector

vector是什么 &#xff1f; vector就是一个可以自动扩充的array。 源码解析 vector主要是通过三个指针来维护的&#xff0c;分别是起点&#xff0c;当前终点&#xff0c;以及当前最大空间 sizeof(vector对象) 3 * 指针大小 vector每当遇到空间不同的情况&#xff0c;都会…

Windows逆向安全(一)之基础知识(十三)

Switch语句 先前讲了分支结构的if else形式&#xff0c;除此之外还有一种分支结构&#xff1a;switch 此次就来以反汇编的角度研究switch语句&#xff0c;并与if else进行比较 Switch语句的使用 有关Switch语句在vc6.0中生成的反汇编可分为4种情况&#xff0c;这4种情况的区…

不用科学上网,免费的GPT-4 IDE工具Cursor保姆级使用教程

1、Cursor 编辑器 可以直接官方网站下载&#xff1a;https://www.cursor.so/ &#xff08;这里以Mac为例&#xff09; 这是一款与OpenAI合作并且基于GPT4的新一代辅助编程神器&#xff0c;它支持多种文件类型&#xff0c;支持格式化文本、多种主题、多语言语法高亮、快捷键设…

react-7 组件库 Ant Design Mobile(移动端)

1.安装组件库 npm install --save antd-mobile 常用组件 tabbar 底部导航 Swiper 轮播图&#xff08;走马灯&#xff09; NavBar&#xff08;顶部返回累&#xff09; 配合 Dialog&#xff0c;Toast InfiniteScroll 无限滚动&#xff08;实现下拉刷新&#xff09; Skeleto…

ROS学习第九节——服务通信

1.基本介绍 服务通信较之于话题通信更简单些&#xff0c;理论模型如下图所示&#xff0c;该模型中涉及到三个角色: ROS master(管理者)Server(服务端)Client(客户端) ROS Master 负责保管 Server 和 Client 注册的信息&#xff0c;并匹配话题相同的 Server 与 Client &#…