跳转至

嵌入式编程风格指南

hh

写在前面

作为一名电控组成员,最重要的作品便是为机器人系统编写的代码。我们希望它能够成为一种构筑现实的材料、沟通思想的桥梁,同时也希望它足够清晰、简明、流畅、优雅,准确地表达每一个改变世界的想法。

事实上,队员的编程风格迥异,这可能或已经导致:

  • 代码含义不明、表达混乱,存在废弃代码和重复代码
  • 编程风格夹杂,大大降低了代码的可读性
  • 代码耦合严重,可复用的部分难以移植
  • 工程合作难以进行
  • 代码整理、归档没有统一的规范,不利于传承和开源

因此,我们基于权威而广泛使用的 Google 编程风格指南(C++),并基于具体需求,编写战队嵌入式编程风格(C++ 语言)指南,旨在让所有电控队员获知统一、规范的编程风格,并尽量遵循该风格进行源码的贡献,编写简洁、可维护、可靠、可测试、高效、可移植的代码。

我们强烈建议使用面向对象的风格设计嵌入式程序。考虑到资源限制、效率需求,所写代码中无特殊需求不要使用 STL,Eigen 等模板库(由于开源模板库中有大量嵌套,导致不开优化时严重耗时,而实际为方便调试,又不会开启优化),同时尽量少依赖于 std 库中的复杂部分。

规则的作用是避免混乱,但规则本身一定要权威,有说服力,并且是理性的。

Google 保持其一贯的严谨精神,5 万汉字的指南涉及广泛,论证严密。我们翻译该系列指南的主因也正是其严谨。严谨意味着指南的价值不仅仅局限于它罗列出的规范,更具参考意义的是它为了列出规范而做的谨慎权衡过程。

指南不仅列出你要怎么做,还告诉你为什么要这么做,哪些情况下可以不这么做,以及如何权衡其利弊。其他团队未必要完全遵照指南亦步亦趋,如前面所说,这份指南是 Google 根据自身实际情况打造的,适用于其主导的开源项目,其他团队可以参照该指南,或从中汲取灵感,建立适合自身实际情况的规范。

—— 摘自《Google 开源项目风格指南:C++ 风格指南》中文版译者序

以下内容为《Google 开源项目风格指南:C++ 风格指南》中文版的重要内容摘取,同时部分地方根据实际需求进行进一步要求或是更改要求,因此建议先熟悉《Google 开源项目风格指南:C++ 风格指南》中文版后再阅读本文档

1 头文件

通常每一个 .cpp 文件都有一个对应的 .hpp 文件。

1.1 自给自足的头文件

所有头文件应该自给自足,也就是头文件的使用者和重构工具在导入文件时无需任何特殊的前提条件。

1.2 #define 防护符

所有头文件都使用 #define 来防止重复导入,防护符的格式是:<项目>_<路径>_<文件名>_HPP_

为了保证符号的唯一性,防护符的名称应该基于该文件在项目目录中的完整文件路径(忽略 inc/)。例如,foo 项目中的文件 foo/bar/inc/baz.hpp 应该有如下防护:

C++
1
2
3
4
#ifndef FOO_BAR_BAZ_HPP_
#define FOO_BAR_BAZ_HPP_
...
#endif  /* FOO_BAR_BAZ_HPP_ */

1.3 内联函数

合理的经验法则是不要内联超过 10 行的函数。谨慎对待析构函数。析构函数往往比表面上更长,因为会暗中调用成员和基类的析构函数!

另一个实用的经验准则:内联那些有循环或 switch 语句的函数通常得不偿失 (除非这些循环或 switch 语句通常不执行)。

2 作用域

2.1 命名空间

建议按如下方法使用命名空间:

  • 遵守命名空间命名规则。
  • 在导入语句、其他命名空间的类的前向声明(forward declaration)之后,用命名空间包裹整个源代码文件:
C++
// .hpp 文件
namespace mynamespace
{
/* 所有声明都位于命名空间中 */
/* 注意没有缩进 */
class MyClass
{
public:
  ...
  void Foo();
};
}  // namespace mynamespace
C++
1
2
3
4
5
6
7
8
9
// .cpp 文件
namespace mynamespace
{
/* 函数定义位于命名空间中 */
void MyClass::Foo()
{
  ...
}
}  // namespace mynamespace
  • 不要在 std 命名空间内声明任何东西。不要前向声明(forward declare)标准库的类。在 std 命名空间内声明实体是未定义行为(undefined behavior),也就是会损害可移植性。若要声明标准库的实体,应该导入对应的头文件。
  • 禁止使用 using 指令引入命名空间的所有符号。
C++
/* 禁止:这会污染命名空间 */
using namespace foo;
  • 除了在明显标注为内部使用的命名空间内,不要让头文件引入命名空间别名(namespace alias)。这是因为头文件的命名空间中引入的任何东西都是该文件的公开 API。正确示例:
