3、参数校验:函数参数合法性检查、指针参数判空、数值范围校验、枚举值有效性检查
大家好,我是你们的老朋友。今天我们来聊聊参数校验。
说实话,我见过太多因为参数没检查而翻车的案例了。有一次,一个同事写的驱动,传进来一个空指针,结果整个系统直接挂掉。从那以后,我养成了一个习惯——任何函数,只要参数来自外部,必须先过一遍校验。
你想想看,函数就像一扇门。你不检查进来的是人是鬼,那后果可想而知。参数校验,就是给这扇门装个门禁系统。
3.1 指针参数判空:最基础的保命操作
指针判空,这应该是每个C程序员的基本功。但说实话,很多人会忘。我自己也犯过这种低级错误。
为什么要判空?因为解引用空指针是未定义行为。轻则段错误,重则数据被破坏。嗯,这里要注意:判空不是可选项,是必选项。
核心原则:任何可能为空的指针参数,在函数入口处立即判空。
// 错误示范:直接使用,不检查
void process_data(int *data) {
*data = 100; // 如果data为NULL,直接崩溃
}
// 正确示范:先判空,再使用
int process_data(int *data) {
if (NULL == data) {
// 记录错误日志
log_error("process_data: data pointer is NULL");
return -1; // 返回错误码
}
*data = 100;
return 0;
}
小技巧:我习惯把 NULL 写在左边,比如 if (NULL == data)。这样万一手滑写成 =,编译器会报错。这招是当年一个老工程师教我的,用了十几年了。
3.2 数值范围校验:别让数据越界
数值范围校验,说白了就是检查传进来的数字是不是在合理范围内。比如一个温度传感器,读数范围是 -40°C 到 125°C。如果有人传进来 9999,那肯定有问题。
我在项目中遇到过一件事:一个电机控制模块,速度参数传了个负数进去,结果电机反向全速旋转,差点把设备搞坏。从那以后,我对数值范围校验特别敏感。
// 数值范围校验示例
#define TEMP_MIN (-40)
#define TEMP_MAX 125
int set_temperature(int temp) {
// 检查下限
if (temp < TEMP_MIN) {
log_error("set_temperature: temp %d below minimum %d", temp, TEMP_MIN);
return -1;
}
// 检查上限
if (temp > TEMP_MAX) {
log_error("set_temperature: temp %d above maximum %d", temp, TEMP_MAX);
return -1;
}
// 范围合法,执行操作
temperature_reg = temp;
return 0;
}
注意:数值范围校验要同时检查上下限。有些人只检查上限,忽略了下限,结果负数传进来照样出事。我曾经就吃过这个亏。
3.3 枚举值有效性检查:别信编译器
很多人觉得枚举值不用检查,因为编译器会保证。但实际情况是:枚举变量可能被强制类型转换,或者从外部接口接收。
举个例子,你定义了一个枚举:
typedef enum {
STATE_IDLE = 0,
STATE_RUNNING,
STATE_STOP,
STATE_MAX // 用于边界检查
} machine_state_t;
如果有人传进来一个 (machine_state_t)99,你的 switch-case 会怎么处理?嗯,大概率是走到 default 分支,或者更糟——直接执行未定义行为。
// 枚举值有效性检查
int set_machine_state(machine_state_t state) {
// 检查枚举值是否在有效范围内
if (state < STATE_IDLE || state >= STATE_MAX) {
log_error("set_machine_state: invalid state %d", state);
return -1;
}
switch (state) {
case STATE_IDLE:
// 处理空闲状态
break;
case STATE_RUNNING:
// 处理运行状态
break;
case STATE_STOP:
// 处理停止状态
break;
default:
// 理论上不会走到这里,但保留以防万一
log_error("set_machine_state: unexpected state %d", state);
return -1;
}
return 0;
}
我的习惯:在枚举定义中,我会加一个 _MAX 或 _COUNT 成员,专门用来做边界检查。这样既清晰又安全。
3.4 参数校验的完整流程
说了这么多,我们来总结一下参数校验的完整流程。我画了一张图,方便大家理解:
这张图展示了参数校验的三个核心步骤。记住:顺序很重要。先判空,再检查数值范围,最后验证枚举值。因为如果指针是空的,后面的检查根本没法做。
3.5 参数校验的常见误区
我总结了一些常见的误区,大家看看自己有没有踩过坑:
| 误区 | 错误做法 | 正确做法 |
|---|---|---|
| 只检查上限 | if (value > MAX) return -1; |
同时检查上下限:if (value < MIN || value > MAX) |
| 忽略枚举边界 | 直接使用枚举值,不检查范围 | 用 _MAX 或 _COUNT 做边界检查 |
| 判空后不返回 | 判空后继续执行 | 判空后立即返回错误码 |
| 不记录错误日志 | 只返回错误码,不记录日志 | 记录错误信息,方便排查问题 |
避坑指南:我曾经在一个项目中,只检查了指针是否为空,但没有检查指针指向的内存是否有效。结果传进来一个野指针,程序照样崩溃。所以,判空只是第一步,还要确保指针指向的内存是可访问的。
3.6 参数校验的代码模板
最后,我给大家一个通用的参数校验模板。你可以直接复制到项目中使用:
/**
* @brief 参数校验通用模板
* @param ptr 指针参数
* @param value 数值参数
* @param state 枚举参数
* @return 0 成功,-1 失败
*/
int validate_params(void *ptr, int value, machine_state_t state) {
// 1. 指针判空
if (NULL == ptr) {
log_error("validate_params: ptr is NULL");
return -1;
}
// 2. 数值范围校验
if (value < VALUE_MIN || value > VALUE_MAX) {
log_error("validate_params: value %d out of range [%d, %d]",
value, VALUE_MIN, VALUE_MAX);
return -1;
}
// 3. 枚举值有效性检查
if (state < STATE_IDLE || state >= STATE_MAX) {
log_error("validate_params: invalid state %d", state);
return -1;
}
// 所有校验通过
return 0;
}
好了,关于参数校验的内容就讲到这里。记住一句话:信任是好的,但校验更安全。在嵌入式开发中,多一次检查,就少一次崩溃。
核心要点回顾:
- 指针参数必须判空,判空后立即返回
- 数值范围要同时检查上下限
- 枚举值要用边界检查,不要相信编译器
- 校验失败要记录错误日志,方便排查