4. 编码规范基础:命名规范、注释规范与排版缩进
好,咱们进入第四章。说实话,编码规范这东西,很多工程师觉得是「形式主义」。我早年也这么想——代码能跑不就行了?直到有一次,我接手一个离职同事的项目,打开文件一看,变量名全是 a1、b2、temp,函数名清一色 func1 到 func50。那一周,我基本是在骂娘中度过的。
从那以后,我彻底明白了:规范不是束缚,是保护。尤其在嵌入式安全领域,一个命名不清的变量,可能直接导致误读、误改,最终酿成线上事故。
这一章,咱们就聊聊命名、注释和排版。这些都是「看着简单,做起来全是坑」的东西。
4.1 命名规范:让代码自己会说话
命名是代码的第一张脸。好的命名,读起来像读英文句子;差的命名,读起来像猜谜语。
4.1.1 变量命名
我个人习惯用 匈牙利命名法 + 下划线分隔 的组合。MISRA C 没有强制规定具体风格,但要求 标识符不能歧义、不能与关键字冲突、长度要有意义。
| 类型 | 推荐格式 | 示例 | 说明 |
|---|---|---|---|
| 全局变量 | g_ 前缀 + 模块名 + 含义 |
g_sysTickCount |
一眼看出作用域 |
| 静态全局变量 | s_ 前缀 + 含义 |
s_adcBuffer |
文件内私有 |
| 局部变量 | 直接含义,小写 + 下划线 | sensor_value |
简短清晰 |
| 指针变量 | p 前缀 + 类型 + 含义 |
pUartBuf |
提醒这是指针 |
| 布尔变量 | is_ / has_ 开头 |
is_initialized |
语义化判断 |
g_ 前缀,没有例外。
4.1.2 函数命名
函数命名要遵循 「动词 + 名词」 结构。说白了,看到函数名就知道它「干了什么」。
// 好的命名
uint32_t get_sensor_value(uint8_t channel);
void set_led_state(LedState state);
bool is_timer_expired(void);
// 差的命名
void process(void); // 处理什么?
uint32_t data(void); // 返回什么数据?
void led(void); // 设置?读取?切换?
MISRA C 规则 8.2 要求:函数必须有显式原型声明。命名清晰了,原型声明才真正有意义。
4.1.3 宏命名
宏的命名,行业惯例是 全大写 + 下划线。为什么?因为宏是预处理阶段替换的,没有类型检查,必须一眼就能和普通变量区分开。
// 好的宏命名
#define BUFFER_SIZE (256)
#define GPIO_PIN_13 (13U)
#define MAX_RETRY_COUNT (3U)
// 差的宏命名
#define buffer_size (256) // 和变量分不清
#define maxRetryCount (3U) // 风格不统一
#define MUL(a, b) a * b,结果调用 MUL(1+2, 3) 展开成 1+2*3,结果等于 7 而不是 9。这种坑,踩一次就记住了。
4.1.4 文件命名
文件命名要体现 模块 + 功能。我习惯用 模块名_功能.c 的格式。
| 模块 | 文件名示例 | 说明 |
|---|---|---|
| UART 驱动 | uart_driver.c |
驱动层 |
| UART 配置 | uart_config.c |
配置层 |
| 系统定时器 | sys_timer.c |
系统服务 |
| 错误处理 | error_handler.c |
公共模块 |
4.2 注释规范:写给人看的说明书
注释不是写给编译器看的,是写给 三个月后的自己 和 接手你代码的同事 看的。我见过太多「为了注释而注释」的代码,比如:
i = i + 1; // 把 i 加 1
这种注释,删掉比留着好。
4.2.1 文件头注释
每个源文件和头文件,开头都要有一个标准文件头。我团队用的模板是这样的:
/**
* @file uart_driver.c
* @brief UART 驱动层实现
* @author 张三
* @date 2024-01-15
* @version v1.2.0
*
* @note 本文件实现 UART 的初始化、发送、接收功能。
* 依赖 uart_hw.c 提供的硬件抽象层。
*
* @copyright Copyright (c) 2024, Company Name. All rights reserved.
*/
这里面有几个关键信息:文件名、功能简述、作者、日期、版本、依赖关系。为什么要有依赖关系?因为半年后你改代码时,需要知道这个文件「碰了哪些地方」。
4.2.2 函数头注释
每个函数都要有函数头注释,说明 功能、参数、返回值、注意事项。
/**
* @brief 获取指定通道的传感器原始值
* @param[in] channel 传感器通道号 (0-3)
* @param[out] value 指向存储读取值的缓冲区
* @return true 读取成功
* false 读取失败(通道无效或硬件错误)
* @note 本函数会阻塞等待转换完成,最大等待时间 10ms
* @warning 调用前需确保传感器已上电且 ADC 已初始化
*/
bool get_sensor_raw_value(uint8_t channel, uint16_t *value);
@param[in] 和 @param[out] 一定要写清楚。我见过一个 bug,就是因为调用者以为参数是输出,实际上函数把它当输入用了,结果数据一直不对。
4.2.3 代码行注释
行注释要解释 「为什么这么做」,而不是 「做了什么」。
// 好的行注释
// 等待硬件 FIFO 非空,超时 100us
while (UART_IS_EMPTY(uart)) {
if (get_tick_count() - start_tick > TIMEOUT_100US) {
return false; // 超时返回错误
}
}
// 差的行注释
while (UART_IS_EMPTY(uart)) { // 等待 UART 为空
// 获取 tick
if (get_tick_count() - start_tick > TIMEOUT_100US) {
return false; // 返回 false
}
}
你想想看,第二种注释是不是等于没写?代码本身已经表达了「做了什么」,注释应该补充代码无法表达的信息。
4.3 排版与缩进:代码的呼吸感
排版缩进,说白了就是代码的「呼吸感」。一段呼吸顺畅的代码,读起来不累;一段挤在一起的代码,看着就头疼。
4.3.1 缩进风格
MISRA C 没有强制规定缩进空格数,但要求 整个项目统一。我个人推荐 4 空格缩进,不用 Tab。为什么?因为不同编辑器的 Tab 宽度不一样,换台机器可能就乱了。
void example_function(void)
{
if (condition)
{
do_something();
do_another_thing();
}
else
{
handle_error();
}
}
注意大括号的位置:我习惯 大括号另起一行。这样每个代码块的开头和结尾一目了然,尤其适合嵌套较深的嵌入式代码。
4.3.2 空行与分组
逻辑相关的代码放在一起,不同逻辑之间用空行隔开。这就像写文章分段一样。
void system_init(void)
{
// 硬件初始化
clock_init();
gpio_init();
uart_init();
// 软件模块初始化
task_manager_init();
error_handler_init();
// 启动系统
start_scheduler();
}
每个初始化步骤之间用空行隔开,读起来是不是舒服多了?
4.3.3 对齐与换行
函数参数太长时,要合理换行。我一般遵循 每行不超过 80 字符 的原则。
// 好的换行
uint32_t calculate_checksum(const uint8_t *data,
uint32_t length,
ChecksumType type);
// 差的换行
uint32_t calculate_checksum(const uint8_t *data, uint32_t length, ChecksumType type);
赋值操作符、比较操作符前后要加空格。这看起来是小事,但能大幅提升可读性。
// 好的风格
int result = a + b;
if (result >= MAX_VALUE) { ... }
// 差的风格
int result=a+b;
if (result>=MAX_VALUE) { ... }
4.4 本章知识体系
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个「编码规范检查清单」来用。
本章核心要点:
- 命名要 自文档化——看到名字就知道用途
- 注释要解释 「为什么」,而不是「是什么」
- 排版要 统一——整个项目保持一致风格
- 所有规范都要 写入项目文档,并用工具自动检查
嗯,这一章的内容就到这里。命名、注释、排版,看起来都是「小事情」,但正是这些小事情,决定了你的代码是「艺术品」还是「垃圾堆」。下次写代码前,不妨多花 30 秒想想命名,多写一行注释说明原因。相信我,三个月后的你会感谢现在的自己。