C++
/* 在 .cpp 中,用别名缩略常用的名称 */
namespace baz = ::foo::bar::baz;
C++
/* 在 .hpp 中,用别名缩略常用的命名空间 */
namespace librarian
{
namespace impl
{  // 仅限内部使用,不是 API
namespace sidetable = ::pipeline_diagnostics::sidetable;
}  // namespace impl

inline void my_inline_function()
{
  /* 一个函数(f 或方法)中的局部别名 */
  namespace baz = ::foo::bar::baz;
  ...
}
}  // namespace librarian
  • 禁止内联命名空间。
  • 如果命名空间的名称包含 “internal”,代表用户不应该使用这些 API。
C++
/* Absl 以外的代码不应该使用这一内部符号 */
using ::absl::container_internal::ImplementationDetail;

2.2 内部链接

若其他文件不需要使用 .cpp 文件中的定义,这些定义可以放入匿名命名空间(unnamed namespace)或声明为 static,以实现内部链接(internal linkage)。但是不要在 .hpp 文件中使用这些手段。

匿名命名空间的声明应与具名命名空间的格式相同,在末尾的注释中,不用填写命名空间名称:

C++
1
2
3
4
namespace
{
...
}  // namespace

2.3 非成员函数、静态成员函数和全局函数

建议将非成员(nonmember)函数放入命名空间;尽量不要使用完全全局的函数(completely global function)。不要仅仅为了给静态成员(static member)分组而使用类(class)。类的静态方法应当和类的实例或静态数据紧密相关。

2.4 局部变量

应该尽可能缩小函数变量的作用域(scope),并在声明的同时初始化。

2.5 静态和全局变量

由于嵌入式开发的特殊性,即不存在程序结束的情况,因此允许使用静态存储周期的变量,但需满足变量初始化时不依赖与其他部分的初始化,即整个程序只有这个变量初始化时也成速顺利通过。对于有依赖关系的情况,可以采用先声明指针,后续通过 new 的方式获取或是类提供无参构造函数并提供 init 等初始化方法在后续自行根据依赖关系进行初始化。

举个例子,比如某个静态类的实例的构造函数包含对 HAL 库的调用,但 HAL 库很大一部分函数只有在 main 函数最前面一部分代码完成相关初始化后才可正常使用,但该静态实例在 main 函数运行前便会调用类的构造函数,但该构造函数依赖 HAL 库的初始化,因而会产生错误,故而禁止该种情况。

同时,建议不要通过 extern 的方式提供全局变量,对于完全单一的类对象,建议使用单例模式,而其余情况下的全局变量建议通过函数接口的形式获取全局变量的指针等方式进行访问。

3 类

3.1 构造函数的内部操作

构造函数(constructor)中不得调用虚函数(virtual method)。不要在没有错误处理机制的情况下进行可能失败的初始化。

若实在存在可能失败的初始化建议再关闭中断后直接跳入死循环的函数中,避免后续因类初始化失败导致的错误运行,若使用组件库则可考虑使用其中提供的 HW_ASSERT 宏进行判断。

3.2 隐式类型转换

不要定义隐式类型转换。定义类型转换运算符单个参数的构造函数时,请使用 explicit 关键字。

3.3 可拷贝类型和可移动类型

类的公有接口必须明确指明该类是可拷贝的、仅可移动的、还是既不可拷贝也不可移动的。如果该类型的复制和移动操作有明确的语义并且有用,则应该支持这些操作。

3.4 结构体还是类

只能用 struct 定义那些用于储存数据的被动对象。其他情况应该使用 class

允许结构体具有构造、析构、赋值方法,若仍有其他需求则应使用类。

3.5 继承

通常情况下,组合(composition)比继承(inheritance)更合适。所有继承都应该使用 public 的访问权限。如果要实现私有继承,可以将基类对象作为成员变量保存,当您不希望您的类被继承时,可以使用 final 关键字。

明确使用 overridefinal(较少使用)关键字限定重写的虚函数或者虚析构函数。原因:如果函数带有 overridefinal 关键字,却没有正确重写基类的虚函数,会导致编译错误,有助于发现常见笔误。

允许多重继承, 建议为一个实现继承,而其他的父类为纯接口类,但强烈建议避免多重实现继承。

对于实在无法避免的菱形继承,请使用虚继承。

3.6 运算符重载

谨慎使用运算符重载(overload)。禁止自定义字面量(user-defined literal)。

3.7 访问控制

类除了常量外的数据成员不得声明为公有(public),对于要修改或是访问的数据成员应提供对应的访问接口,而不是直接声明为公有。

为保证整体的运行效率,允许将基类(建议为抽象基类)的数据成员声明为保护(protected),以便于子类高效快速地访问。

3.8 声明次序

类的定义通常以 public: 开头,其次是 protected:,最后以 private: 结尾。空的部分可以省略。

在各个部分中,应该将相似的声明分组,并建议使用以下顺序:

  1. 类型和类型别名(using,typedef,enum,嵌套结构体和类,友元类型)

  2. (可选,仅适用于结构体)非静态数据成员

  3. 静态常量

  4. 工厂函数(factory function)

  5. 构造函数和赋值运算符

  6. 析构函数

  7. 所有其他函数(非静态成员函数,静态成员函数,友元函数)

  8. 所有其他数据成员(非静态,静态)

