本帖最后由 dffzh 于 2025-6-5 08:45 编辑
#申请原创#
@21小跑堂
为什么会有防御式编程呢?其实防御式编程的概念来源于防御式驾驶思维,因为你不知道也无法确定司机下一步要做什么,那怎么样才能保证司机在做出危险动作的时候自己不会受到伤害呢?这个时候就需要自己承担自我保护责任,就算是他人过错;所以,防御式编程的主要思想就是不能因外部的错误数据而破坏程序的正常运行。 另外,防御式编程其实也算一种软件开发的方法,通过预判和处理潜在错误来防止程序崩溃或者产生不可预知的行为,从而达到提高代码安全性和健壮性的目的。 那到底都有哪些防御式编程的代码操作呢?我们一起来看看吧! 1、函数入参验证 在定义和编写函数时,会经常接触到函数形参,有时候函数形参或者形参之间会有值范围和一些特殊性存在,有些实参值是不能作为形参输入的,一旦输入错误数据,那可能就会出现问题。 比如自定义了一个有符号数的除法运算函数,形参1为被除数dividend,形参2为除数divisor,形参3为商quotient(输出结果)。最简单的代码实现就是如下: void divide(int dividend, int divisor, int *quotient)
{
*quotient = dividend / divisor;
}
那如何增加防御式代码呢? 大家都知道,除数不能为0,要对除数执行非零判断,如下所示: int divide(int dividend, int divisor, int *quotient)
{
// 检查除数是否为0
if (0 == divisor)
{
return -1; // 错误代码
}
*quotient = dividend / divisor;
return 0; // 成功
}
2、指针安全检查 自定义函数时,函数入参类型用指针类型是非常常见的操作,但是相比其他基本数据类型而言,指针又是最容易出问题的,包括空指针和野指针在内往往都是导致程序崩溃的罪魁祸首。 因此,函数入参使用指针类型时,一定要先进行指针的安全性检查。 如下代码,先检查指针变量dest和src是否为NULL(空),如果为NULL,则直接返回,不执行拷贝操作。 void copy(char *dest, const char *src, size_t dest_size)
{
// 检查指针是否为NULL
if (NULL == dest || NULL == src)
{
return;
}
// 确保目标缓冲区足够大
size_t src_len = strlen(src);
if (src_len >= dest_size)
{
src_len = dest_size - 1;
}
// 安全拷贝
strncpy(dest, src, src_len);
dest[src_len] = '\0'; // 确保字符串终止
}
3、边界检查 有时候我们定义的函数的入参是有特殊的实际意义的,比如阈值范围、数组长度和协议命令码等,如果实际输入的数值不在合理范围内,即超出了边界,那就要执行异常返回处理。 比如一个根据ADC数据计算电量的函数,ADC的正常范围是100~2000,那我们就可以按如下方式增加防御式编程,以避免输出异常的电量数据: #define ADC_VALUE_MIN (100)
#define ADC_VALUE_MAX (2000)
int cal_volatge(unsigned int adc_data)
{
//判断ADC数据是否在合理范围内
if((adc_data < ADC_VALUE_MIN) || (adc_data > ADC_VALUE_MAX))
{
return -1;
}
//计算电压
return 0;
}
4、断言处理 断言(assertion)是一种在程序中的一阶逻辑(如一个结果为真或假的逻辑判断式),目的是为了表示与验证软件开发者预期的结果。 断言处理在STM32的固件库里其实是非常常见的,会对一些入参进行检查:
当程序执行到断言的位置时,对应的断言应该为真。若断言不为真时,程序会中止执行,并给出错误信息。 C标准库里有一个assert.h头文件,该头文件常用于防御式编程,其中提供了一个assert宏,它只带一个参数,通过布尔表达式的方式描述一些非预期错误,包括空指针、输入输出参数值不在合理范围及数组越界等。 比如下面的代码通过断言判断指针是否为NULL: void set_int_array_value(IntArray *arr, size_t index, int value)
{
//使用断言检查内部不变式
assert(arr != NULL);
// 运行时检查
if (index >= arr->size)
{
return;
}
arr->data[index] = value;
}
以上通过几个实际的常用的代码demo介绍了防御式编程的基本内容,其实防御式编程还涉及其他内容,简单总结如下: 验证所有输入:包括用户输入、文件内容和函数参数等; 检查返回值:特别是内存分配、文件操作等可能失败的调用; 使用断言:验证程序内部不变式; 初始化变量:特别是指针和敏感数据; 边界检查:数组访问、循环条件等; 资源管理:确保分配的资源最终被释放,原则就是谁申请谁释放; 错误处理:提供有意义的错误信息并处理错误; 代码审查:代码走查,代码走读,多人检查代码中的潜在问题; 静态分析:使用工具检测潜在问题,包括语法错误等。 通过以上这些防御式编程的操作和实践,将会显著提高代码的安全性和健壮性,有望彻底告别软件死机问题。 |