前言
代码首先是给人看的,其次才是给机器执行的,因此一般情况下代码的可读性优先于性能,只有确定性能是瓶颈时,才需要主动优化。
可读性高的代码应当是易于理解并且易于实现的,代码越长越难看懂,可能出错的地方就越多,可靠性也越低。这就要求开发团队有一套统一的编程规范,根据清晰、简洁、风格统一的原则,来实现可靠性高,易于维护和重构的代码,对于C语言这种灵活度极高的语言来说更为重要。
目录
一、标识符命名
现行的标识符命名规则有很多,典型的如:驼峰命名、匈牙利命名,各种风格孰优孰劣,业内也一直争论不休,归根到底这其实并不是一个客观的技术问题,更多的是和每个团队的习惯沿袭有关。
标识符命名同样要遵循清晰、简洁、风格统一的大原则,考虑到嵌入式产品主要基于Linux系统开发,为了保持代码风格的统一,我们选择使用小写字母加下划线作为标识符命名的UNIX风格。所有自研的C语言代码都应遵守本规范制定的规则,但在改动第三方代码时,除非整体改造,原则上应延续第三方原有的代码风格。
1.1 通用规则
1.1.1 标识符的命名要清晰、简明、有意义,可以使用完整的英语单词或是缩写,禁止使用单个字符和汉语拼音,避免让人产生误解。
使用单词缩写时应使用公认的缩写,常用的单词缩写可参考附录表格。如果代码中使用了特殊缩写,则需要在定义处给出必要的注释说明。
一个项目内专有名词应当统一,避免使用同意不同名的标识。
只允许定义i 、j、k等单个字符作为局部循环变量,其他情况则禁止使用。
uint32_t num; /* ok */
uint32_t error_value; /* good */
uint32_t aaa; /* bad */
uint32_t shu_zhi; /* bad */
1.1.2 尽量避免标识符中出现数字编号,除非逻辑上编号有实际意义。
uint8_t timer0; /* ok */
uint8_t flag0; /* bad */
1.1.4 除头文件或编译开关等特殊标识符,不使用下划线“_”作为标识符的开头或结尾。
一般来说,以下划线开头或结尾的宏都是内部的定义,为避免冲突应使用单词作为标识符开头。
1.1.3 标识符的命名长度应当小于32个字符,以下划线“_”为间隔总字段不超过5段。
ISO C 标准要求内部标识符前31个字符必须是不同的,以保证移植性。过长的标识符可读性也较差,通常用5个字段足以准确描述变量或函数的意义。
1.2 文件命名
1.2.1 目录和文件使用小写字母和下划线命名,尽量不使用数字。
为了避免文件重名,尽量不要使用简单的高频词汇,可以使用模块+功能来命名文件。
1.2.2 模块对外接口的头文件统一使用“模块名_api.h”的格式命名。
一个功能模块可以包含若干源文件,允许使用一个公共的头文件作为接口文件。
1.3 宏和常量命名
1.3.1 宏和常量使用大写字母和下划线命名。
为了避免文件内部常量和外部变量重名,可以使用模块名作为常量的前缀。
#define GPIO_TEST_SWITCH 1 /* ok */
const static uint8_t GPIO_MAX_NUM = 16; /* ok */
1.3.2 枚举值也是常量,同样使用大写字母和下划线命名。
/* ok */
typedef enum {
COLOUR_RED;
COLOUR_BLUE;
COLOUR_GREEN;
} colour_t;
1.4 结构体和枚举命名
1.4.1 结构体使用小写字母和下划线命名,统一使用typedef重命名,并以“_t”作为结构体名称的尾缀。
有一些规范要求结构体以“_s”作为结尾,枚举以“_e”作为结尾,用以区分彼此。但在实际操作中,结构体赋值往往需要跟踪到定义才知道具体的成员(现在的编辑器也会有快捷显示),而枚举一般很少使用枚举名,通常是直接使用枚举值,因而区分的收益不是很大。
1.4.2 结构体成员也使用小写字母和下划线命名。
成员名称应当尽量简短,明确结构体某一属性即可,不要重复结构体名称已经表明的信息,也不要出现明显和结构体意义不相关的成员名。
1.4.3 枚举命名和结构体命名在格式上保持一致。
/* standard format, ok */
typedef struct tree_node {
uint32_t data;
struct tree_node *left;
struct tree_node *right;
} binary_tree_t;
/* general format, ok */
typedef struct {
uint8_t age;
uint8_t gender;
uint8_t weight;
uint8_t coat_color;
} cat_t;
/* bad */
typedef struct {
uint8_t cat_age;
uint8_t cat_gender;
uint16_t cat_weight;
uint16_t dog_coat_color;
} cat_t;
1.5 变量命名
1.5.1 变量使用小写字母和下划线命名,全局变量以“g_”作为前缀,静态变量以“s_”作为前缀,局部变量不加前缀。
不允许局部变量和全局变量重名,因此通过前缀加以区分,尽管局部变量和全局变量的作用域不同不会发生语法错误,但容易使人误解。同理,在函数内部不要定义同名的局部变量。
全局变量命名应该尽量详细,需要包含具体的模块名;局部变量命名则应该尽该量简约,说明用途或含义即可。
uint8_t g_timer_mode = 0; /* global variable, ok */
uint8_t s_flag = 0; /* static variable, ok */
uint8_t count = 0; /* local variable, ok */
1.6 函数命名
1.6.1 函数使用小写字母和下划线命名,函数的名称应该体现函数的功能或动作。
常用操作的反义词参见附录,可作为命名参考。
1.6.2 函数指针以“_func”作为名称尾缀,对外接口函数则以“模块名_”作为名称前缀。
int32_t drive_flash_init(void); /* ok */
void (*timer_timeout_func)(void); /* ok */
二、文件
文件设计是程序设计的重要一环,同样应当遵循“高内聚,低耦合”的设计原则。
2.1 文件结构
2.1.1 文件职责应当单一,单个文件最大不得超过2000行。
2.1.2 每一个 .c 文件都应有一个同名 .h 文件,用于声明需要对外公开的接口。
如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。
2.1.3 一个模块可以包含多个 .c 文件,为方便外部使用,建议每一个模块提供一个 .h 。
需要注意的是,这个.h并不是简单的包含所有内部的.h,它是为了模块使用者的方便,对外提供的模块整体接口。
2.1.4 引用库文件通过<.h>方式引用,其他文件通过“.h”引用。引用顺序:C库(C++库)、第三方库、项目内头文件, 同一类型的头文件采用字母顺序排列。禁止包含用不到的头文件。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "activation.h"
2.1.5 只能通过包含头文件的方式使用其他 .c 提供的接口,禁止通过 extern 的方式使用外部函数接口或变量。
直接使用extern引用外部函数容易在外部函数改变时声明和定义变得不一致,同时也会使得代码整体架构变得离散不易维护。
extern uint8_t g_timer_mode; /* bad */
extern int32_t drive_flash_init(void); /* bad */
2.2 头文件
2.2.1 头文件只能包含宏、枚举、结构体和函数的声明,且应该声明在唯一的头文件中,禁止在头文件中定义变量或实现函数。
头文件应该用于声明对象、函数、typedef 和宏,而不应该包含或生成占据存储空间的对象或函数(或它们的片断)的定义。这样就清洗划分了 .c 和 .h 文件的用途,即只有 .c 文件才包含可执行的源代码,而头文件只能包含声明。
2.2.2 仅在内部使用的宏、枚举、结构体和函数,不应放在头文件,而应该放在 .c 文件中。
2.2.3 所有头文件都应采用#define宏防止重复包含,命名格式为“文件名_H”,为保证唯一性推荐使用“路径名_文件名_H”。
当头文件第一次被包含时会定义这个宏,在头文件被再次包含时将不再展开文件内容。
#ifndef FTRMWARE_DRIVE_FLASH_H
#define FTRMWARE_DRIVE_FLASH_H
/* user code */
#endif /* FTRMWARE_DRIVE_FLASH_H */
2.2.4 禁止头文件循环依赖。
头文件循环依赖是指a.h包含b.h,b.h包含c.h,c.h包含a.h,导致修改任何一个头文件,都会使所有包含了a.h/b.h/c.h的代码全部重新编译一遍。而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。
三、变量
3.1 数据类型
3.1.1 使用指示大小和符号的typedef 数据类型代替基本数据类型定义变量。
嵌入式系统或单片机型号驳杂,直接使用基本数据类型char、int、short、long、float 和doulb,无法保证代码的移植性。使用typedef重新定义基本数据类型,在切换平台时仅需调整typedef定义即可。
/* 32bit system */
typedef signed char int8_t;
typedef signed short int16_t;
typedef signed int int32_t;
typedef signed long int64_t;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long uint64_t;
typedef float float32_t;
typedef double float64_t;
int32_t value; /* good */
int number; /* bad */
3.1.2 使用char 类型定义字符或字符串,int8_t 或uint8_t 类型只能用于定义数值或数组。
C语言中没有string类型,因此使用char定义string类型的数据。而且不论任何硬件平台,char类型都是一个字节,不存在长度兼容问题。
char str[] = "Hello World"; /* ok */
int8_t arr[5] = {1, 2, 3, 4, 5}; /* ok */
int8_t url[] = "www.xxxx.com"; /* bad */
3.1.3 尽量减少没有必要的数据类型默认或强制转换。
进行数据类型强制转换时,数据的意义、取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。
3.2 全局和局部变量
3.2.1 变量的使用要做到“名副其实”,一个变量只能有一个功能,不能把一个变量用作多种用途。
一个变量只能用来表示一个特定意义,当实际数据的意义和变量代表的意义不同时,应该定义一个新变量来承载数据,而不是为图方便依旧使用这个变量。除了难以理解外,还可能会引发逻辑上的错误。
uint32_t calculate_area(uint16_t length, uint16_t width)
{
if ((0 == length) || (0 == width)) {
return 0;
}
length *= width; /* bad */
return length;
}
uint32_t calculate_area(uint16_t length, uint16_t width)
{
if ((0 == length) || (0 == width)) {
return 0;
}
uint32_t area = length * width; /* good */
return area;
}
3.2.3 尽量不用或者少用全局变量,且全局变量不能作为对外的接口使用。
全局变量可以认为是模块的私有数据,在单个文件内定义全局变量时使用static修饰,以防止外部文件的非正常访问。
确实需要外部操作全局变量时,应提供接口函数来设置、获取,同时要注意全局数据的访问互斥。
3.2.4 所有变量在使用前都必须初始化,严禁使用未经初始化的变量作为右值。
根据ISO C 标准,static修饰的变量缺省地被自动赋予零值,除非经过了显式的初始化。实际中,一些嵌入式环境没有实现这样的缺省行为,因此不论全局变量或局部变量都应初始化后再使用。
#define DATA_BUF_SIZE 10
static uint8_t g_data_buf[DATA_BUF_SIZE] = { 0 }; /* good */
uint8_t get_data_num(void)
{
uint8_t num = 0; /* good */
uint8_t index; /* bad */
while (index < DATA_BUF_SIZE) {
if (g_data_buf[index] > 0) {
num++;
}
index++;
}
return num;
}
void set_data_buf(uint8_t index, uint8_t data)
{
if (index >= DATA_BUF_SIZE) {
return;
}
uint8_t temp = 0; /* not good */
temp = g_data_buf[index]; /* uint8_t temp = g_data_buf[index]; ok */
if (data > temp) {
temp = data - temp;
}
g_data_buf[index] = temp;
return;
}
3.2.5 明确全局变量的初始化顺序,尽量避免跨模块的初始化依赖,确实有依赖时要保证正确的时序关系。
例如系统初上电时,业务模块的全局变量依赖文件模块从flash读取的数据进行初始化,则在使用业务模块全局变量前必须保证文件模块读取完成。
3.2.6 应使用大括号以指示和匹配数组及结构体的非0初始化;初始化为0或NULL时,只用一层大括号即可。
ISO C 要求数组、结构体和共用体的初始化列表要以一对大括号括起来(尽管不这样做的行为是未定义的)。本规则更进一步地要求,使用附加的大括号来指示嵌套的结构,迫使程序员显式地考虑和描述复杂数据类型元素的初始化次序。
int16_t arr1[2][2] = { 1, 2, 3, 4 }; /* bad */
int16_t arr2[2][2] = { { 1, 2 }, { 3, 4 } }; /* good */
int16_t arr3[2][2] = {0}; /* ok */
3.2.7 结构功能单一,不要设计面面俱到的数据结构。
结构的定义应该可以明确的描述一个对象,设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。
3.2.8 通信过程中使用的结构,必须注意字节序。
处理器大小端、位数不同,数据结构的字节序差别会很大,所以跨平台数据交互前,都应进行必要的字节序转换以符合通信要求。
四、宏和常量
4.1 宏定义
4.1.1 应尽可能使用函数代替宏,减少用宏定义表达式。
宏对比函数,有一些明显的缺点:
- 宏缺乏类型检查,不如函数调用检查严格
- 宏形式的代码难以打断点调试,不利于定位问题
- 宏如果调用的很多,会造成代码空间的浪费,不如函数空间效率高
- 宏展开后可能会产生意想不到的副作用
#define SQUARE(a) (a) * (a)
SQUARE(i++); /* 展开后 (i++) * (i++) i被加两次*/
uint32_t square(uint16_t a)
{
uint32_t result = a * a;
return result;
}
square(i++); /* i只加一次*/
4.1.2 确实需要用宏定义表达式时,要使用完备的括号,并且不允许参数发生变化。
#define SUM(a, b) a + b /* bad */
#define SUM(a, b) (a + b) /* bad */
#define SUM(a, b) (a) + (b) /* bad */
#define SUM(a, b) ((a) + (b)) /* ok */
4.1.3 宏定义有多条语句时要放在大括号中,最多不超过10行,不允许使用 return、goto、continue、break等改变程序流程的语句。
#define CHECK_PARA_VALID(para) {if (para == NULL) {return ret;}} /* bad */
4.1.3 宏定义末尾不要跟分号“;”。
分号不是宏定义的必要组成部分,如果宏定义末尾跟了分号,调用宏时不写分号不符合一般C语句的书写习惯,写分号则重复,更要命的是有些情况下还会导致语法错误。
#define SUM(a, b) ((a) + (b)); /* bad */
uint8_t v1 = 1;
uint8_t v2 = 2;
uint8_t v3 = 3;
uint8_t v4 = 4;
uint8_t result = SUM(v1, v2) + SUM(v3, v4); /* error ((1) + (2)); + ((3) + (4)); */
4.1.4 不允许使用#undef。
使用#undef会使宏的存在或含义产生混乱。
4.2 常量
4.2.1 常量使用十进制或十六进制表示,禁止使用八进制常量。
#define BUF_MAX_SIZE 100 /* ok */
static uint8_t g_buf_len = 0; /* ok */
static uint8_t g_buf[BUF_MAX_SIZE]= { 0x00 }; /* ok */
static uint8_t g_buf_index = 01; /* bad */
4.2.2 不允许直接使用魔鬼数字。
uint8_t month = 12; /* December or 12 months ? */
所谓魔鬼数字是指程序中出现的难以理解的数字。魔鬼数字除了使代码难以理解外,如果这个数字在程序中多处使用,很难统一修改。因此:
- 多处使用的数字,必须定义const全局变量或宏,变量名或宏命名应当自注释数字的意义
- 集中在局部使用的数字,可以在代码周围增加说明注释,也可以定义局部const变量,变量命名自注释
- 0作为一个特殊的数字,作为一般默认值使用没有歧义时不用特别定义
4.2.3 在枚举列表中,“=”不能显式用于除首元素之外的元素上,除非所有的元素都是显式初始化的。
/* ok */
typedef enum {
MONDAY = 1,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
} week_t;
/* bad */
typedef enum {
MONDAY,
TUESDAY = 2,
WEDNESDAY = 3,
THURSDAY,
FRIDAY,
} week_t;
五、函数
函数设计要求代码简单直接,用合适的数据结构和直截了当的控制语句将函数有机的组织起来。
5.1 函数体
5.1.1 函数功能要单一,即一个函数只实现一个功能。
一个函数实现多个功能给开发、使用、维护都带来很大的困难。将没有关联或者关联很弱的语句放到同一函数中,会导致函数职责不明确,难以理解,难以测试和改动。
5.1.2 重复代码应该尽可能提炼成函数。
重复代码提炼成函数可以降低维护成本和资源消耗,并提高代码的复用性。
5.1.3 为了函数的可读性和维护性,应避免函数过长,有效代码行数一般不超过 50 行,对于逻辑联系特别紧密或层次清晰的可放宽至100行。
过长的函数往往意味着函数功能不单一,应该考虑重新设计函数结构,使功能内聚行数减少。
有效代码行指非空非注释的代码行。
限制函数行数目的在于提升代码的可读性,但有些场景下代码内在逻辑联系特别紧密或层次简单,拆开反而影响理解,因而可以放宽行数限制,不要“为拆而拆”。
5.1.4 为避免函数的代码块嵌套过深,函数的代码块嵌套不超过4层。
函数的代码块嵌套深度指的是函数中的代码控制块(例如:if、for、while、switch等)之间互相包含的深度。每级嵌套都会增加阅读代码时的脑力消耗,因为需要在脑子里维护一个“栈”。应当优先使用卫语句,在不满足条件时直接退出函数实现“短路”操作,以减少代码的嵌套层级。
/* bad */
void usart1_irq_handler(void)
{
uint8_t res;
if (USART1->SR) {
res = USART1->DR;
if ((USART_RX_STA & 0x8000) == 0) {
if ((USART_RX_STA & 0x4000) == 0) {
if (res == 0x0d) {
USART_RX_STA |= 0x4000;
} else {
if (USART_RX_STA > (USART_REC_LEN - 1)) {
USART_RX_STA = 0; /* 5层嵌套 */
}
}
}
}
}
}
/* ok */
void usart2_irq_handler(void)
{
uint8_t res;
if (!USART2->SR) {
return;
}
res = USART2->DR;
if (((USART_RX_STA & 0x8000) != 0) || ((USART_RX_STA & 0x4000) != 0)) {
return;
}
if (res == 0x0d) {
USART_RX_STA |= 0x4000;
} else {
if (USART_RX_STA > (USART_REC_LEN - 1)) {
USART_RX_STA = 0;
}
}
}
5.1.5 在源文件范围内声明和定义的所有函数,除非外部可见,否则都应使用static修饰。
如果一个函数只是在同一文件内调用,那么就用static修饰,避免和其他文件中的相同标识符发生混淆。
5.1.6 非调度函数应避免使用共享变量,不可避免的地方应集中使用;可重入函数应避免使用共享变量,若确实需要使用,则应通过互斥手段(关中断、信号量)对其加以保护。
调度函数是指根据输入的消息或命令,启动相应的函数或过程,而本身并不完成具体功能的函数。
可重入函数是指可能被多个任务并发调用的函数。在多任务操作系统中,函数具有可重入性是多个任务可以共用此函数的必要条件。
共享变量指的全局变量和static变量。
功能函数应尽量通过操作参数或局部变量来实现具体功能,各个任务会维护自己的栈空间,数据不会彼此影响。
5.2 参数和返回值
5.2.1 模块接口函数要对入参的合法性做全面检查,模块内部使用的函数对入参的合法性做安全检查即可。
供外部调用的接口函数,其入参是不可信的,需要对参数做全面的检查保证数据有效并符合取值范围。
模块内部使用的函数入参一般已经过滤,通常认为是可信的,过度的参数检查会造成大量冗余代码,因此只要在使用前判断指针不为空、除数不为零等安全条件即可。
5.2.2 函数的不变参数使用const 修饰。
使用const修饰不变的入参,在编译时会对其进行检查,避免函数内部误操作导致的错误,使代码更安全。
void* memcpy(char *strDest, const char *strSrc, int Count); /* good */
5.2.3 函数的参数个数最多不超过5个,参数中有数组指针的,一定要加数组长度的参数。
5.2.4 不带参数的函数应当声明为void 类型的参数。
int32_t hal_timer_init(); /* bad */
int32_t hal_timer_init(void); /* good */
5.2.5 函数参数应该使用确定的类型,尽量避免使用通用的void *类型,通用类型不利于参数检查。
5.2.6 函数不要调用另一个函数作为参数使用,这不利于代码阅读和调试。
func1(func2(), func3()); /* bad */
5.2.7 除具有打印功能的函数外,不得定义变参函数。
变参存在许多潜在的问题,仅允许在打印日志相关的函数中使用stdarg.h、va_arg、va_start 和va_end。
5.2.8 除特定的算法函数外,禁止函数递归调用,使用递归时要有确定退出条件。
5.2.9 void 返回类型的函数也需要显式的return 语句,以表明函数结束。
void hal_adc_init(void)
{
/* code */
return;
}
5.2.10 对函数的错误码做全面处理。
一个函数(标准库中的函数/第三方库函数/用户定义的函数)能够提供一些指示错误发生的方法。可以是错误标记变量、特殊的返回值或者其他手段,只要函数提供了这样的机制,调用程序都应该在函数返回时立刻检查错误指示。
六、语句和表达式
6.1 表达式
6.1.1 用括号明确表达式的操作顺序,避免过分依赖默认优先级。
使用括号强调所使用的操作符,防止因默认的优先级与设计思想不符而导致程序出错,然而过多的括号会分散代码降低可读性,因此不要并列过多的条件。
6.1.2 禁止在表达式中进行赋值操作及其他有副作用的操作。
uint8_t count = 0;
if (count++ < 10) { /* bad */
/* code */
}
6.1.3 不允许使用逗号运算符。
使用逗号运算符会降低代码的可读性,可以使用其他方法达到相同的效果。
6.1.4 不允许使用三目运算符。
三目运算符确实可以在形式上简化代码,但嵌套使用会导致程序恶化,而且其执行顺序由编译器决定,其语义又可以用if else 语句替换,没必要为了单纯减少代码行数使用该运算符。
6.1.5 除数不能为零;指针变量使用前要NULL进行判空;无符号数做减法,减数不能大于被减数。
这些都是导致程序异常甚至死机的错误,在程序中一定要避免。
6.1.6 除非操作数是布尔类型,否则应当显示的表明表达式的判断条件。
int32_t num = 0;
if (num) { /* bad */
/* code */
}
if (num != 0) { /* ok */
/* code */
}
bool flag = flase;
if (flag) { /* ok */
/* code */
}
6.1.7 浮点表达式不能做相等或不等的检测。
这是浮点类型的固有特性,等值比较通常不会计算为true。而且,这种比较行为不能在执行前做出预测,它会随着实现的改变而改变。
6.2 语句
6.2.1 for 语句的三个表达式应该只关注循环控制,不应该存在其他副操作。
for 语句的三个表达式都存在时,它们应该只用于如下目的:
- 第一个表达式 初始化循环计数器
- 第二个表达式 对循环计数器和其他值的比较
- 第三个表达式 循环计数器递增或递减,不能在循环体中修改循环计数器
6.2.2 使用else if 语句时,最后必须加else 分支。
/* ok */
if (count == 0) {
/* code */
} else if (count > 0) {
/* code */
} else { /* nothing */
}
6.2.3 使用switch 语句时,最后必须加default 分支,case 后不跟break 的要注释说明。
switch (cmd) {
case RT_TIMER_CTRL_GET_TIME:
*(rt_tick_t *)arg = timer->init_tick;
break;
case RT_TIMER_CTRL_SET_PERIODIC:
timer->parent.flag |= RT_TIMER_FLAG_PERIODIC;
break;
case RT_TIMER_CTRL_GET_STATE: /* example */
case RT_TIMER_CTRL_GET_REMAIN_TIME:
*(rt_tick_t *)arg = timer->timeout_tick;
break;
default: /* nothing */
break;
}
else if 语句最后加else 语句,switch 语句中最后加default 的目的是要求程序做保护性编程,应执行适当动作保证程序安全,即使没有执行动作也要添加注释以说明原因。
6.2.4 仅允许在函数体内使用goto 语句,用于出现错误时统一处理,其他情况则禁止使用goto 。
/* ok */
void func1(void)
{
if (!func2()) {
goto EXIT;
}
if (!func3()) {
goto EXIT;
}
EXIT:
func4();
return;
}
6.2.5 不允许代码中存在执行不到的语句。
程序中执行不到的代码或未使用的变量不仅占用额外的空间,还会影响程序的可读性和性能,应当删除。
七、注释
7.1 要求
7.1.1 应当力求减少注释,最多不要超过代码量的30%,优秀的代码可以自我解释,不通过注释也可轻易读懂。
注释无法把糟糕的代码变好,需要很多注释来解释的代码往往存在坏味道,需要重构。
7.1.2 注释要解释代码难以直接表达的意图,而不是重复描述代码。
注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码。注释不是为了名词解释,而是为了说明用途。
7.1.3 修改代码时要同时修改其注释,以保证注释与代码的一致性,不用的注释要及时删除。
7.1.4 注释应放在其代码上方相邻位置或右方,禁止放在其他位置。
7.1.5 考虑程序的实际使用者,建议尽量使用中文进行注释。
7.1.6 不允许程序中存在注释掉的代码。
不使用的代码应当及时删掉,具有兼容性质的代码应当使用宏控,而不是注释掉。
7.2 格式
7.2.1 注释统一使用 /* */ 进行注释,不使用 // 进行注释,单行注释内容左右各留一个空格。
/* 单行注释 */
/**
* 多行注释
*/
7.2.2 在文件开头顶格注释版权声明,并简要概述文件作用或实现的功能,并记录作者名和创建日期。
修改文件内容时,仅修改声明的日期即可,具体的修改内容应该通过版本工具进行记录。只有涉及大的功能变更时才需修改说明,但此时应该优先考虑增加新文件,而不是在原文件上大改。
/**
* Copyright (C) 2022-2022 Company Name
*
* @brief 文件简介
* @author 作者
* @date 2022-11-20
*/
7.2.3 接口函数声明的注释要描述函数功能及用法,包括输入和输出参数、返回值和调用要求等。
不要为所有的函数都写注释,只在重要的、复杂的函数定义处注释其功能和实现要点即可。
/**
* @brief 功能简介
*
* @param 参数1说明
* @param 参数2说明
*
* @return 返回值说明
*/
八、排版与格式
8.1 通用格式
8.1.1 代码文件统一用UTF-8 编码格式保存。
尤其是代码中存在中文时,使用UTF-8 编码可以避免在不同编辑器上显示乱码的问题。
8.1.2 除函数定义外,语句块左大括号紧跟前行, 每级缩进为4个空格,不允许使用Tab 缩进。
大体上遵循K&R 风格,实现紧凑的代码结构,提高阅读效率,但K&R 风格的缩进是8个空格,过于冗长,使用4个空格足已,这也是其他主流编程语言的通用规范。
在编写代码时可以将编辑器的Tab 键设置为4个空格以简化操作。
8.1.3 if 、for 、do 、while 、case 、switch 、default 等语句需要独占一行。
8.1.4 if 、for、else、while 等语句即使只有一行内容甚至没有内容,也要写{},不允许省略。
/* ok */
typedef struct {
uint8_t age;
uint8_t gender;
} cat_t;
if (num == 0) {
/* code */
} else if (num > 0) {
/* code */
} else {
/* code */
}
while (true) {
/* code */
}
do {
/* code */
} while (i < 5);
for (uint8_t i = 0; i < 10; i++) {
/* code */
}
switch (cmd) {
case CMD1:
/* code */
break;
case CMD2:
/* code */
break;
default:
/* code */
break;
}
void func (void)
{
/* code */
return;
}
8.1.5 一条语句不能过长,如不能拆分则需要分行写,一行最多不超过120个字符。
换行时要增加一级缩进,可在操作符尾处换行,注意行尾不要有空格。
换行时一个完整的语句放在一行,不要单纯因为字符数换行。
8.1.6 多个短语句不允许写在同一行内 ,即一行只写一条语句。
/* bad */
int32_t a, b, c;
int32_t a; int32_t b;
int32_t a = 0; int32_t b;
/* ok */
int32_t a = 0;
int32_t b;
8.1.7 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符前、后要加空格 ; 对于关系密切的立即操作符(-> . ),则不加空格;指针变量* 统一跟随变量,在类型和* 间加空格。
对于已经很清晰的语句没有必要再加空格,如括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格。
/* ok */
uint16_t arr[][] = {{1, 2, 3}, {4, 5, 6}};
if (time >= MAX_TIME_VALUE)
a = b + c;
a *= 2;
char *p = 'a';
p = &mem;
i++;
p->id = pid;
p.id = pid;
8.1.8 预处理和宏语句必须顶格写,不允许在# 前出现其他字符。
虽然语法要求预处理指令前可以有空格,但在操作中应避免这样的写法,即使预处理在语句块中也要顶格写,不受缩进限制。
8.1.9 #define不能在语句块中进行定义,必须放在文件开头。
尽管在C代码中的任何位置放置#define 都是合法的,但放在语句块中会使人误以为该宏仅在该语句块起作用。#define 指令要放在第一个函数定义之前。
8.1.10 注释在上方的,需要顶格开始;在右侧的,在仅有一个注释的时候,注释与语句留一个空格; 若多行语句均需注释,则注释与最长语句留一个空格,其他注释与之对齐。
8.2 文件示例
/**
* Copyright (C) 2021-2021 0x1abin
*
* @brief Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* @author 0x1abin
* @date 2021-12-28
*/
#ifndef MULTI_TIMER_H
#define MULTI_TIMER_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define MULTI_TIMER_SUCCESS 0
#define MULTI_TIMER_FAILURE (-1)
typedef struct multi_timer_handle {
struct multi_timer_handle *next;
uint64_t deadline;
multi_timer_func_t callback;
void *user_data;
} multi_timer_t;
typedef uint64_t (*platform_ticks_func_t)(void);
typedef void (*multi_timer_func_t)(multi_timer_t *timer, void *user_data);
/**
* @brief Start the timer work, add the handle into work list.
*
* @param timer target handle strcut.
* @param timing Set the start time.
* @param callback deadline callback.
* @param user_data user data.
*
* @return MULTI_TIMER_SUCCESS or MULTI_TIMER_FAILURE.
*/
int32_t multi_timer_start(multi_timer_t *timer, uint64_t timing, multi_timer_func_t callback, void *user_data);
#ifdef __cplusplus
}
#endif
#endif
/**
* Copyright (C) 2021-2021 0x1abin
*
* @brief from MultiTimer project
* @author 0x1abin
* @date 2021-12-28
*/
#include <stdio.h>
#include "multi_timer.h"
/* Timer handle list head. */
static multi_timer_t *timer_list = NULL;
/* Timer tick */
static platform_ticks_func_t ticks_func = NULL;
int32_t multi_timer_start(multi_timer_t *timer, uint64_t timing, multi_timer_func_t callback, void *user_data)
{
if ((timer == NULL) || (callback == NULL)) {
return MULTI_TIMER_FAILURE;
}
multi_timer_t **next_timer = &timer_list;
/* Remove the existing target timer. */
while (*next_timer != NULL) {
if (timer == *next_timer) {
*next_timer = timer->next; /* remove from list */
break;
}
next_timer = &(*next_timer)->next;
}
/* Init timer. */
timer->deadline = ticks_func() + timing;
timer->callback = callback;
timer->user_data = user_data;
#ifdef MULTI_TIMER_DEBUG
printf("info: multi_timer_start timer->deadline %d\r\n", timer->deadline);
#endif
/* Insert timer. */
next_timer = &timer_list;
while (next_timer != NULL) {
if (*next_timer != NULL) {
timer->next = NULL;
*next_timer = timer;
break;
}
if (timer->deadline < (*next_timer)->deadline) {
timer->next = *next_timer;
*next_timer = timer;
break;
}
next_timer = &(*next_timer)->next;
}
return MULTI_TIMER_SUCCESS;
}
说明:用例参考0x1abin/MultiTimer开源项目,在此仅为展示编码格式,程序内容已经删减修改并不准确。
附录
argument / arg | buffer / buf | clock / clk | command / cmd | compare / cmp | configuration / cfg | device / dev |
error / err | hexadecimal / hex | increment / inc | initialize / init | maximum / max | message / msg | minimum / min |
parameter / para | previous / prev | register / reg | semaphore / sem | statistic / stat | synchronize / sync | temp / tmp |
add/remove | begin/end | create/destroy | insert/delete | first/last | get/release | increment/decrement |
put/get | add/delete | lock/unlock | open/close | min/max | old/new | start/stop |
next/previous | source/target | show/hide | send/receive | source/destination | copy/paste | up/down |
参考资料
- 华为C&C++ 语言编程规范
- 阿里C 语言规范
- MISRA-C 编程规范
- Mbed TLS 编码规范