不要在类定义中放置大段的函数定义。通常,只有简单、对性能至关重要且非常简短的方法可以声明为内联函数。

4 函数

4.1 输入和输出

我们倾向于按值返回,否则按引用返回。避免返回指针。除非它可以为空。

我们允许返回如单例模式等具有全生命周期的指针。

C++ 函数由返回值提供天然的输出,有时也通过输出参数(或输入/输出参数)提供。我们倾向于使用返回值而不是输出参数:它们提高了可读性,并且通常提供相同或更好的性能。

非可选输入参数通常是值参或 const 引用, 非可选输出参数或输入/输出参数通常应该是引用(不能为空)。对于可选的参数,我们使用带无效值的缺省参数来表示可选的按值输入, 使用 const 指针来表示可选的其他输入。使用非常量指针来表示可选输出和可选输入/输出参数

在排序函数参数时,将所有输入参数放在所有输出参数之前。特别要注意,在加入新参数时不要因为它们是新参数就置于参数列表最后,而是仍然要按照前述的规则,即将新的输入参数也置于输出参数之前。

4.2 编写简短函数

我们倾向于编写简短,凝练的函数。我们承认长函数有时是合理的,因此并不硬性限制函数的长度。如果函数超过 40 行,可以思索一下能不能在不影响程序结构的前提下对其进行分割。

4.3 缺省参数

对于虚函数,不允许使用缺省参数,因为在虚函数中缺省参数不一定能正常工作。如果在每个调用点缺省参数的值都有可能不同,在这种情况下缺省函数也不允许使用。(例如, 不要写像 void f(int n = counter++); 这样的代码)

4.4 可重入函数的保护

当函数希望实现可重入特性(或被迫应具有重入特性,如 HAL_UARTEx_RxEventCallback 函数会同时被串口空闲中断与 DMA 半接收触发),即可被多线程并发调用时,应避免使用全局变量和 static 局部变量,否则需要选择互斥手段进行保护:

  • 关闭全局中断
  • 在操作变量前申请信号量,操作完毕方可释放信号量

5 其他 C++ 特性

5.1 右值引用

只在定义移动构造函数与移动赋值操作时使用右值引用。

5.2 变长数组和 alloca()

我们不允许使用变长数组和 alloca()

变长数组在较高版本的 cpp 下能通过编译,但是对于长度交长的变长数组在生成时会消耗不少时间,同时容易引起难以发现的内存越界

5.3 友元

我们允许合理的使用友元类及友元函数。

5.4 类型转换

建议使用 C++ 的类型转换,如 static_cast<>()。但不禁止使用 int y = (int)x 或 int y = int(x) 等转换方式,但要求使用者一定要清楚自己的意图

5.5 const 用法

const 变量。数据成员,函数和参数为编译时类型检测增加了一层保障;便于尽早发现错误。因此,我们强烈建议在任何可能的情况下使用 const

  • 如果函数不会修改你传入的引用或指针类型参数,该参数应声明为 const
  • 尽可能将函数声明为 const。访问函数应该总是 const。其他不会修改任何数据成员, 未调用非 const 函数,不会返回数据成员非 const 指针或引用的函数也应该声明成 const
  • 如果数据成员在对象构造之后不再发生变化, 可将其定义为 const

此外有时改用 C++11 推出的 constexpr 更好。

静态常量建议写为该形式:static const int kStaticNum = 1;

5.6 整型

通过 #include <cstdint> 后,则可使用 int8_tuint16_t 这类表明大小与有无符号的类型进行编程,编程中建议使用大小能满足需求的类型即可,如:

C++
1
2
3
for (uint8_t i = 0; i < 20; i++) { // 0~255 的数值范围以够用
    ...
}

同时,还可通过 #include <cstddef> 后使用 size_t 变量类型。

注意:使用整形时应格外注意其数值范围,如无符号整型不可能为负、出现在比较有符合变量和无符号变量时,主要是 C 的类型提升机制会致使无符号类型的行为出乎你的意料。

5.7 0nullptrNULL

指针使用 nullptr,字符使用 '\0'(而不是 0 字面值)。

对于指针(地址值),使用 nullptr,因为这保证了类型安全。

使用 '\0' 作为空字符。使用正确的类型使代码更具可读性。

5.8 sizeof

尽可能用 sizeof(varname) 代替 sizeof(type)

使用 sizeof(varname) 是因为当代码中变量类型改变时会自动更新,您或许会用 sizeof(type) 处理不涉及任何变量的代码,比如处理来自外部或内部的数据格式,这时用变量就不合适了。

5.9 auto

auto 绕过烦琐的类型名,只要可读性好就继续用,auto 只能用在局部变量里用。别用在文件作用域变量,命名空间作用域变量和类数据成员里。永远别列表初始化 auto 变量。

