第四章 参数设计原则:参数数量控制、输入输出参数分离、const限定符使用

函数参数怎么设计,说实话,这是很多嵌入式工程师容易忽略的地方。我见过太多人写函数时,参数一甩就是五六个,甚至七八个。调用的时候自己都记不清顺序,更别提别人看了。

今天咱们就聊聊参数设计的三个核心原则。这三个原则用好了,你的函数会变得清晰、安全、易维护。

4.1 参数数量控制:别超过4个

我个人习惯,函数参数尽量控制在4个以内。为什么是4个?因为人的短期记忆大概能同时记住4个东西。超过这个数,调用时就得翻文档。

我在项目中遇到过这样一个案例:一个驱动初始化函数,参数有8个——端口号、波特率、数据位、停止位、校验位、流控模式、超时时间、缓冲区大小。每次调用都要对着头文件数半天。后来我把它拆成了一个配置结构体,清爽多了。

坏例子:

// 参数太多,调用者容易搞混
void uart_init(uint8_t port, uint32_t baud, uint8_t data_bits,
               uint8_t stop_bits, uint8_t parity, uint8_t flow_ctrl,
               uint32_t timeout_ms, uint16_t buf_size);

好例子:

// 用结构体打包参数
typedef struct {
    uint32_t baud;
    uint8_t  data_bits;
    uint8_t  stop_bits;
    uint8_t  parity;
    uint8_t  flow_ctrl;
    uint32_t timeout_ms;
    uint16_t buf_size;
} uart_config_t;

void uart_init(uint8_t port, const uart_config_t *config);

你想想看,参数少了,调用时一目了然。而且结构体还能扩展,加新字段不影响已有代码。

小技巧:如果参数确实多,可以考虑用结构体指针。但注意,结构体本身也要设计得合理,别把不相干的字段塞进去。

4.2 输入输出参数分离:别让一个参数干两件事

这个原则说白了就是:输入参数只管输入,输出参数只管输出。别搞那种既是输入又是输出的参数,除非你有特别充分的理由。

我曾经在一个项目中看到这样的代码:

// 糟糕的设计:status既是输入又是输出
void process_data(uint8_t *data, uint16_t len, int *status);

调用者得先给status赋个初值,函数内部再改它。这到底是什么意思?是输入还是输出?代码读起来非常累。

正确的做法是:

// 清晰的设计:输入和输出分开
int process_data(const uint8_t *data, uint16_t len, uint8_t *result);

这里data是输入,用const修饰;result是输出,不加const。一眼就能看出来哪个是输入,哪个是输出。

注意:输出参数尽量用指针,而且最好在函数注释里说明:输出缓冲区由谁分配、多大。我曾经见过一个bug,调用者传了个空指针进去,函数直接写崩了。

嗯,这里还要提一下返回值。能用返回值就别用输出参数。比如:

// 用返回值更自然
int get_temperature(void);  // 返回温度值,负数表示错误

// 而不是
void get_temperature(int *temp);  // 输出参数,返回值表示状态

当然,如果返回值已经被占用了(比如返回错误码),那就用输出参数。但一定要在文档里写清楚。

4.3 const限定符使用:给参数加把锁

const这个关键字,很多人觉得可有可无。其实不然。它就像一把锁,告诉编译器:这个参数你别改,改了报错。

我个人习惯,只要是指向输入数据的指针,一律加const。为什么?因为安全。你想想看,一个函数接收了外部数据,如果不小心改了它,可能会引发连锁反应。

举个例子:

// 不加const,容易误改
void print_data(uint8_t *data, uint16_t len) {
    for (uint16_t i = 0; i < len; i++) {
        data[i] = data[i] + 1;  // 手滑写错了,把打印改成了自增
        printf("%02x ", data[i]);
    }
}

// 加const,编译器帮你拦住
void print_data(const uint8_t *data, uint16_t len) {
    for (uint16_t i = 0; i < len; i++) {
        // data[i] = data[i] + 1;  // 编译报错!不能修改const数据
        printf("%02x ", data[i]);
    }
}

你看,加了const之后,手滑的代码直接编译不过。这就是const的价值——把错误扼杀在编译期。

const使用规则:

参数类型 是否加const 说明
输入值参数(int, char等) 通常不加 值传递,改不了外部变量
输入指针参数 必须加 防止误改外部数据
输出指针参数 不加 就是要改它
输入输出指针参数 不加 既要读又要写

我曾经在一个项目中,因为没加const,导致一个全局缓冲区被某个函数意外修改了。排查了两天才找到原因。从那以后,我写函数参数时,第一件事就是问自己:这个参数是输入还是输出?输入就加const。

进阶技巧:const还可以用在函数返回值上。比如返回一个字符串常量,加const可以防止调用者修改它。

const char* get_version(void);  // 返回的字符串不能修改

4.4 总结一下

参数设计这件事,说白了就是三个字:少、清、锁。

  • :参数别超过4个,多了就用结构体打包
  • :输入输出分开,别混在一起
  • :输入指针加const,让编译器帮你把关

这三个原则用好了,你的函数接口会变得非常干净。调用者一看就知道怎么用,不用翻文档。维护者一看就知道哪些能改、哪些不能改。

嗯,最后说一句:参数设计是函数设计的门面。门面干净了,里面再复杂也容易理解。门面乱七八糟,里面写得再好也没人愿意看。