Skip to content

【EMBEDDED】代码设计规范

约 4551 字大约 15 分钟

嵌入式

2025-08-04

作者:

  • 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.hfoo_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.cpp

chap 3 尾声

优秀的代码风格不仅提升个人开发中的效率,也能为团队开发提供长期保障。通过遵循命名约定、合理的函数设计和一致的代码格式,我们不仅能提升代码的可读性和可维护性,还能显著降低团队协作成本。自动化工具 clang-format 可以帮助我们保持格式统一,时刻保持代码格式的整洁与有序。

伟大的程序往往遵循大道至简的原则,以简洁、高效与良好的解耦设计为根基。在个人开发中,我们同样应当培养这些习惯,让代码逻辑清晰、结构稳固,从而在面对复杂需求时依旧保持可扩展性与优雅性。

chap 0b100 负面例子

原本只是想写点 Counterexample 的, 结果越写越带入感情了, 上班上的 (

根据真实代码改编, 但并不完全相同

仅做学习用, 不包含实际业务逻辑