5.10 模板编程

不要使用复杂的模板编程。

6 命名约定

命名具有一定随意性,但相比按个人喜好命名,一致性更为重要。因此,我们对代码设计的命名提出约定,目的是让团队的代码尽可能统一,以及让我们在不需要查找类型声明的情况下快速区分命名代表的含义:类型,变量,函数,常量,宏等等,以利于后续阅读和修改。有以下原则:

  • 保持一致性
  • 命名应具有描述性,清晰且具有明确含义

6.1 通用命名规则

函数命名,变量命名,文件命名要有描述性,不要用只有项目开发者能理解的缩写,也不要通过砍掉几个字母来缩写单词;少用缩写。

C++
1
2
3
int price_count_reader;    // 无缩写
int num_errors;            // "num" 是一个常见的写法
int num_dns_connections;   // 人人都知道 "DNS" 是什么
C++
1
2
3
4
5
6
int n;                     // 毫无意义
int nerr;                  // 含糊不清的缩写
int n_comp_conns;          // 含糊不清的缩写
int wgc_connections;       // 只有贵团队知道是什么意思
int pc_reader;             // "pc" 有太多可能的解释了
int cstmr_id;              // 删减了若干字母

注意,一些特定的广为人知的缩写是允许的,例如用 i 表示迭代变量和用 T 表示模板参数。

模板参数的命名应当遵循对应的分类:类型模板参数应当遵循类型命名的规则,而非类型模板应当遵循变量命名的规则。

6.2 文件命名

文件名要尽可能全部小写,可以包含下划线(_) 或连字符(-),依照项目的约定。如果没有约定,那么 “_” 更好。

对于有关特殊物品名称的文件名根据其大小写进行。如对于 GM6020 电机而言,文件名可写为 motor_GM6020.hpp

C++ 文件要以 .cpp 结尾,头文件以 .hpp 结尾。

通常应尽量让文件名更加明确。http_server_logs.hpp 就比 logs.hpp 要好。定义类时文件名一般成对出现,如 foo_bar.hppfoo_bar.cpp,对应于类 FooBar

内联函数定义必须放在 .hpp 文件中。如果内联函数比较短,就直接将实现也放在 .hpp 中。

6.3 类型命名

所有类型命名 —— 类,结构体,类型定义(typedef),枚举,类型模板参数 —— 均使用相同约定,即以大写字母开始,每个单词首字母均大写,后续字母小写(缩写的单词同样遵守改约定,如 OLED 作为类时应命名为 Oled),无特殊情况不包含下划线。例如:

C++
/* 类和结构体 */
class UrlTable
{ ...
}
class UrlTableTester
{ ...
}
struct UrlTableProperties
{ ...
}

/* 类型定义 */
typedef hash_map<UrlTableProperties *, string> PropertiesMap;

/* using 别名 */
using PropertiesMap = hash_map<UrlTableProperties *, string>;

/* 枚举 */
enum UrlTableErrors { ...

对于有关特殊物品名称的类命名根据其大小写进行,适当使用 _ 进行分割。如对于 DM-J4310 电机而言,类名可写为 DM_J4310

6.4 变量命名

变量(包括函数参数)和数据成员名一律小写,单词之间用下划线连接。类的成员变量以下划线结尾,但结构体的就不用,如:a_local_variablea_struct_data_membera_class_data_member_。对于结构体的构造函数,为避免参数与数据重名的问题,允许使用下划线开头,如:_num

C++
/* 普通变量命名 */
string table_name;  // 好 - 用下划线
string tablename;   // 好 - 全小写

string tableName;  // 差 - 混合大小写

void CalcSum(int* num_lst, size_t len)
{
  ...
}

/* 类数据成员 */
class TableInfo
{
  ...
 private:
  string table_name_;  // 好 - 后加下划线
  string tablename_;   // 好
  static Pool<TableInfo>* pool_;  // 好
};

/* 结构体变量 */
struct UrlTableProperties {
  string name;
  int num_entries;
  static Pool<UrlTableProperties>* pool;

