Skip to content

Meta Embedded C++ Style Guide

liuzikai edited this page Jan 17, 2022 · 1 revision

概述

本文档是 Meta-Embedded 工程 C++ 代码规范,推行统一的代码规范的目的有:

  • 减少因不良代码风格而带来的错误和 Bug
  • 增加代码可读性与可维护性
  • 减少协作成本、重构成本、测试成本
  • 满足控制组长的强迫症

划重点

  • 全英文,尽可能使用准确、易懂的词汇,无拼音或意义不明的缩写
  • 文件名:全小写 + 下划线(_),如 remote_interpreter.cpp
  • 变量、方法、函数:全小写 + 下划线,如 void start_receive(int receive_mode)
  • 类型:全小写 + 下划线 + _t ,如 rc_status_t
  • 常量、宏:全大写 + 下划线,如 REMOTE_RC_S_UP
  • 类:首字母大写的驼峰拼写法,如 class GimbalInterface {}
  • 代码后同一行注释:两个空格 + // + 一个空格 + 内容 ,首字母小写,如
uint16_t last_angle_raw;  // the raw angle of the newest feedback, in [0, 8191]
  • 独立成行注释:// + 一个空格 + 内容 的格式。首字母大写,如
// Fill the header
txmsg.IDE = CAN_IDE_STD;
  • 缩写单词,首字母大写则全大写,首字母小写则全小写。
  • Git commit:认真写,英文,使动语态
Bad: I fix a bug in GimbalController.
Good: Fix a bug in GimbalController that protection fails when both motors are disabled.

目录

  • 1 命名规范
    • 1.1 文件命名
    • 1.2 类命名
    • 1.3 类型命名
    • 1.4 变量、函数、方法命名
    • 1.5 常量、枚举命名
  • 2 注释与日志规范
    • 2.1 CLion 中的三种注释格式
      • 2.1.1 普通注释
      • 2.1.2 单行高亮注释
      • 2.1.3 多行高亮注释
      • 2.1.4 TODO 与 FIXME 注释
    • 2.2 文件注释
    • 2.3 类、结构体、枚举注释
    • 2.4 方法/函数注释
    • 2.5 变量、常量、宏注释
    • 2.6 实现注释
    • 2.7 TODO 与 FIXME 注释
    • 2.8 Git 日志
  • 3 头文件
    • 3.1 ifdef 保护
    • 3.2 定义书写
    • 3.3 注释书写
  • 4 模块化、封装与访问控制
    • 4.1 模块化
    • 4.2 封装
      • 4.2.1 结构体、枚举、常量封装
      • 4.2.2 实例变量/方法 vs 类变量/函数
      • 4.2.3 宏的使用
    • 4.3 访问控制

1 命名规范

命名总体原则是清晰、易读。

  • 不可使用无意义命名,例如 xyz ,但一些局部变量可以使用常见命名,例如 for 循环使用的 i
  • 不可使用拼音
  • 不可使用意义不明的缩写,但常见缩写可以使用,例如 configconf 用于表示 configuration。如需自定义缩写,务必在显眼处书写注释

1.1 文件命名

代码文件应写在 .cpp 文件中,一般而言,每个 cpp 文件应有一个同名 .h 头文件。

文件名应全部小写,使用下划线(_)作为分隔,例如:

remote_interpreter.cpp

1.2 类命名

每个单词首字母大写(大驼峰拼写法),缩写单词则全部大写,例如:

class GimbalInterface {}

1.3 类型命名

struct、enum 应以 小写字母 + 下划线(_)分隔 + “_t“ 结尾 命名,减少 typedef 的使用。例如:

enum rc_status_t {
    REMOTE_RC_S_UP = 1,
    REMOTE_RC_S_DOWN = 2,
    REMOTE_RC_S_MIDDLE = 3
};

struct rc_t {
    float ch0; // normalized: -1.0(leftmost) - 1.0(rightmost)
    float ch1; // normalized: -1.0(downmost) - 1.0(upmost)
    float ch2; // normalized: -1.0(leftmost) - 1.0(rightmost)
    float ch3; // normalized: -1.0(downmost) - 1.0(upmost)
    rc_status_t s1;
    rc_status_t s2;
} ;

1.4 变量、函数、方法命名

变量、函数、方法使用 小写字母 + 下划线(_)分隔,例如:

int feedback_raw_angle;
void start_receive();

ChibiOS API 使用驼峰命名法(第一个单词小写,之后单词首字母大写),这种命名方法也可以接受,但尽可能与现有代码保持统一。

int feedbackRawAngle;
void startReceive();

内部使用的变量、函数、方法,尽可能使用访问控制。对于暴露给外部的变量、函数、方法,或临时变量,可以使用以下划线结尾(或开头)的命名方法,标示内部使用,例如:

int key_code_;  // hold key code data, only for internal use
int _key_code;  // hold key code data, only for internal use

模块初始化代码可写在类的构造函数中,但需要 HAL 或 ChibiOS 初始化后执行的代码则不可,一般这一部分代码会放置在命名为 startbegin 的函数/方法中。

1.5 常量、枚举命名

常量、枚举使用 全大写 + 下划线(_)分隔

static constexpr unsigned int SKD_THREAD_INTERVAL = 1;  // PID calculation interval [ms]

class Remote {
public:
    enum rc_status_t {
        REMOTE_RC_S_UP = 1,
        REMOTE_RC_S_DOWN = 2,
        REMOTE_RC_S_MIDDLE = 3
    };
}

常量、枚举显示在所需要的 Scope 中。如需暴露在较大的 Scope 中,需增加特有前缀,例如 GIMBAL_

使用 true, false, nullptr,不要使用 TRUE, FALSE, NULL


2 注释与日志规范

注释很重要!注释很重要!注释很重要!写注释的目的是为了便于下一个读代码的人(很有可能是你自己)理解,因此,写注释应从读者角度出发,以便于读者理解为准则。

注释使用 英文 书写。

2.1 CLion 中的三种注释格式

在 CLion 中,对以下三种注释有独特的代码高亮。

2.1.1 普通注释

包含 ///**/ 注释,在 CLion 中以灰色显示。

IMAGE

代码后同一行的注释,使用 两个空格 + // + 一个空格 + 注释内容 的格式,首字母不大写

uint16_t last_angle_raw;  // the raw angle of the newest feedback, in [0, 8191]

独立成行的注释,使用 // + 一个空格 + 注释内容 的格式。注释内容句首字母大写:

// Fill the header
txmsg.IDE = CAN_IDE_STD;
txmsg.SID = 0x1FF;
txmsg.RTR = CAN_RTR_DATA;
txmsg.DLC = 0x08;

2.1.2 单行高亮注释

包括:

  • /** 开头,*/ 结尾的注释
  • /// 开头的但行注释

多用于划分区块。

2.1.3 多行高亮注释

/** 开头,*/ 结尾的注释在 CLion 中会以绿色高亮显示。

IMAGE

多行高亮注释可包含以 @ 开头的关键字,CLion 的自动补全会提供常见的关键字,并将关键字加粗加下划线。

IMAGE

常用的有:

  • @brief:概述、简介
  • @note:特别说明
  • @params:方法/函数参数,见 3.3 方法/函数注释。
  • @return:方法/函数返回值,见 3.3 方法/函数注释。

2.1.4 TODO 与 FIXME 注释

IMAGE

这两种注释会以亮绿色显示,且 CLion 提供了快速查看这两种注释的功能。

IMAGE


2.2 文件注释

每个文件开头应包含作者、文件创建时间、修改记录等信息。如文件受版权保护,则也应包含版权公告。可使用 CLion 自动生成的文件注释。

2.3 类、结构体、枚举注释

类、结构体、枚举前应有注释,描述其用途与注意事项,除非其用途非常显而易见。使用 多行高亮注释

IMAGE

2.4 方法/函数注释

每一个方法/函数应有对应的注释,用于描述函数的用途、各个参数、返回值、注意事项等。使用 多行高亮注释。值得一体的是,将注释在 .h 和 .cpp 文件中复制并没有太大意义。一般而言,建议将最完善的注释放在头文件中。

IMAGE

