首页 > 其他分享 >C语言的缺陷/错误处理问题探讨

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

时间:2023-08-20 14:35:47浏览次数:36  
标签:断言 检查 uint8 C语言 assert 参数 错误处理 缺陷 函数

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

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、返回一个枚举和多个输出参数;

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

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

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

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

10、返回一个错误对象。

不管是返回负数、布尔值、枚举都是类似的,关键在于如何在返回这些值的时候还能有输出参数,这就用到了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;
	}
}

标签:断言,检查,uint8,C语言,assert,参数,错误处理,缺陷,函数
From: https://blog.51cto.com/u_16038948/7161299

相关文章

  • 计算机萌新的成长历程——初识C语言
    2020.8.12晚上11:00我自己正式下定决心开始学习C语言,通过公众号“枫月软件”安装了VisualStudio2019Professional,随后在B站上跟着鹏哥的教学视频正式开始接触C语言。(PS:鹏哥的B站名称是“c语言编程学习”)51CTO网站也是通过视频才知道,并按照要求注册账号编写自己的第一篇博客。我......
  • C语言基本知识要点:
    目录一.基本数据类型 1.数据类型划分 2.基本数据类型的基本知识(32位系统) (1)整型:short2字节,int4个字节,long4个字节 (2)字符型:char2个字节(一一对应ASCII码) (3)浮点型: 3.进制转换 (1)八进制转二进制 八进制数据的1位对应二进制数据的3位 (2)十六进制转二进制 4.数据在内存中的存储方......
  • C语言基本知识要点:
    目录一.基本数据类型 1.数据类型划分 2.基本数据类型的基本知识(32位系统) (1)整型:short2字节,int4个字节,long4个字节 (2)字符型:char2个字节(一一对应ASCII码) (3)浮点型: 3.进制转换 (1)八进制转二进制 八进制数据的1位对应二进制数据的3位 (2)十六进制转二进制 4.数据在内存中的存储方......
  • C语言编程的结构化要求和正确性与容错性要求
    一、结构化要求(1)禁止出现两条等价的支路。(2)禁止使用GOTO跳转语句。(3)用IF语句来强调只执行两组语句中的一组。禁止ELSEGOTO和ELSERETURN。(4)用CASE实现多路分支。(5)避免从循环引出多个出口。(6)3.6函数只有一个出口。(7)不使用条件赋值语句。(8)避免不必要的分支。(9)不要轻易用条件......
  • 经典c语言排序算法
    前言前段时间偶然在公众号中看到了一篇汇总c语言排序算法的文章,感觉蛮不错的,这里直接copy记录下,学习积累一下。演示C语言经典排序算法(qq.com)排序算法简介1.算法分类十种常见排序算法可以分为两大类:比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(n......
  • 第一个C语言程序
    第一个C语言程序#include<stdio.h>/*这是我的第一个C语言程序我很开心*/intmain(void){printf("hello,world\n");return0;}保存退出gcchello.c//a.out文件./a.out//hello,world注意:不要出现中文标点符号分析:[1]C语言程序绝大部分的内......
  • C语言函数(12)--- 递归(4)
    我们前一篇文章介绍了利用函数的递归来实现斐波那契数代码如下:#define_CRT_SECURE_NO_WARNINGS1#include<stdio.h>#include<windows.h>intFib(intn){ if(n<=2){ return1; } else{ returnFib(n-1)+Fib(n-2); }}intmain(void){ intn=0; intret=0;......
  • c语言笔记4
    c语言笔记4(指针)1.指针的应用1.1内存空间32位机:一次处理数据的大小4B(字节)64位机:一次处理数据的大小8B(字节)计算处理数据的最小单位是1B(字节),计算存储数据的最小单位二进制的1b(位)一个程序启动后的进程分区:栈、堆、全局区、常量区、代码区内存寻址:(32位)最大......
  • C语言多文件编程中全局变量的声明与定义会遇到的问题
    C语言多文件编程中全局变量的声明与定义会遇到的问题目录C语言多文件编程中全局变量的声明与定义会遇到的问题描述:1.创建3个文件,分别是main.c、test.h、test.c,具体代码如下:1.1.1test.h1.1.2test.h1.1.3main.c1.1.4这样能直接编译通过。2.创建3个文件,分别是main.c、test.h、te......
  • 用C语言做一个菱形图案
    如何来完成一个菱形图案的打印呢?首先我们把它简单的看成两个三角组成的图案,分为上下两部分,我们先来完成上半部分,首先肯定是一行一行来打印,为了后期维护方便我们用循环来完成,我们输入几行就打印几行。那一行由什么组成的呢?是用空格和星号来组成的,我们先来第一步打印空格,我们经过发现......