  /* 构造函数 */
  UrlTableProperties(string _name, int _num_entries)
      : name(_name), num_entries(_num_entries), pool() {}
};

对于有关数学公式符号有关的变量可根据实际情况进行命名,不严格要求全为小写,如状态空间方程中常用 A 表示状态转移矩阵。

6.5 常量命名

声明为 constexprconst 的变量,或在程序运行期间其值始终保持不变的,命名时以 “k” 开头,大小写混合。例如:

C++
const int kDaysInAWeek = 7;

所有具有静态存储类型的变量都应当以此方式命名。对于其他存储类型的变量,如自动变量等,这条规则是可选的。如果不采用这条规则,就按照一般的变量命名规则。

6.6 函数命名

常规函数使用大小写混合,取值和设值函数则要求与变量名匹配:MyExcitingFunction()MyExcitingMethod()my_exciting_member_variable()set_my_exciting_member_variable()。对于首字母缩写的单词,更倾向于将它们视作一个单词进行首字母大写(例如,写作 StartRpc() 而非 StartRPC())。

对类方法而言,成员函数需将开头改为小写,其余部分与普通函数一致,如:updateTick(),而对于非成员静态函数而言,其命名方式与普通函数一致

对于取值和设值函数,其命名需与变量一直,例如:int count()int get_count()void set_count(int count),当不是简单设置与取值时建议仍以成员函数的命名方式进行,如 setInput(int input)

6.7 命名空间命名

命名空间以小写字母命名。最高级命名空间的名字取决于项目名称。要注意避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突。

顶级命名空间的名称应当是项目名或者是该命名空间中的代码所属的团队的名字。命名空间中的代码,应当存放于和命名空间的名字匹配的文件夹或其子文件夹中。

命名空间中的代码极少需要涉及命名空间的名称,因此没有必要在命名空间中使用缩写。

6.8 枚举命名

枚举的命名应当和常量一直:kEnumName。对于是否带 class 关键字的命名方式采用不同的要求。

  • 对于带 class 关键字的枚举的枚举值则直接采用常量的命名方式。
  • 对于不带 class 关键字的枚举的枚举值则应在 “k” 后补上枚举的类型名,以避免与统一命名空间内不同枚举中相同命名的枚举值冲突。
C++
enum class UrlTableErrors {
  kOK = 0,
  kOutOfMemory,
  kMalformedInput,
};

enum AlternateUrlTableErrors {
  kAlternateUrlTableErrorsOk = 0,
  kAlternateUrlTableErrorsOutOfMemory = 1,
  kAlternateUrlTableErrorsMalformedInput = 2,
};

代码中推荐优先使用带 class 关键字的枚举,只有当使用枚举作为数组索引或是以或形式拼接的状态量(如 Status status = kStatusTxErr | kStatusRxErr)时才使用不带 class 关键字的枚举。

不用宏的方式进行命名原因:当枚举值与宏同名时将产生问题。

6.9 宏命名

不建议使用宏。以往用宏展开性能关键的代码,现在可以用内联函数替代。用宏表示常量可被 const 变量代替,用宏 “缩写” 长变量名可被引用代替

使用宏定义表达式时,应使用完备的括号,如:

C++
#define AREA(a,b) ((a) * (b))

以上每个括号都是必要的。

当使用宏定义多条表达式时,应使用最安全的 do ..。while (0) 写法,如:

C++
1
2
3
4
5
#define FOO(x)         \
  do {                 \
    printf("%s\n",x); \
    do_something(x);   \
  } while (0)

使用宏时,不允许参数发生变化,如不允许:

C++
1
2
3
4
#define SQUARE(a) ((a) * (a))

uint8_t a = 5u;
uint8_t b = SQUARE(a++); /* 不允许 */

尽量不在宏定义中使用可能改变程序流程的语句。

允许使用一些宏技巧,如用 # 进行字符串化、用 ## 连接字符串、使用可变参数宏定义等,如:

C++
1
2
3
#define TO_STR(x) (#x)
#define NEW_XN(n) x##n
#define VA_MACRO(...) printf(__VA_ARGS__)

预定义宏

编译器支持标准预定义宏:

描述
__cplusplus 当翻译单元编译为 C++ 时,定义为整数文本值。 其他情况下则不定义。
__DATE__ 当前日期,一个以 "MMM DD YYYY" 格式表示的字符串常量。
__TIME__ 当前时间,一个以 "HH:MM:SS" 格式表示的字符串常量。
__FILE__ 当前源文件的名称。 __FILE__ 展开为字符型字符串文本。
__LINE__ 当前源文件中的整数行号,一个十进制常量。
__STDC__ 判断当前的编译器是否为标准 C 编译器,若是则返回值 1
__FUNC__ 函数名(非标准)

CMSIS 组件对编译器的特定宏定义封装,常用的有:

__STATIC_INLINE
__STATIC_FORCEINLINE
__WEAK
__PACKED
__PACKED_STRUCT
__PACKED_UNION

6.10 命名规则的特例

如果你命名的实体与已有 C/C++ 实体相似,可参考现有命名策略。

bigopen():函数名,参照 open() 的形式

uinttypedef

bigposstructclass,参照 pos 的形式

sparse_hash_map:STL 型实体;参照 STL 命名约定

LONGLONG_MAX:常量,如同 INT_MAX

7 注释

请使用 中文 进行注释。虽然在编程时使用中文注释需要来回切换语言,但是考虑到队伍中平均英语能力,为此使用中文注释,为的是大家能养成编程的时候做好注释的习惯,以便于后续接手代码的人能尽快读懂代码。

有以下原则:

