4、命名规范(下):函数命名(动作+对象),模块前缀的使用,避免命名冲突的实战技巧

好,咱们接着聊命名。上一节我们把变量和宏的命名捋了一遍,这一节重点说说函数命名。函数是代码的执行单元,名字起得好,读代码就像读故事。起得不好,那就是在猜谜。

我个人习惯,函数命名一定要遵循 “动作 + 对象” 这个黄金法则。你想想看,函数就是一段可执行的逻辑,它总要干点什么吧?要么读数据,要么写寄存器,要么初始化硬件。把“干什么”和“对谁干”说清楚,名字就成功了一大半。

动作 + 对象:函数命名的基本盘

什么叫“动作 + 对象”?很简单:

  • 动作:动词,比如 ReadWriteSetGetInitDeInitEnableDisable
  • 对象:名词,比如 TemperatureGPIOUARTBufferStatus

组合起来就是:

// 好的命名
uint32_t ReadTemperature(void);
void      WriteGPIO(uint8_t pin, uint8_t value);
void      InitUART(uint32_t baudrate);
bool      GetStatus(void);

// 糟糕的命名
uint32_t temp(void);           // 动作呢?对象呢?
void gpio(uint8_t a, uint8_t b); // 完全不知道要干嘛
void process(void);            // 太模糊了,万物皆可process

我在项目中遇到过一位同事,写了一个函数叫 do_stuff()。我当时就问他:“这个函数到底干了什么?”他想了半天说:“嗯……就是初始化一些东西,然后检查一下状态,再发个数据。”你看,一个函数干了三件事,名字还叫“做事情”。这种代码维护起来真的很痛苦。

核心原则:一个函数只做一件事,名字就要精确描述这一件事。如果名字需要两句话才能解释清楚,说明函数设计有问题。

模块前缀:给函数贴上“身份证”

在嵌入式项目中,我们经常要面对多个模块。比如一个项目里可能有 UARTI2CSPIGPIOTimer 等等。如果不加前缀,很容易出现命名冲突。

我建议每个模块都使用 模块前缀。前缀通常是大写字母缩写,放在函数名最前面:

// UART模块
void UART_Init(uint32_t baudrate);
void UART_SendByte(uint8_t data);
uint8_t UART_ReceiveByte(void);

// I2C模块
void I2C_Init(uint32_t frequency);
bool I2C_Write(uint8_t devAddr, uint8_t regAddr, uint8_t data);
bool I2C_Read(uint8_t devAddr, uint8_t regAddr, uint8_t *buffer);

这样做的好处很明显:

  • 避免冲突:不同模块可以有同名的函数,比如 UART_InitI2C_Init 不会打架。
  • 提高可读性:看到 UART_ 开头,就知道这是串口相关的操作。
  • 方便检索:在 IDE 里输入 UART_,所有串口函数就都出来了。

小技巧:前缀不要超过 4 个字符。太长了写起来累,读起来也费劲。比如 UARTI2CSPIGPIO 就很好。别搞成 UART_DRIVER_ 这种,太啰嗦了。

避免命名冲突的实战技巧

命名冲突在嵌入式开发里太常见了。尤其是当你把多个第三方库、中间件、或者不同同事写的模块拼在一起时,冲突就像地雷一样等着你踩。

我曾经在一个项目里,同时用了两个传感器驱动库。一个叫 sensor.h,另一个也叫 sensor.h。两个库里都有 Init() 函数。结果链接的时候直接报重复定义。嗯,那场面,真的很尴尬。

从那以后,我总结了几条实战技巧:

1. 使用模块前缀 + 下划线

这是最基础也是最有效的方法。每个模块都用自己的前缀,就像给函数穿上了不同颜色的衣服,一眼就能认出来。

// 温度传感器模块
void TSENSOR_Init(void);
float TSENSOR_ReadTemperature(void);

// 压力传感器模块
void PSENSOR_Init(void);
float PSENSOR_ReadPressure(void);

2. 避免使用过于通用的名字

InitReadWriteSetGet 这种名字,太容易冲突了。一定要加上模块前缀或对象名。

警告:永远不要在头文件里定义不带前缀的全局函数。比如 void Init(void); 这种,一旦项目变大,几乎必然冲突。

3. 使用静态函数限制作用域

如果某个函数只在当前 .c 文件内部使用,一定要加上 static 关键字。这样即使名字和别人重复了,也不会冲突。

// 只在 uart.c 内部使用
static void _UART_FlushBuffer(void);

// 全局可见
void UART_Init(uint32_t baudrate);

我个人习惯,内部函数用 _ 前缀开头,比如 _UART_FlushBuffer。这样一看就知道是内部函数,不要在外面调用。

4. 统一命名风格

团队里一定要统一风格。是用 UART_Init 还是 uart_init?我个人偏好驼峰式(CamelCase),因为可读性更好。但如果你团队习惯下划线,那就统一用下划线。关键是 一致性

风格 示例 说明
驼峰式 UART_Init 模块前缀大写,函数名首字母大写
下划线式 uart_init 全小写,下划线分隔
匈牙利式 uart_Init 不推荐,容易混淆

我的建议:在嵌入式 C 里,我推荐使用 模块前缀大写 + 驼峰式。比如 UART_InitI2C_WriteByte。这种风格在 STM32 的 HAL 库、FreeRTOS 等主流代码里都很常见,大家看着也习惯。

实战案例:一个完整的模块命名示例

咱们来看一个完整的例子。假设我们要写一个 DS18B20 温度传感器驱动

// ds18b20.h
#ifndef _DS18B20_H_
#define _DS18B20_H_

#include <stdint.h>
#include <stdbool.h>

// 初始化 DS18B20
bool DS18B20_Init(void);

// 读取温度值(单位:摄氏度)
float DS18B20_ReadTemperature(void);

// 设置温度报警阈值
void DS18B20_SetAlarmThreshold(float low, float high);

// 获取报警状态
bool DS18B20_GetAlarmStatus(void);

#endif
// ds18b20.c
#include "ds18b20.h"

// 内部函数:发送复位脉冲
static bool _DS18B20_Reset(void) {
    // 实现代码...
}

// 内部函数:写入一个字节
static void _DS18B20_WriteByte(uint8_t data) {
    // 实现代码...
}

// 内部函数:读取一个字节
static uint8_t _DS18B20_ReadByte(void) {
    // 实现代码...
}

bool DS18B20_Init(void) {
    // 实现代码...
}

float DS18B20_ReadTemperature(void) {
    // 实现代码...
}

你看,这个模块的命名非常清晰:

  • 所有全局函数都以 DS18B20_ 开头,一看就知道是哪个模块。
  • 内部函数用 static 隐藏,加上 _ 前缀,避免外部误用。
  • 函数名遵循“动作 + 对象”原则,比如 ReadTemperatureSetAlarmThreshold

避坑指南:我曾经在写内部函数时,忘了加 static。结果另一个模块里也有一个同名的 _DS18B20_Reset 函数,链接时直接报错。从那以后,我写内部函数的第一件事就是敲 static 关键字。

总结

函数命名这件事,说白了就是 让别人(包括未来的你)能一眼看懂这个函数是干什么的。记住三个要点:

  1. 动作 + 对象:动词在前,名词在后,说清楚干什么和对谁干。
  2. 模块前缀:给每个模块一个独特的身份证,避免冲突。
  3. 内部函数加 static:隐藏实现细节,减少命名空间污染。

嗯,命名规范到这里就讲完了。下一节我们聊聊注释规范。别看注释不起眼,写好了能救命,写不好能坑人。咱们下节见。