第四章 参数设计原则:参数数量控制、输入输出参数分离、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,让编译器帮你把关
这三个原则用好了,你的函数接口会变得非常干净。调用者一看就知道怎么用,不用翻文档。维护者一看就知道哪些能改、哪些不能改。
嗯,最后说一句:参数设计是函数设计的门面。门面干净了,里面再复杂也容易理解。门面乱七八糟,里面写得再好也没人愿意看。