  • 源码的注释适当即可,代码合理分段并添加注释对代码进行简单描述,完美的代码应尽可能做到不需要注释即可被轻松读懂,这对编码提出了更高的要求
  • 注释不应重复描述代码、翻译代码,而应解释代码难以直接表达的意图,或在个别关键点使用提示性的注释以解释一段复杂的算法是如何工作的

在注释中,应避免使用缩写。完整注释模板请参考附录 B:注释模板,建议使用该模板或类似的格式创建注释。

7.1 头部注释

文件头部应添加说明注释,列出:文件名、文件说明、版本号、修改日期、作者姓名、修改日志、其他注意事项及版权声明等信息。下方给出了一个模板头部注释:

C
/**
 *******************************************************************************
 * @file      :<file>.c/h
 * @brief     :
 * @history   :
 *  Version     Date            Author          Note
 *  V0.9.0      yyyy-mm-dd      <author>        1。<note>
 *******************************************************************************
 * @attention :
 *******************************************************************************
 *  Copyright (c) 2024 Hello World Team,Zhejiang University.
 *  All Rights Reserved.
 *******************************************************************************
 */

7.2 函数注释

重要或复杂内部函数定义、提供外部使用的函数定义和声明的上方应添加函数注释,列出:函数功能、输入参数和可能的参数值、返回值和其他说明和要求。对于独立函数指针和结构体的函数指针成员,也应采用函数注释。下方给出了一个模板函数注释:

C
1
2
3
4
5
6
7
/**
 * @brief       <brief>
 * @param       <param>
 *   @arg       <arg>
 * @retval      <retval>
 * @note        <note>
 */

7.4 变量注释

对于模块内的 static 变量,及不得不使用的外部可见的全局变量,应提供注释说明,包括对其功能含义、取值范围、存取注意事项等的说明。

对于结构体中的每个成员,其含义和用途无法用变量名称完全描述的,需要在其右方添加说明注释。

7.5 语句注释

对语句的注释只能放置在其上方或右方,若放于上方应与其上面的代码用空行隔开。如:

C
1
2
3
4
5
... /* comments */
... // comments

/* comments */
...

7.6 TODO 注释

对于临时的,短期的解决方案,或已经够好但仍不完美的代码,使用 TODO 注释。TODO 注释要使用全大写的字符串 TODO,在随后的圆括号里写上你的名字或其它身份标识,主要目的是便于根据规范的 TODO 格式进行查找。请尽可能记录与这一 TODO 相关的问题或事项。如:

C
// TODO(Hello World):remove the "last visitors" feature

8 格式

规定

每个人都可能有自己的代码格式,但整个项目的代码格式统一是很重要的,只有这样才能让所有人轻松地阅读和理解代码。格式的规定非常冗杂,在这里我们直接参考使用 Google 风格指南 的 “格式” 部分,并做些许改动。改动部分将在 9 自动格式化 列出。

我们可以使用一些工具来自动化地配置代码格式,如 Clang-Format,该工具允许将格式配置为 "Google Style"。 CLionVisual Studio Code 均可使用该工具,具体的说明见 9 自动格式化

在本节,只简明扼要地指出几个自动工具无法涉及的格式规定及一些争议规定。

8.1 缩进

程序块每级缩进为 2 个空格,Tab 对应 2 个空格。请开启编辑器 / IDE 的 Tab 转空格功能。

8.2 行长度

我们对每行代码最大字符数不做限定,但建议保持在 80 以内。

80 行标尺效果

VSCode 中可在全局的 settings.json 中添加如下内容后在编辑框 80 字符的位置便会出现标尺,方便编写者了解代码长度。

Text Only
1
2
3
"editor.rulers": [
    80
],

8.3 字符编码

统一采用 UTF-8 编码。

8.4 空行

相对独立的程序块之间、变量说明之后应添加空行。

8.5 注释

注释符和注释内容之间用一个空格进行分隔。

8.6 条件、循环和开关选择语句

对于条件与循环语句,内部内容需加上大括号,即使只有一条语句

C++
1
2
3
4
5
6
7
if (condition) {  // 圆括号里没有空格.
  ...  // 2 空格缩进.
} else if (...) {  // else 与 if 的右括号同一行.
  ...
} else {
  ...
}

switch 语句中的 case 块可以使用大括号也可以不用,取决于你的个人喜好。如果用的话,要按照下文所述的方法。

如果有不满足 case 条件的枚举值,switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理,编译器将给出 warning)。如果 default 应该永远执行不到,简单的加条 assert

C++
switch (var) {
  case 0: {  // 2 空格缩进
    ...      // 4 空格缩进
    break;
  }
  case 1: {
    ...
    break;
  }
  default: {
    assert(false);
  }
}

空循环体应使用 {},而不是一个简单的分号。

C++
1
2
3
4
5
6
while (condition) {
  // 反复循环直到条件失效。
}
for (int i = 0; i < kSomeNumber; ++i)
{
}  // 可 - 空循环体。
C++
while (condition);  // 差 - 看起来仅仅只是 while/loop 的部分之一。

建议

注释格式

建议文件头部注释、函数注释、全局及常量变量、类型定义的注释格式采用工具可识别的格式,可用于后期由注释直接导出代码说明文档,如 Doxygen 格式。我们给出的注释模板符合该格式。

9 自动格式化