在 CLion 中,在函数前输入 /**,按一下回车,CLion 会自动生成参数与返回值的关键字。

2.5 变量、常量、宏注释

各个变量、常量、宏应当有对应的注释,用以描述其用途,除非其非常明显。对于一些带单位的变量应当写明单位。根据注释长短使用 普通注释多行高亮注释

IMAGE

2.6 实现注释

在实现代码中合适位置写下注释,用于概括代码作用、记录巧妙的实现、注意事项等。但注释内容应避免与代码内容重复,反面例子:

if (failure) return false;  // return false if failed

2.7 TODO 与 FIXME 注释

顾名思义,TODO 注释用于记录之后需要增加、改进、修改的事项,FIXME 注释用于记录需要修复的事项。这两种注释不应长久保留在代码中,应及时处理或移除。

2.8 Git 日志

Git commit 时需要详细书写日志,明确说明本次 commit 做的更改,这能帮助其他成员快速了解 commit 的内容,如果之后需要版本回退,有完整的日志也能帮助快速定位回退位置。

提交日志包含以下内容:

  • 增加的模块、功能
  • 修复的问题
  • 目前代码还存在的问题,下一步计划
  • ...

日志使用使动语态书写:

I fix a bug in GimbalController. // Bad
Fix a bug in GimbalController that protection fails when both motors are disabled. // Good

3 头文件

3.1 ifdef 保护

每个头文件都应有 ifdef 保护,防止一个头文件被包含多次。宏命名规则为 工程_文件名_后缀名,应全部大写,或使用 CLion 新建头文件时自带的 ifdef 保护。例如:

#ifndef META_EMBEDDED_CAN_INTERFACE_H
#define META_EMBEDDED_CAN_INTERFACE_H

// Header content

#endif // META_EMBEDDED_CAN_INTERFACE_H

3.2 定义书写

各种定义应当写在头文件中,而函数声明、函数或方法定义则应当写在对应的 cpp 文件中。但在类的定义中,较短的函数或方法代码亦可直接写在类定义中。

3.3 注释书写

头文件是明确模块接口的地方,因此头文件中应当详细书写接口注释、模块注释等。注释书写规范参考 注释与日志规范 一节。


4 模块化、封装与访问控制

C++ 提供了类、命名空间等语言特性用于封装,访问控制也非常灵活高效,在开发过程中应当注意代码的模块化与封装。

4.1 模块化

模块化的主要目的有:复用代码、便于测试、便于协作。封装主要通过类来实现。

例如使用经过封装的 GimbalInterface 模块和 GimbalController 模块,主线程代码只需要这样写:

// Use Remote::rc.ch0 * rc_yaw_angle_angle as target angle
// Input target angle and actual angle to get target velocity
yaw_target_velocity = GimbalController::yaw.angle_to_v(GimbalInterface::yaw.actual_angle, 
                                                       Remote::rc.ch0 * rc_yaw_angle_angle);

// Input target velocity and actual velocity to get target current
GimbalInterface::yaw.target_current = (int) GimbalController::yaw.v_to_i(GimbalInterface::yaw.angular_velocity, 
                                                                         yaw_target_velocity);
// Send instructions to motors
GimbalInterface::send_gimbal_currents();

主线程只需要控制各个模块间的数据流动、调用函数即可,如果需要更改控制逻辑,也只需要修改主线程的代码。

以上是模块化的基本思路,具体实现可能随着程序架构的发展而改变。

4.2 封装

各模块的独立,主线程代码的简洁、测试的简便均有赖于模块的封装。简而言之,封装即是将内部使用的变量、代码等隐藏,对外只提供必须的接口。


Q:是不是只要封装做得好,内部的代码就可以不遵守规范为所欲为了?(滑稽) A:(严肃脸)不,你还是会被打

IMAGE

4.2.1 结构体、枚举、常量封装

一个类特有的结构体、枚举、常量应置于类的命名空间下 。正确示例:

class MotorController {
public:

    enum motor_id_t {
        YAW_ID,
        PITCH_ID
    };
    
    motor_id_t id;
};

错误:

enum motor_id_t {
    YAW_ID,
    PITCH_ID
};

class MotorController {
public:
    motor_id_t id;
};

4.2.2 实例变量/方法 vs 类变量/函数

当一个类需要产生多个实例,且每个实例需要各自独立的变量和方法时,使用实例变量和方法,例如 PIDController,在底盘各个电机、云台电机上均需要独立的 PID 控制器。

当整个程序只需要一个实例时,使用类变量和类函数。例如 Shell,由于整个程序只需要一个 Shell,该类中均为类变量和类函数。在其他代码中可以直接通过命名空间调用而无需定义实例:

Shell::start(HIGHPRIO);
Shell::addCommands(gimbalCotrollerCommands);  // Shell 由 ChibiOS 提供代码修改而来,因而沿用了 ChibiOS 的命名,将来应统一化

yaw_target_angle = Shell::atof(argv[0]);

4.2.3 宏的使用

由于 #define 不受命名空间限制,如非必须,使用命名空间内的常量代替宏,例如:

#define REMOTE_RX_BUF_SIZE 18

class Remote {
    // Other things
};

可修改为:

class Remote {

private:
    static const int rx_buf_size = 18;
    
    // Other things
};

4.3 访问控制

类的访问控制根据最小需要授权,可以使用友元(friend)给特定的类、函数授权。

在类定义中,将相同授权的成员放在一起,即不要出现多于一个的 publicprotectedprivate

class ClassName {
public:
    // Something
protected:
    // Something
private:
    // Something
}

更新

  • 2019.1.12 首次发布 --liuzikai
  • 2019.6.28 增加单行高亮注释命名,修改常量命名、typedef 使用说明等 --liuzikai
  • 2019.11.19 增加 highlight section --liuzikai
  • 2022.01.17 小幅修改 --liuzikai

参考资料

Clone this wiki locally