嵌入式开发实战:7个提升代码质量的工程技巧
1. 嵌入式软件代码质量:一个被低估的“隐形资产”
在嵌入式开发这个行当里,我们常常被各种“硬”指标追着跑:内存还剩多少?CPU负载到顶了吗?中断响应时间达标没?这些当然重要,它们是产品能跑起来的基石。但有一个“软”指标,其重要性往往被严重低估,直到项目后期才以各种诡异Bug、难以维护的代码和团队内耗的形式爆发出来,那就是代码质量。它不是某个具体的性能参数,却深刻影响着开发效率、产品稳定性和团队士气。尤其在资源受限、对可靠性要求极高的嵌入式环境中,高质量的代码不是锦上添花,而是雪中送炭。今天,我们不谈那些宏大的方法论,就从一个一线开发者的视角,分享七个经过实战检验、能切实提升嵌入式代码质量的技巧。这些技巧不依赖于昂贵的工具,更多是思维习惯和编码实践的转变,目标是让你写的代码不仅机器能懂,三个月后的你和你的同事也能轻松看懂、放心修改。
2. 技巧一:拥抱静态分析,让机器成为你的第一道防线
很多嵌入式开发者对静态代码分析工具抱有复杂的感情:觉得它啰嗦、报一堆无关紧要的警告、甚至有时会“误伤”。但我想说,如果你还没把静态分析工具集成到你的日常开发流程中,那你可能正在用最宝贵的时间——你的脑力,去处理机器最擅长发现的低级错误。
2.1 为什么静态分析对嵌入式开发至关重要
嵌入式C/C++代码的一个特点是直接操作硬件,指针满天飞,内存自己管,位操作是家常便饭。这种灵活性带来了威力,也埋下了无数隐患:空指针解引用、数组越界、未初始化的变量、可疑的类型转换、资源泄漏(内存、文件描述符)……这些问题在桌面环境可能只是导致程序崩溃,在嵌入式环境里,轻则功能异常,重则系统死锁、硬件状态错乱,排查起来犹如大海捞针。静态分析工具能在你编译甚至编写代码时,就基于代码的语法、语义和控制流进行分析,提前发现这些潜在缺陷。它就像一个不知疲倦的代码审查员,永远在你提交代码前帮你把关。
2.2 工具选择与集成策略
市面上工具很多,从开源到商业,各有侧重。对于嵌入式开发,我建议至少配置两层静态分析:
第一层是编译器自带的警告。这是最基础也最容易被忽略的。请务必将编译警告级别调到最高(如GCC的
-Wall -Wextra -Werror
)。
-Werror
是关键,它把警告当作错误处理,强制你解决所有警告。一开始可能会很痛苦,因为要清理历史代码,但这能彻底杜绝“这个警告好像没事,先不管了”的心态,从根源上提升代码的严谨性。
第二层是专用的静态分析工具。例如:
- Cppcheck : 轻量、开源、易于集成,对未定义行为和资源泄漏检查有不错的效果。
- Clang Static Analyzer : 与Clang/LLVM工具链深度集成,路径敏感分析能力强,能发现更复杂的问题。
- PC-lint / FlexeLint : 老牌商业工具,规则集极其丰富,对MISRA C等安全编码规范支持好,适合对安全性要求极高的领域(如汽车、医疗)。
集成上,不要等到最后才跑。最好的方式是集成到你的IDE(如VS Code, Eclipse)中,实时提示;其次集成到构建系统(如Makefile, CMake)中,每次编译都自动执行;最起码也要集成到持续集成(CI)流水线中,确保入库的代码都通过了静态检查。
2.3 处理误报与团队磨合
静态分析工具肯定会有误报。正确的做法不是关闭那条规则,而是:
- 评估 : 这个警告是否真的毫无风险?有时它揭示了代码中一些不优雅或存在潜在风险的设计。
-
抑制
: 如果确认是误报,使用工具提供的方式(如特定的注释
// cppcheck-suppress nullPointer)在本地抑制,并附上简短注释说明原因。这保证了警告列表的“洁净”,也留下了审计线索。 - 定制规则 : 根据项目特点,关闭一些确实不相关的规则(如对特定第三方库的编码风格警告),聚焦于安全、缺陷类规则。
让团队接受静态分析需要一个过程。可以从新模块或关键模块开始,展示它发现的真实Bug案例,让大家看到其价值。一旦习惯,你会发现自己对代码的“嗅觉”都变得更敏锐了。
3. 技巧二:编写可测试的代码,从设计之初就考虑“如何测”
嵌入式软件测试难,是共识。硬件依赖、实时性、异步事件……但这绝不能成为代码不可测试的借口。恰恰相反,正因为测试难,我们才更需要在代码设计阶段就为测试铺路。可测试的代码,往往是高内聚、低耦合、职责清晰的代码,其质量本身就更优。
3.1 依赖注入与硬件抽象层(HAL)
这是嵌入式领域实现可测试性的核心手段。不要在你的业务逻辑函数里直接调用
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET)
。这样做的结果是,你想在PC上测试这段逻辑时,必须链接整个硬件库,或者做复杂的桩函数替换。
正确的做法是,通过接口(在C语言中,可以用函数指针结构体)来抽象硬件操作。例如,定义一个
DigitalOutput
接口,包含
set_high
,
set_low
等方法。你的业务代码只依赖这个接口。在真实目标板上,提供基于HAL库的实现;在单元测试环境中,提供一个“模拟(Mock)”实现,这个模拟实现可以记录引脚状态的变化,或者模拟特定的硬件故障,从而让你能在纯软件环境验证业务逻辑的正确性。
// 接口定义(通常放在一个头文件)
typedef struct {
void (*set_high)(void);
void (*set_low)(void);
bool (*read)(void);
} DigitalOutputInterface;
// 业务代码(依赖于接口)
void blink_led(DigitalOutputInterface *led) {
led->set_high();
delay_ms(500);
led->set_low();
delay_ms(500);
}
// 真实硬件实现
void hal_led_set_high() { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); }
// ... 其他函数
DigitalOutputInterface real_led = {hal_led_set_high, hal_led_set_low, hal_led_read};
// 测试用的模拟实现
bool test_led_state = false;
void mock_led_set_high() { test_led_state = true; printf("[Mock] LED set HIGH\n"); }
// ...
DigitalOutputInterface mock_led = {mock_led_set_high, mock_led_set_low, mock_led_read};
这样,测试
blink_led
函数时,你只需要传入
mock_led
,无需任何硬件,就能验证其行为(比如,通过检查
test_led_state
或打印输出来确认)。
3.2 分离时序逻辑与业务逻辑
嵌入式代码中经常混合同步/异步操作、延时、中断。为了可测试,尽量将纯业务逻辑(算法、状态机)与具体的时序、延时操作分离。例如,一个需要延时的任务,可以拆分为“计算下一步状态”和“等待条件满足”两部分。测试时,你只关心“计算”部分,可以轻松注入各种输入并验证输出状态。“等待”部分则在集成测试或硬件在环(HIL)测试中验证。
3.3 利用链接期替换进行集成测试
对于模块间接口的测试,链接期替换是个实用技巧。例如,模块A调用模块B的函数
b_do_something()
。在编写模块A的测试时,你可以创建一个同名的测试桩函数
b_do_something()
,并在链接测试可执行文件时,让它优先链接你的测试桩,而不是真实的模块B。这样就能隔离模块A,测试其调用逻辑和错误处理,而无需关心模块B的内部实现或硬件依赖。这需要构建系统(如CMake)的良好支持来管理不同的构建目标。
4. 技巧三:善用断言(Assert),在问题发生地就地捕获
断言(Assert)是一个强大的防御性编程工具,但它在中大型嵌入式项目中的使用却非常保守,很多人担心它在发布版本中的性能开销。我的观点是:在开发阶段,断言应该被广泛且积极地使用;在发布版本中,则可以有策略地保留关键断言。
4.1 断言 vs. 错误处理
首先要分清两者的职责:
- 错误处理 : 针对预期中可能发生的、可恢复的异常情况,如网络超时、用户输入错误、传感器数据暂时无效。通常通过返回错误码、跳转到错误处理分支等方式。
-
断言
: 用于捕获绝不应该发生的、程序内部的逻辑错误,即“不可能”的条件。例如,函数参数应在某个范围内、指针非空、某个状态机状态是有效的、
switch语句的default分支不应该到达。断言失败意味着代码中存在Bug。
在嵌入式C中,通常使用标准库的
assert(condition)
宏。当条件为假时,它会打印错误信息(文件名、行号)并调用
abort()
。
4.2 嵌入式环境中断言的最佳实践
-
定义清晰的断言策略 : 在项目头文件中自定义断言宏。这允许你统一控制断言在调试(Debug)和发布(Release)版本中的行为。
// config.h #ifdef DEBUG #define MY_ASSERT(expr) \ do { \ if (!(expr)) { \ log_error("ASSERT FAILED: %s, file %s, line %d", #expr, __FILE__, __LINE__); \ while(1) { /* 触发看门狗或进入安全状态 */ } \ } \ } while(0) #else // 发布版本:可以选择禁用,或仅保留最关键的、开销极小的断言 #define MY_ASSERT(expr) ((void)0) // 完全禁用 // 或者:记录到非易失存储器后系统复位 #define MY_ASSERT_CRITICAL(expr) \ do { if (!(expr)) { nv_log_fatal_error(...); system_reset(); } } while(0) #endif使用自定义宏
MY_ASSERT而不是裸的assert,可以让你加入项目特定的错误日志、安全恢复机制(如看门狗触发、系统复位)。 -
在函数入口检查参数 : 这是断言最经典的用法。对于内部函数(非公开API),严格检查输入参数的有效性。
void internal_process_data(uint8_t *buffer, size_t len) { MY_ASSERT(buffer != NULL); MY_ASSERT(len > 0 && len <= MAX_BUFFER_SIZE); // ... 业务逻辑 }这能在Bug传播初期就将其捕获,极大缩短调试时间。你立刻就知道是哪个调用者传入了非法参数。
-
在不变式处使用断言 : 不变式是程序在某个点上必须满足的条件。例如,在操作一个双向链表后,断言其前后向指针的一致性;在完成一个复杂计算后,断言结果在物理意义可能的范围内(如温度值不可能低于绝对零度)。
-
谨慎处理资源相关的断言 : 对于内存分配(
malloc)返回NULL,这在一个资源受限的系统中是“可能”发生的预期错误,更适合用错误处理而非断言(除非你在设计上确保分配一定成功)。但对于一个你刚成功打开的文件描述符,在随后的write操作中返回EBADF(错误的文件描述符),这更可能是一个程序逻辑错误,适合用断言。
4.3 发布版本中的断言
完全禁用所有断言是危险的,它会让一些在测试中未覆盖到的、深藏的条件Bug在现场 silently fail(静默失败),导致后续行为不可预测。一个折中的方案是:在发布版本中,将断言降级为“带日志的故障安全机制”。例如,断言失败时,不立即
abort
,而是将一个错误码和上下文信息记录到非易失存储器(如EEPROM或Flash的特定区域),然后执行一次优雅的系统复位。这样,现场设备“活了回来”,同时留下了宝贵的故障现场信息供后续分析。这需要硬件支持(有独立的记录存储区)和复位恢复逻辑的精心设计。
5. 技巧四:实施一致的编码规范,并自动化检查
编码规范争论是工程师之间的“圣战”。用空格还是Tab?大括号换不换行?变量怎么命名?这些争论本身消耗的精力,往往超过了规范带来的好处。关键在于: 有一个一致的规范,比追求一个“完美”的规范更重要 。一致性让代码可预测,降低了阅读和理解的认知负荷,尤其在多人协作和长期维护中。
5.1 制定与裁剪规范
不要从零开始造轮子。可以参考成熟的规范,如:
- MISRA C/C++ : 汽车等行业的事实安全标准,规则极其严格,侧重于避免未定义行为、提高可移植性和可靠性。对于高安全完整性等级(SIL/ASIL)的项目几乎是强制要求。但规则太多,需要根据项目进行裁剪。
- CERT C/C++ : 侧重于安全编码,防止软件漏洞。
- Google C++ Style Guide : 全面、具体,在互联网行业影响很大。
- 项目自定义规范 : 基于上述某个基础,结合团队习惯和产品特点(如针对实时性、低功耗的特定约定)进行增删。
规范文档应包含:命名约定(全局变量加
g_
前缀?静态变量加
s_
?)、排版格式、注释风格(Doxygen?)、文件组织、头文件保护、特定语言特性的使用限制(如禁用
goto
,限制递归深度)等。
5.2 利用工具实现自动化检查
手动检查编码规范是不可持续的。必须借助工具将其自动化,并集成到开发流程中。
-
格式化工具(Formatter) : 这是第一步,也是争议消除器。工具自动将代码格式化成统一风格。
clang-format是目前C/C++领域最强大的格式化工具,高度可配置。团队只需商定一个.clang-format配置文件,提交到代码库。开发者可以在保存文件时自动格式化(IDE集成),更重要的是在CI流水线中设置一个检查步骤,如果提交的代码不符合格式,直接拒绝合并。这彻底杜绝了格式争论。 -
Linter工具 : 比格式化更进一步,检查代码风格和潜在问题。
clang-tidy是一个集大成者,它基于Clang,能进行语义分析,检查出不仅仅是格式问题,还包括一些编码规范违反(如使用C风格类型转换而不是static_cast)、可能存在的Bug(如变量作用域过大)、以及可以进行的现代化改进(建议使用C++11/14/17的新特性)。你可以为clang-tidy编写自定义的检查规则(Check),来强制执行你们团队的特定约定。 -
CI/CD流水线门禁 : 将格式化检查、Linter检查、静态分析、单元测试等作为代码合并请求(Pull Request)的必须通过的检查项。只有全部通过的代码才能合入主分支。这保证了代码库质量的持续提升,而不是随时间推移而腐化。
5.3 处理历史代码与团队适应
对于存量巨大的历史代码,一次性格式化可能会产生一个巨大的、与功能无关的提交,扰乱版本历史。一个可行的策略是:对新文件和修改过的文件强制执行新规范,对旧文件,在有人主动修改它们时(“童子军规则”:离开时让营地比你来时更干净),逐步进行格式化更新。团队适应期需要技术负责人(Tech Lead)的推动和工具链的顺畅支持,当大家体会到自动化带来的便利和一致性代码的易读性后,就会从被动遵守变为主动依赖。
6. 技巧五:精心设计模块接口,降低耦合与认知负担
嵌入式系统往往是模块化的,但模块之间的接口设计好坏,直接决定了系统是“高内聚、低耦合”的优雅架构,还是一团“意大利面条”式的混乱依赖。一个好的接口,应该是自描述的、稳定的、且易于使用的。
6.1 接口设计原则
-
单一职责与最小化 : 一个头文件(.h)、一个模块,应该只做一件事,并且把这件事做好。避免创建“万能”头文件(如
common.h,utils.h),它们会变成依赖地狱的源头。接口函数应该尽可能少,参数尽可能明确。如果一个函数参数超过4个,或者有一个复杂的结构体参数,就需要考虑是否应该拆分这个函数,或者将这个结构体封装成具有行为的对象(在C++中)或提供一组操作它的函数(在C中)。 -
隐藏实现细节 : 这是最重要的原则之一。在C语言中,可以在头文件中使用不透明指针(Opaque Pointer)。模块对外只提供一个结构体类型的指针,而不暴露其内部成员。所有操作都通过模块提供的函数接口进行。
// my_module.h typedef struct MyModuleContext MyModuleContext; // 前向声明,不完整类型 MyModuleContext* my_module_create(void); void my_module_do_something(MyModuleContext* ctx, int param); void my_module_destroy(MyModuleContext** ctx);这样,模块内部的实现可以自由修改(如增减成员变量、调整算法),只要接口函数的行为不变,所有调用它的代码都无需重新编译或修改。这极大地降低了模块间的编译依赖和耦合度。
-
明确所有权与生命周期 : 接口文档(或通过命名)必须清晰表明资源的所有权。例如,函数
create_xxx()返回一个指针,调用者负责在适当时候调用destroy_xxx()。函数get_xxx()可能只是返回一个内部数据的只读视图,调用者不应修改或释放它。避免出现“谁分配、谁释放”的困惑。 -
使用错误码而非魔术值 : 函数应该通过返回明确的错误码(如定义枚举类型
ModuleError_t)来指示失败,而不是返回一个特殊的魔术值(如-1,NULL)。这更清晰,也便于调用者进行错误处理和传播。对于确实需要返回一个有效值范围的函数,可以考虑将结果和错误码分离(通过输出参数返回结果,函数返回值表示错误)。
6.2 头文件是合同的体现
头文件是模块对外的唯一合同。它必须:
-
自包含(Self-contained)
: 即
#include它所需的所有其他头文件。调用者只需包含这一个头文件就能使用该模块,无需关心其内部依赖顺序。 -
有保护(Include Guards 或
#pragma once) : 防止重复包含。 - 有清晰的注释 : 使用Doxygen风格的注释,说明每个函数的功能、参数含义、返回值、可能的错误码、以及前置/后置条件。这对于生成API文档和团队协作至关重要。
- 稳定 : 一旦发布,尽量避免破坏性更改。如果需要增加功能,优先考虑增加新的函数,而不是修改现有函数的签名。
6.3 依赖方向与层级管理
遵循“依赖倒置”原则:高层模块(业务逻辑)不应依赖低层模块(硬件驱动、具体实现),二者都应依赖抽象(接口)。在嵌入式C中,这通常通过我们之前提到的“接口结构体”(函数指针表)来实现。这允许你在不改变高层业务代码的情况下,替换底层的硬件驱动,为测试和移植提供了极大的灵活性。
同时,要清晰地定义模块的层级关系(如:硬件抽象层 -> 设备驱动层 -> 中间件层 -> 应用层),并严格禁止跨层或反向依赖(如应用层直接调用硬件寄存器操作)。这可以通过物理目录隔离和链接期检查来辅助实现。
7. 技巧六:建立有效的代码审查文化,让知识流动起来
代码审查(Code Review)是提升代码质量最有效、也是成本最低的手段之一,没有之一。它不仅能发现Bug和设计缺陷,更是团队知识共享、统一编码风格、培养新人的绝佳机会。然而,流于形式的、充满火药味的代码审查反而会损害团队士气。关键在于建立一种积极、建设性的审查文化。
7.1 嵌入式代码审查的关注重点
除了通用的代码风格、逻辑正确性,嵌入式代码审查要特别关注以下方面:
-
资源管理
: 内存分配(
malloc/new)与释放(free/delete)是否成对出现?是否有泄漏的可能(如在错误分支上忘记释放)?文件描述符、硬件句柄(如I2C总线句柄)是否正确关闭?中断是否被正确启用/禁用? - 并发与竞态条件 : 共享数据(全局变量、静态变量)在多任务(RTOS任务)或中断上下文访问时,是否使用了正确的同步机制(如互斥锁、信号量、关中断)?是否存在优先级反转的风险?审查者要像侦探一样,思考所有可能的执行路径。
- 实时性与性能 : 循环中是否有不必要的延时或阻塞调用?函数执行时间是否可预测?中断服务程序(ISR)是否足够短小精悍,只做最必要的操作(如设置标志、发送消息)?是否可能存在中断嵌套导致栈溢出?
-
硬件相关假设
: 代码是否对硬件特性(如字节序、内存对齐、寄存器位宽)做了不正确的假设?是否考虑了硬件的不稳定性(如传感器读数抖动)?延时操作是用的忙等待(
for循环)还是基于系统节拍?后者更节能且不影响系统调度。 - 错误处理与恢复 : 是否检查了所有可能失败的函数返回值(特别是库函数、驱动函数)?错误发生时有合理的恢复或降级策略吗?系统是否可能进入一个不可恢复的状态?
7.2 如何进行一场高效的代码审查
- 小批量提交 : 鼓励开发者频繁提交小的、独立的变更集(Change Set)。一次审查几百行代码的效果,远好于一次审查几千行。小变更也更容易回滚。
- 明确审查清单 : 团队可以维护一个针对项目的审查清单(Checklist),作为审查者的指引,确保不会遗漏关键点。
- 使用正确的工具 : 使用GitLab、GitHub、Gerrit等平台的代码审查功能。它们支持行内评论、讨论线程、状态跟踪,比邮件或口头交流高效得多。
- 聚焦于代码,而非个人 : 评论时使用“代码”作为主语,而不是“你”。例如,说“这个循环可能会在中断上下文中被调用,需要考虑加锁”,而不是“你怎么没考虑加锁?”。目标是改进代码,而不是批评作者。
- 提供具体的改进建议 : 不要只说“这里不好”,要尽可能给出“如何修改会更好”的具体建议,甚至可以附上修改后的代码片段。这能加速迭代,也是对作者的帮助。
- 作者的态度 : 将审查视为学习和改进的机会,而不是挑刺。对每一条评论给予回应,解释自己的设计思路,如果接受建议就修改,如果不同意则礼貌讨论。最了解这段代码上下文的人毕竟是作者。
7.3 审查作为学习与传承的渠道
对于团队新人,参与代码审查是快速学习项目架构和编码规范的最佳途径。对于复杂或关键的修改,可以组织小型的、同步的审查会议(或线上会议),由作者讲解变更动机和设计,审查者即时提问。这种面对面的交流往往能碰撞出更好的设计思路。代码审查记录本身也是一份宝贵的学习资料,记录了某个设计决策的来龙去脉。
8. 技巧七:为代码添加有意义的注释与文档,但代码本身应是最好的文档
关于注释,有两个极端:一种是“自文档化代码”的狂热信徒,认为任何注释都是多余的;另一种是事无巨细,给每一行代码都加上注释。在嵌入式开发中,我们需要一种平衡的、务实的注释哲学。
8.1 什么该注释,什么不该注释
-
不该注释的
:
-
直白的代码逻辑
:
i++; // i增加1这类注释毫无价值,反而增加噪音。 -
能从函数/变量名看出的意图
: 如果函数名
calculate_average_temperature已经很清楚,就无需在函数头写“此函数用于计算平均温度”。
-
直白的代码逻辑
:
-
必须注释的
:
-
“为什么”(Why)
: 这是注释的最高价值所在。解释这段代码为什么存在,为什么用这种方法而不是另一种,当时考虑了哪些权衡,背后的业务规则或物理原理是什么。例如:
// 使用查表法而非实时计算,因为浮点运算在目标MCU上开销过大,且温度范围有限。 // 表数据基于传感器校准曲线生成,每0.5度一个点。 uint16_t temp_adc_to_celsius(uint16_t adc_value) { // ... 查表实现 } - 复杂的算法或逻辑 : 如果实现了一个非平凡的算法(如特定的滤波算法、协议解析状态机),用注释概述其原理和步骤。
- 解决过的坑和陷阱 : 如果某段代码是为了绕过一个硬件Bug、编译器特性或第三方库的缺陷,一定要详细注释,包括Bug的现象、参考的工单号、测试条件等。这能防止后来人“优化”掉这段看似多余的代码。
- 公开API(头文件) : 使用Doxygen等格式,为每个模块、函数、参数、返回值、错误码撰写清晰的文档。这是给模块使用者的合同。
- 全局变量和重要的数据结构 : 说明其用途、取值范围、并发访问要求等。
-
魔法数字(Magic Number)
: 任何直接出现在代码中的字面常量,除非是0、1这样的,或者含义极其明显(如
buffer[256]),否则都应该用有意义的枚举或常量定义替换,并加以注释。例如,if (status == 0x5A)不如if (status == DEVICE_STATUS_READY)清晰。
-
“为什么”(Why)
: 这是注释的最高价值所在。解释这段代码为什么存在,为什么用这种方法而不是另一种,当时考虑了哪些权衡,背后的业务规则或物理原理是什么。例如:
8.2 嵌入式特有的注释要点
-
硬件寄存器操作
: 在配置硬件寄存器时,注释应引用数据手册的相关章节和位域描述。
// 配置USART2: 8数据位,无校验,1停止位,波特率115200。 // 参考RM手册 39.6.4 USART_BRR寄存器计算。 USART2->BRR = (SystemCoreClock / 115200); // 假设系统时钟已配置正确 USART2->CR1 |= USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; // 使能发送、接收、USART -
时序要求
: 如果代码中有延时或等待某个硬件信号,注明要求的时序参数。
// 根据数据手册,发送复位命令后,需要等待至少1ms才能进行下一步操作。 send_reset_command(); delay_ms(2); // 留有余量,等待2ms -
并发与中断上下文
: 明确标注哪些函数或代码段只能在任务上下文调用,哪些可以在中断上下文调用,以及所需的锁或保护机制。
/** * @brief 更新显示缓冲区。非线程安全,调用者需持有g_display_mutex。 * @note 此函数不能在中断服务程序中调用。 */ void update_display_buffer(const char* text) { // ... }
8.3 让代码自文档化
最好的文档是代码本身。通过做到以下几点,可以大幅减少对解释“做什么”(What)的注释的需求:
-
使用有意义的命名
: 变量、函数、模块名要清晰表达其意图。
read_sensor()比get_data()好;is_button_pressed()比check_button()好。 - 保持函数短小、功能单一 : 一个函数只做一件事,并且做好。这样函数名本身就足以说明其行为。
-
使用枚举和常量代替魔法数字
:
case STATE_IDLE:远比case 0:好懂。 - 分解复杂表达式 : 将复杂的条件判断或计算公式拆分成多个有中间变量的步骤,并为这些中间变量起好名字。
注释不是用来弥补糟糕代码的。当你觉得需要写很多注释来解释一段代码在干什么时,首先应该考虑的是:能否重构这段代码,让它变得更清晰?当代码已经足够清晰时,注释就应该专注于解释那些代码本身无法表达的“为什么”和背后的设计决策。
更多推荐


所有评论(0)