我们在 Google 格式规范的基础上做出以下修改:

  • 花括号前换行的设置设为 Linux 风格,即函数换行,其他情况不换行
  • 不允许在一行中使用简短的 if 语句写法

9.1 VS Code 格式化插件

使用 Visual Studio CodeC/C++ 插件,能够自动格式化代码,使其符合格式规范。安装插件后,建议配置如下:

  • 打开设置(Ctrl + ,),搜索:
Text Only
C_Cpp.clang_format_fallbackStyle
  • 粘贴以下配置:
Text Only
{ BasedOnStyle:Google, BreakBeforeBraces:Linux, UseTab:Never,IndentWidth:2,TabWidth:2,AllowShortIfStatementsOnASingleLine:false,ColumnLimit:0}

image-20221217015517877

使用 Alt + Shift + F 进行格式化,或进行以下设置,在保存时自动格式化:

  • 打开设置(Ctrl + ,),搜索:
Text Only
editor.formatOnSave
  • 进行以下配置:

image-20221217020730346

9.2 VS Code 快速生成注释插件

使用 Visual Studio CodekoroFileHeader 插件,经过配置,能够快速生成文件头部注释和函数注释。安装插件后,建议配置如下:

  • 注释快捷键很可能冲突,请自行设置快捷键。使用 Ctrl K + Ctrl S 唤出键盘快捷方式菜单,搜索快捷键,共有两个:头部注释,命令 ID 为 extension.fileheader;函数注释,命令 ID 为 extension.cursorTip

  • 在全局 settings.json 用户设置文件中进行字段配置,配置模板可参考 附录 C:koroFileHeader 配置

image-20221217021400943

10 C、CPP 混合编程

由于嵌入式开发中设计 HAL 库的使用(C 语言),而战队的开发使用 CPP 语言,因而设计 C 与 CPP 的混合编程。混合编程中只要涉及部分为头文件。

对于 C 语言,其头文件应添加 extern "C" {...} 的声明,其作用为当 CPP 调用该头文件时,大括号内部的内容将以 C 语言标准进行编译。

.h

C
#ifndef C_FILE_H_
#define C_FILE_H_

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

#include ...

...

void Func1(void);

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* C_FILE_H_ */

对于 CPP 语言,要被 C 语言调用的头文件应也需添加 extern "C" {...} 的声明(被 CPP 调用的不需要添加),其作用为当 C 调用该头文件时,大括号内部的内容将以 C 语言标准进行编译,以保证正常编译。但进一步的要求为 CPP 的头文件中最好不要出现头文件包含,同时不能出现任何 CPP 的特性(如类、模板等),建议的方法为额外添加一个用于封装的 .hpp.cpp,其中源文件部分包括所有所需的 .hpp 头文件,然后所有内容封装为 C 形式的函数,然后在 .hpp 中进行该函数的声明,在 C 文件中仅包含该头文件,然后调用声明的函数。

.hpp

C++
#ifndef CPP_FILE_HPP_
#define CPP_FILE_HPP_

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

void Func2(void);

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* CPP_FILE_HPP_ */

附录

A 英文单词缩写参考

单词 缩写
argument arg
average avg
buffer buff / buf
calculate calc
clock clk
command cmd
communicate comm
compare cmp
configuration config / cfg
count cnt
device dev
dictionary dict
driver drv
error err
frequency freq
include inc
initialize init
length len
list lst
management mgmt
manager mgr
maximum max
message msg
middle mid
minimum min
module mod
negative neg
number num
parameter para / param
positive pos
previous prev
protocol prot
register reg
repository repo
semaphore sem
segment seg
sequence seq
source src
stack stk
statistic stat
synchronize sync
temp tmp
tempeature temp
utility util

B 注释模板

文件头部注释

.cpp

C++
/**
*******************************************************************************
* @file      :<file>.c
* @brief     :
* @history   :
*  Version     Date            Author          Note
*  V0.9.0      yyyy-mm-dd      <author>        1. <note>
*******************************************************************************
* @attention :
*******************************************************************************
*  Copyright (c) 2024 Hello World Team,Zhejiang University.
*  All Rights Reserved.
*******************************************************************************
*/
/* Includes ------------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Private constants ---------------------------------------------------------*/
/* Private types -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
/* External variables --------------------------------------------------------*/
/* Private function prototypes -----------------------------------------------*/

.hpp

C++
/**
*******************************************************************************
* @file      :<file>.h
* @brief     :
* @history   :
*  Version     Date            Author          Note
*  V0.9.0      yyyy-mm-dd      <author>        1. <note>
*******************************************************************************
* @attention :
*******************************************************************************
*  Copyright (c) 2024 Hello World Team,Zhejiang University.
*  All Rights Reserved.
*******************************************************************************
*/
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef PROJECT_PATH_FILE_HPP_
#define PROJECT_PATH_FILE_HPP_

