【EMBEDDED】代码设计规范
作者:
Marisa9961 [email protected]
Marisa9961 : 以下内容总结自个人建议与互联网内容,如有失误还请指出
nmpassthf [email protected]
nmpassthf : 加了一点工作中遇到的负面例, 不要学
在电赛中,我们在使用 C/C++ 编写代码时,需要时刻注意保持代码的规范和整洁。这样的好习惯不仅在调试时能够帮助你保持清晰的思路,也能在回顾旧代码时快速回忆思路。
chap 0 优秀的代码风格是什么样的?
在 Google开源项目风格指南 中,一套优秀的代码规范应该能做到如下需求:
- 风格规则应该有影响力
- 为读者优化, 而非为作者优化
- 和现有代码保持一致
- 恰当时与广大 C++ 社区保持一致
- 避免使用奇特或危险的语法结构
- 避免使用那些正常水平的 C++ 程序员认为棘手或难以维护的语法结构
- 需要注意我们的规模
- 在必要时为优化让路
其中笔者认为最重要的一条,就是 “为读者优化, 而非为作者优化” ,这一理念提醒我们,编写代码时要始终将 可读性 放在重点。这样的编码习惯不仅能提升个人开发效率,更能促进团队协作的流畅性,同时也为应对未来可能出现的复杂需求奠定了良好的基础。
然而,由于 Google开源项目风格指南 并非专为C语言设计,在嵌入式开发领域不能完全照搬。为此,笔者基于该指南进行适配调整,提出一套适用于嵌入式系统的代码规范,并针对交叉编译环境及C/C++混合编程等特殊场景下的注意事项进行详细解析。
本规范将大量引用 Google开源项目风格指南 的原文示例,并附上笔者的解读与思考。为获得最佳理解效果,建议读者在条件允许时参考阅读原始指南。
chap 1 代码设计规范
chap 1.1 命名约定
chap 1.1.1 变量命名
在变量命名时, 可读性 必须作为首要原则。命名应当具有充分的描述性,无需刻意追求简短。
而且现代IDE的自动补全功能,能显著降低长变量名的输入成本,切不可为了节省几个字符而牺牲代码的清晰表达。
int n; // 毫无意义.
int nerr; // 含糊不清的缩写.
int n_comp_conns; // 含糊不清的缩写.
int wgc_connections; // 只有贵团队知道是什么意思.
int pc_reader; // "pc" 有太多可能的解释了.
int cstmr_id; // 删减了若干字母.int price_count_reader; // 无缩写
int num_errors; // "num" 是一个常见的写法
int num_dns_connections; // 人人都知道 "DNS" 是什么变量 (包括函数参数) 和数据成员名一律 小写 , 单词之间用下划线连接。
即便不熟悉如何用单词表达变量,也可以通过搜索翻译或者提问AI的方式,编写一个更好的命名。切记,不要使用拼音,不要使用拼音,不要使用拼音!
int ZhengXian[256]; // 错误:随意使用拼音
int sine[256]; // 正确:表意清晰另外在一些约定俗成的使用场景下,使用短缩写并无影响,例如在 for 循环中使用 i 作为迭代变量。至于哪些短缩写是合理,还请自行积累经验。
chap 1.1.2 函数命名
常规函数使用大小写混合(即 “驼峰变量名” 或 “帕斯卡变量名” )没有下划线。
对于首字母缩写的单词, 更倾向于将它们视作一个单词进行首字母大写。 例如 PWM ,就可写作 Pwm ,统一函数命名风格。
与此同时注意,函数的命名也应当具有描述性,需要清晰地表达函数功能。
AddTableEntry()
DeleteUrl()
OpenFileOrDie()chap 1.1.3 常量命名
此处讨论的常量包括:常量、枚举、宏;这类数据在程序运行期间应该始终保持不变,故综合起来讨论。
在 Google开源项目风格指南 中,声明为 const 的变量,命名时以 “k” 开头,大小写混合。
const int kDaysInAWeek = 7;然而笔者其实并不完全赞同这样的写法,更倾向于 DAYS_IN_A_WEEK 这类更为醒目的写法,例如写作如下形式:
const int DAYS_IN_A_MOUTH = 30;
enum {DAYS_IN_A_YEAR = 365};之所以 Google开源项目风格指南 中的常量这么奇怪,其实是为了严格区分常量与宏,进而避免宏可能带来安全隐患,如无类型检查、展开后的意外行为或是作用域污染等。
不过代码风格的选择并无绝对的对错,关键在于一致性和可读性。不必因为某一权威规范而过度拘束,重要的是选择适合自己项目的风格。毕竟,代码是写给人看的,而不是机器。
chap 1.1.4 类型命名
类型名称的每个单词首字母均大写,不包含下划线。
所有的自定义类型命名都应遵守这个原则,所有类型命名 —— 结构体,联合体,类型定义 (typedef),枚举 —— 均使用相同约定, 即以大写字母开始, 每个单词首字母均大写, 不包含下划线。
// 结构体
struct DatabaseSchema { ...
// 联合体
union DatabaseRecord { ...
// 类型定义
typedef DatabaseRecord* RecordPtr;
// 枚举
enum DatabaseTableType { ...chap 1.1.5 文件命名
文件和文件夹的命名,都统一使用小写字母和下划线的形式。
以下的命名形式都是可以接受的:
- gpio.c
- uart_driver.c
- freertos_hooks.c
通常应尽量让文件名更加明确. http_server_logs.h 就比 logs.h 要好;文件名一般成对出现,如 foo_bar.h 和 foo_bar.c 。
chap 1.2 头文件
正确使用头文件会大大改善代码的可读性和执行文件的大小、性能。通常每个 .c 文件应该有一个配套的 .h 文件,常见的例外情况包括单元测试和仅有 main() 函数的 .c 文件。
chap 1.2.1 避免重复编译
在 C 的惯例用法当中,我们应该注意避免头文件的重复编译。使用类似如下的条件编译,即可达到效果。
#ifndef __LCD_H__
#define __LCD_H__
...
#endif // __LCD_H__同时为了保证符号的唯一性, 防护符的名称应该基于该文件在项目目录中的完整文件路径。例如,项目中的文件 app/tasks/gui.h 应该有如下防护:
#ifndef __APP_TASKS_GUI_H__
#define __APP_TASKS_GUI_H__
...
#endif // __APP_TASKS_GUI_H__chap 1.2.2 头文件顺序
推荐按照以下顺序导入头文件: 配套的头文件, C 语言系统库头文件, C++ 标准库头文件, 其他库的头文件, 本项目的头文件。
举例来说,就是如下形式:
#include "app/tasks/gui.h"
#include <stdio.h>
#include <string.h>
#include <memory>
#include <utility>
#include <lvgl/lvgl.h>
#include <porting/lv_port_disp.h>
#include <porting/lv_port_indev.h>
#include "components/lcd/st7789.h"chap 1.2.3 内联函数
你可以通过使用 inline 声明建议编译器展开函数, 编译器会将函数指令硬编码至调用处,进而避免出栈入栈的开销。只要内联函数体积较小, 内联函数可以令目标代码更加高效。
inline int add(int a, int b) {
return a + b; // 声明为 inline 的函数
}
int main() {
int x = 5, y = 3;
int result = add(x, y); // 编译器可能会将 add(x, y) 直接替换为 (x + y)
}注意, inline 关键字只是向编译器提供一个优化建议,编译器会根据自身优化策略决定是否真正内联函数(比如太复杂的函数即使加了 inline 也可能不被内联)。
合理的经验法则是不要内联超过 10 行的函数,否则可能会导致程序体积增大。
另一个实用的经验准则: 内联那些有循环或 switch 语句的函数通常得不偿失 (除非这些循环或 switch 语句通常不执行)。
chap 1.3 函数
chap 1.3.1 简短凝练
我们承认长函数有时是合理的, 因此并不硬性限制函数的长度。但是如果函数超过 40 行,可以思索一下能不能在不影响程序结构的前提下对其进行分割。
将其中功能重复的模块提取出来,将程序功能解耦,提高逻辑性和复用率。
chap 1.3.2 合理使用常量修饰
在函数中使用常量修饰 const 可以提高代码的安全性和可读性。
合理使用 const 可以明确标识哪些变量或参数在函数执行过程中不会被修改,从而避免意外的修改行为。
void print_buffer(const char *buf, size_t len) {
// 若内部不小心修改了 buf 指向的内容,不符合函数意图,编译器会报错...
}
const char* get_error_message(int code) {
// 防止外部调用的时候,错误修改指针指向的内容...
}chap 1.3.3 参数整合
在一些情况下,函数参数会出现参数多,类型相似,一旦顺序写错也不易察觉;而且阅读代码时看不见参数名,不清楚每个参数分别代表什么。例如下面的情况:
void UART_Init(uint32_t baudrate, uint8_t data_bits, uint8_t stop_bits, uint8_t parity);
int main() {
UART_Init(115200, 8, 1, 0); // 哪个是 data_bits 哪个是 parity
}我们可以将上述参数包装成结构体:
typedef struct {
uint32_t baudrate;
uint8_t data_bits;
uint8_t stop_bits;
uint8_t parity;
} UART_Config;
void UART_Init(const UART_Config *config);
int main() {
UART_Config usart1_cfg = {
.baudrate = 115200,
.data_bits = 8,
.stop_bits = 1,
.parity = 0,
}; // 显式赋值结构体成员,避免误传参数
UART_Init(&usart1_cfg);
}当然,返回类型也可以考虑使用结构体包装:
typedef struct {
float temperature;
float humidity;
} sensor_data_t;
sensor_data_t sensor_read(void);chap 1.4 格式
chap 1.4.1 函数声明与定义
返回类型和函数名在同一行,参数也尽量放在同一行,如果放不下就对形参分行,分行方式与函数调用一致。
函数应该看上去像这样:
ReturnType Function(Type par_name1, Type par_name2) {
DoSomething();
}如果同一行文本太多, 放不下所有参数:
ReturnType ReallyLongFunctionName(Type par_name1, Type par_name2,
Type par_name3) {
DoSomething();
}甚至连第一个参数都放不下:
ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
Type par_name1, // 4 空格缩进
Type par_name2,
Type par_name3) {
DoSomething(); // 2 空格缩进
}理所当然的,在函数调用的时候也可以考虑这样的缩进对齐,
bool retval = DoSomething(argument1, argument2, argument3);
bool retval = DoSomething(averyveryveryverylongargument1,
argument2, argument3);
if (...) {
...
if (...) {
DoSomething(
argument1, argument2, // 4 空格缩进
argument3, argument4);
}
}上述的代码格式可以自由发挥,针对不同情景下的不用应用更改,只需要保持可读性和整洁性。
看到这里是不是已经有点晕了?
不用担心,后面会介绍代码自动格式化工具
只需要按下快捷键就能迅速整理代码格式,别以为所有人都是这样老老实实手敲出来的
chap 1.4.2 条件判断
对基本条件语句有两种可以接受的格式,一种在圆括号和条件之间有空格,另一种没有。
if (condition) { // 圆括号里没有空格
... // 2 空格缩进
} else if (...) { // else 与 if 的右括号同一行
...
} else {
...
}
if ( condition ) { // 圆括号与空格紧邻 - 不常见
... // 2 空格缩进
} else { // else 与 if 的右括号同一行
...
}两种情况都可以使用,区别不大,可以酌情选择。
chap 1.4.3 选择判断
switch 语句可以使用大括号分段,以表明 cases 之间不是连在一起的。在单语句循环里,括号可用可不用。
如果有不满足 case 条件的枚举值, switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理, 编译器将给出 warning)。
空循环体应使用 {} 或 continue 。
switch (var) {
case 0: { // 2 空格缩进
... // 4 空格缩进
break;
}
case 1: {
...
break;
}
default: {
assert(false);
}
}chap 1.4.4 循环
在单语句循环里,括号可用可不用:
for (int i = 0; i < kSomeNumber; ++i)
printf("I love you\n");
for (int i = 0; i < kSomeNumber; ++i) {
printf("I take it back\n");
}空循环体应使用 {} 或 continue,而不是一个简单的分号。
while (condition) {
// 反复循环直到条件失效
}
for (int i = 0; i < kSomeNumber; ++i) {} // 可 - 空循环体
while (condition) continue; // 可 - contunue 表明没有逻辑chap 2 代码格式化工具
chap 2.1 代码格式化
对于团队开发而言,要求每一位成员都能时时刻刻遵守代码规范是不可能的,手动维护代码的整洁性要耗费大量的精力和时间,而且往往都是机械性的重复劳动。
在这种情况下,我们往往会借助代码格式化工具来确保所有人风格统一。
对于 C/C++ 的代码格式化而言,有一款强大的工具叫做 clang-format 。
TODO GIF 代码格式化功能演示 动图
只要按下快捷键,就能快速达到上面的效果。
其实在前面章节中,讲到格式的时候笔者做了很多的删减。原版的谷歌风格指南规定了很多关于代码缩进,以及代码前后之间空格间距的要求,远不止 chap 1.4 中的那点内容,手敲这些内容只会让你怀疑人生。
所以别再自己一点一点的敲空格和回车了,直接使用格式化工具来整理代码吧。
由于笔者的主要使用的 IDE 是 Visual Studio Code ,讲解主要以 Vscode 为基础。
其他现代化 IDE 诸如 Visual Studio 、 Clion 等都包含自带的格式化功能,甚至包括 Vim/Neovim 等文本编辑器也可以通过插件的形式调用 clang-format 等代码格式化工具。
各位有兴趣的读者可以自行探索,接下来会详细讲解 clang-format 具体配置。
嵌入式领域常用的 IDE 例如 Keil 似乎也支持代码格式化功能,但笔者也并未实践过。笔者非常不推荐使用 Keil 编写代码,具体原因过多,暂不列出。
chap 2.2 使用方法
chap 2.2.1 clang 插件
既然 clang-format 是属于 clang 工具链中的一个独立工具,属于 clang 生态的一部分,那么最好的用法就是直接使用 Vscode 中的 clang 插件。
在安装好 clang 插件后,可以不需要任何配置,直接使用快捷键 shift+alt+f 即可对 C/C++ 代码进行格式化整理。
当然如果你喜欢其他的代码格式,也可以通过在项目根目录新建 .clang-format 文件,并在其中规定格式规范。
clang-format 具有多种内置的代码风格,例如: LLVM, Google, Chromium, Mozilla, Microsoft, GNU 等诸多知名互联网厂商的代码规范,其中在不进行任何配置的情况下默认使用 LLVM 规范。
.clang-format 文件采用 yaml 的格式,十分便于阅读和修改,例如下面的样例:
BasedOnStyle: Google
IndentWidth: 4
TabWidth: 4
PointerAlignment: Right
DerivePointerAlignment: false在这套配置中,就指定了以 Google 代码风格为基础,以四个空格缩进为标准,保证指针符号 * 始终右对齐。
另外如果觉得这些参数很头疼,也可以尝试使用 clang-format configurator 这类图形化配置网站,可以实时反应你的配置改动,更方便地找到自己的喜好。
chap 2.2.2 C/C++ 插件
如果你不熟悉 clang 插件,也可以使用微软官方提供的 C/C++ 插件。
但其实在你安装 C/C++ 插件的同时,扩展程序会自动安装 clang-foramt 到你的扩展程序目录。
具体的使用方式与 clang 插件无异,但是可以通过在 settings 中的图形化界面里配置具体的代码风格,不过还是绕不开 Clang-Format Options 编写。
顺带一提,笔者非常不推荐使用 C/C++ 插件,不仅性能堪忧,且代码补全与检查远不如 Clang 插件。
nmpassthf 注: 这里要注意区分
代码高亮(IntelliSense)和代码格式化(clang-format)的区别。
LLVM系列的工具软件中,clangd负责提供代码高亮,clang-format负责代码格式化, 其职责是分开的。
chap 2.2.3 命令行
当然,如果你的开发环境足够硬核,既没有 IDE 也没有乱七八糟的插件,也可以使用命令行的方式调用 clang-format 直接对代码文件整理。
clang-format -i src/main.cpp如果需要指定 .clang-format 文件,就可以使用如下命令:
clang-format -i -style=file src/main.cpp或者直接指定所需要的参数:
clang-format -i -style="{BasedOnStyle: Google, IndentWidth: 4}" src/main.cppchap 3 尾声
优秀的代码风格不仅提升个人开发中的效率,也能为团队开发提供长期保障。通过遵循命名约定、合理的函数设计和一致的代码格式,我们不仅能提升代码的可读性和可维护性,还能显著降低团队协作成本。自动化工具 clang-format 可以帮助我们保持格式统一,时刻保持代码格式的整洁与有序。
伟大的程序往往遵循大道至简的原则,以简洁、高效与良好的解耦设计为根基。在个人开发中,我们同样应当培养这些习惯,让代码逻辑清晰、结构稳固,从而在面对复杂需求时依旧保持可扩展性与优雅性。
chap 0b100 负面例子
原本只是想写点 Counterexample 的, 结果越写越带入感情了, 上班上的 (
根据真实代码改编, 但并不完全相同
仅做学习用, 不包含实际业务逻辑
版权所有
版权归属:nmpassthf
