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 参数校验的完整流程

说了这么多,我们来总结一下参数校验的完整流程。我画了一张图,方便大家理解:

参数校验完整流程 函数入口 步骤1:指针参数判空 步骤2:数值范围校验 步骤3:枚举值有效性检查 执行函数逻辑

这张图展示了参数校验的三个核心步骤。记住:顺序很重要。先判空,再检查数值范围,最后验证枚举值。因为如果指针是空的,后面的检查根本没法做。

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;
}

好了,关于参数校验的内容就讲到这里。记住一句话:信任是好的,但校验更安全。在嵌入式开发中,多一次检查,就少一次崩溃。

核心要点回顾:

  • 指针参数必须判空,判空后立即返回
  • 数值范围要同时检查上下限
  • 枚举值要用边界检查,不要相信编译器
  • 校验失败要记录错误日志,方便排查

公众号:蓝海资料掘金营,微信deep3321