/* Includes ------------------------------------------------------------------*/
/* Exported macro ------------------------------------------------------------*/
/* Exported constants --------------------------------------------------------*/
/* Exported types ------------------------------------------------------------*/
/* Exported variables --------------------------------------------------------*/
/* Exported function prototypes ----------------------------------------------*/

#endif /* PROJECT_PATH_FILE_HPP_ */

函数注释

C++
1
2
3
4
5
6
7
/**
* @brief       <brief>
* @param       <param>
*   @arg       <arg>
* @retval      <retval>
* @note        <note>
*/

C/CPP koroFileHeader 配置

JSON
// koloFileHeader -------------------------------------------------
// 头部注释
"fileheader.customMade"{
  "custom_string_obkoro9""******************************************************************************",
  "FilePath""only file name",
  "Description""",
  "custom_string_obkoro1"" @history   :",
  "custom_string_obkoro2""  Version     Date            Author          Note",
  "custom_string_obkoro3""  V0.9.0      yyyy-mm-dd      <author>        1。<note>",
  "custom_string_obkoro10""******************************************************************************",
  "custom_string_obkoro11"" @attention :",
  "custom_string_obkoro4""******************************************************************************",
  "custom_string_obkoro1_copyright""  Copyright (c) ${now_year} Hello World Team,Zhejiang University.",
  "custom_string_obkoro5""  All Rights Reserved.",
  "custom_string_obkoro7""******************************************************************************"
},
// 函数注释
"fileheader.cursorMode"{
  "Description""",
  "param"""// param 开启函数参数自动提取 需要将光标放在函数行或者函数上方的空白行
  "   @arg"" None",
  "return""",
  " @note"" None"
},
"fileheader.configObj"{
  "prohibitAutoAdd"["md""json"],
  "dateFormat""YYYY-MM-DD",
  "autoAdd"false// 默认开启
  "autoAddLine"1// 默认文件超过100行就不再自动添加头部注释
  "createHeader"false// 默认关闭
  "language"{
    // 一次匹配多种文件后缀文件 不用重复设置
    "h/c/hpp/cpp"{
      "head""/**"// 统一增加几个*号
      "middle"" *",
      "end"" */"
    }
  },

  "wideSame"true// 头部注释等宽设置
  "wideNum"12// 头部注释字段长度
  "functionWideNum"13// 0 默认关闭 设置一个正整数即可开启 比如12
  "specialOptions"{
    "FilePath"" @file  ",
    "Description"" @brief   ",
    "param"" @param",
    "return"" @retval"
  },

  "afterAnnotation"{
    "c""/* Includes ------------------------------------------------------------------*/\n/* Private macro -------------------------------------------------------------*/\n/* Private constants ---------------------------------------------------------*/\n/* Private types -------------------------------------------------------------*/\n/* Private variables ---------------------------------------------------------*/\n/* External variables --------------------------------------------------------*/\n/* Private function prototypes -----------------------------------------------*/\n",
    "cpp""/* Includes ------------------------------------------------------------------*/\n/* Private macro -------------------------------------------------------------*/\n/* Private constants ---------------------------------------------------------*/\n/* Private types -------------------------------------------------------------*/\n/* Private variables ---------------------------------------------------------*/\n/* External variables --------------------------------------------------------*/\n/* Private function prototypes -----------------------------------------------*/\n",
    "h""/* Define to prevent recursive inclusion -------------------------------------*/\n#ifndef PROJECT_PATH_FILE_HPP_\n#define PROJECT_PATH_FILE_HPP_\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n/* Includes ------------------------------------------------------------------*/\n/* Exported macro ------------------------------------------------------------*/\n/* Exported constants --------------------------------------------------------*/\n/* Exported types ------------------------------------------------------------*/\n/* Exported variables --------------------------------------------------------*/\n/* Exported function prototypes ----------------------------------------------*/\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif /* PROJECT_PATH_FILE_HPP_ */",
    "hpp""/* Define to prevent recursive inclusion -------------------------------------*/\n#ifndef PROJECT_PATH_FILE_HPP_\n#define PROJECT_PATH_FILE_HPP_\n/* Includes ------------------------------------------------------------------*/\n/* Exported macro ------------------------------------------------------------*/\n/* Exported types ------------------------------------------------------------*/\n#endif /* PROJECT_PATH_FILE_HPP_ */"
  }// 头部注释之后插入内容

  "atSymbol"["@""@"]// 所有文件的头部注释和函数注释的默认值
  "atSymbolObj"{},
  "colon"[":"""]// 所有文件的头部注释和函数注释的默认值
  "colonObj"{},
  "openFunctionParamsCheck"true// 默认开启
  "typeParamOrder""param",
  "functionParamsShape"[""""]// [] or {}
  "functionParamAddStr"":"// 默认不增加字符串
  "functionTypeSymbol""None" // 参数没有类型时的默认值
},

D 参考资料

[1] Google 开源项目风格指南(C++ 部分)

E 版本说明

版本号 发布日期 说明 贡献者
2022.12.17 首次发布 薛东来
2024.07.10 修改为 CPP 版本 蔡坤镇