一、前言
C语言是我们嵌入式使用最多的一种编程语言。在日常工作中,由于C语言没有强制的编码风格要求,导致每个人的代码风格各异,不利于同事们之间的沟通与移植。
编码规范的好处:
- 促进团队之间的合作:一些较大的项目,我们可以拆分成多个功能,每个人负责不同的功能,最后再合并。如果没有统一的代码规范,那么每个人的代码必将风格迥异,导致合并代码的时候不仅要去处理程序的问题,还得花大量的时间和精力去理解别人的代码。
- 减少BUG的出现:规范输入输出的参数,对一些异常的处理规范,这样就会在测试过程中减少一些异常以及低端的代码错误引起的低级bug。
- 降低维护成本:当项目上线逐渐累积,后期的维护成本也有随之提升。例如:A开发完产品,B维护过程中加了一段代码,之后还有C、D等等,这时候每个人的代码还不规范会导致项目维护成本骤增,出现传说中的"屎山上拉屎"的现在,更严重的需要项目重构等,严重浪费人力资源。
- 利于代码审查:一个合格的项目在上线前后有专人进行代码审查,防止出现一些低端的逻辑性错误。规范的代码的风格,可以让代码审查效率更高,也可以尽快的发现一些bug。
- 自身的成长:当自身对自己代码规范性越来越严格的时候,自身的能力也会提升的越快,同时也会减少由于代码不规范所引起的低级bug,可以花更多的时间去找问题解决问题的方案。
代码规范的原则:安全、可靠、可读、可维护、可测试以及可移植
PS:修改外部开源代码、第三方代码时,应该遵守开源代码、第三方代码已有规范,保持风格统一,避免因规则、风格冲突而对第三方代码进行修改
二、代码规范
(一)通用规范
- 编码格式统一采用UTF-8:更好的适配各种环境
- 换行符号采用Unix 的风格 LF,而不采用windows的风格CRLF
- 代码缩进一律为4个空格符号
(二)头文件
1. 命名规则
头文件的命令应遵循以下规则:
-
按照unix风格,小写的单词用下划线连接
-
包含模块、子模块、功能信息(如有必要)
-
仅使用通用的缩写,不要随意使用字母缩写,缩写必须能够完全的表达其含义
-
如果外部头文件合内部头文件同名,可以在内部头文件名之前加上“_”以区分
-
禁止同名头文件
2. 描述信息
头文件的头部需要加上描述性的注释信息,用于描述该文件的作用、版本以及作者等
头文件头部注释模板:
/**
* @file xx.h
* @author xxxxxx
* @brief xxxx module is used to xxxx
* @version 0.1
* @date 2022-12-06
*
*/
3. 头文件宏
为了避免头文件重复include导致冲突,每个头文件必须使用宏来避免冲突,宏的命名方式和文件名保持一致,格式:
-
以双下划线开头、结尾
-
文件名中字母大写
-
所有非字母符号转换成下划线
-
所有的头文件内容都必须放在宏保护区域内
如demo_test.h,转换成宏应如下:
#ifndef __DEMO_TEST_H__
#define __DEMO_TEST_H__
#endif
4. extern "C"的使用
为了保证在c函数在c++环境下正常使用,应在头文件使用 extern “c”宏来保证函数声明的确定性。
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif /* __cplusplus */
5. 外部头文件引用
头文件引用,必须放在 extern "C"宏之外。
-
保证所有的引用的头文件的必要性,禁止循环依赖,如a.h依赖b.h,b.h依赖c.h,c.h依赖a.h。
-
外部头文件禁止引用内部头文件
-
头文件引用应保持有序、美观
-
外部头文件引用外部头文件的时候需要注意层次,避免依赖倒置
-
内部头文件只能引用本模块的内部头文件,引用的时候需要使用相对路径
6. 头文件函数命名规则
详细参考函数规范:函数
7. 头文件存放位置
一般的头文件我们都存放在对应的include文件夹下,如有特殊的头文件:例如只给自己本模块代码使用,外部无需调用的可以存放在src文件夹下,防止外部的调用。
8. 内联函数
-
谨慎使用内联函数,仅当测试数据证明内联函数有效的提升了性能之后,才可以使用内联函数
-
内联函数的定义、实现都应当放在头文件中
-
内联函数的长度不应超过10行
-
内联函数规范参照:函数
9. 数据类型定义
-
仅对外的数据类型需要放到头文件。
-
模块内部使用的数据类型,放到源文件里去定义。
-
数据类型规范参照:类型
(三)源文件
1. 命名规则
相对来说源文件的命名规则和头文件类似:
- 按照unix风格,小写的单词用下划线连接
- 包含模块、子模块、功能信息(如有必要)
- 仅使用通用的缩写,不要随意使用字母缩写,缩写必须能够完全的表达其含义
- 禁止同名头文件
2. 描述信息
袁文件的头部需要加上描述性的注释信息,用于描述该文件的作用、版本以及作者等
源文件头部注释模板:
/**
* @file xxx.c
* @author xxx
* @brief xxx module is used to xxx
* @version 0.1
* @date 2022-12-07
*
*/
/***********************************************************
*************************micro define***********************
***********************************************************/
/***********************************************************
***********************typedef define***********************
***********************************************************/
/***********************************************************
***********************variable define**********************
***********************************************************/
/***********************************************************
***********************function define**********************
***********************************************************/
3. 函数
(1) 命名规范
-
遵守unix风格命名方式,以小写字母和下划线组成函数名;
int iot_init_params(IN unsigned char *p_init_params);
-
对外接口应包含模块、功能信息,以提高可读性;
-
函数命名采用动宾结构的方式,如set_xxx,get_xxx;
void user_uart_init(void); void user_uart_get_data(OUT unsigned char *p_data, OUT unsigned int *p_data_len);
-
使用通用的字母缩写,不应该随意使用缩写,以提高可读性;
// bad: VOID sf_register_mqc_cb(VOID) // sf,mqc皆为不通用的缩写 // good: VOID smartframe_register_mqtt_clinet_cb(VOID) // 如果有明确的通用的缩写,可以使用;没有明确通用的缩写,尽量使用完整单词
-
模块内部函数接口应以“__”双下划线开头,并建议以声明为static;
// bad: BOOL_T scan_test_ssid(VOID) // good: STATIC BOOL_T __scan_test_ssid(VOID)
(2) 函数的申明
通过头文件引用方式获得函数的声明,而不是使用extern方式;尽量避免使用extern作用与函数。
// bad:
extern int mf_init(void);
mf_init();
// good:
#include"mf_test.h"
mf_init();
(3) 函数的形参
函数形参名称应保持在同一行,如需要换行,则需要保持合理的对齐方式
-
参数名应遵守unix风格,以小写字母和下划线,并严格保持一致;
-
使用通用的字母缩写,不应该随意使用缩写,以提高可读性;
-
参数名应该具有明确含义,不要使用无意义的如param1,param2之类的参数;
-
复杂参数应该使用地址传递;
-
参数应该根据其用途进行修饰,如一些入参可以使用const以避免其被修改;
-
每个参数之间,需要保留1个空格作为间隔,保证阅读的流畅性,函数名称和左小括号直接不需要间隔。
-
没有参数的函数应使用void作为参数;
// 行宽可以容纳,尽量保持在同一行
void iot_wf_timeout_set(IN int timeout);
// 参数太多、太长,一行无法完全容纳,与第一个参数对齐:
void iot_wf_mcu_dev_init(IN CONST GW_WF_CFG_MTHD_SELcfg,
IN CONST GW_WF_START_MODE start_mode,
IN CONST IOT_CBS_S *cbs,
IN CONST CHAR_T *p_firmware_key,
IN CONST CHAR_T *product_key,
IN CONST CHAR_T *wf_sw_ver,
IN CONST CHAR_T *mcu_sw_ver);
// bad
OPERATE_RET mf_init(IN CONST MF_IMPORT_INTF_S* intf, IN CONST CHAR_T* file_name, IN CONST CHAR_T* file_ver, IN BOOL_T wrMacEn);
void func();
OPERATE_RET publish_event(CONST CHAR_T* name, VOID* data);//实在没有类型接收可以使用void *
//good
OPERATE_RET mf_init(IN CONST MF_IMPORT_INTF_S* intf, IN CONST CHAR_T* file_name, IN CONST CHAR_T* file_ver, IN BOOL_T mac_write_en);
void func(void);
(4) 函数的返回值
-
函数必须指定返回值;
// bad: func(void); // good: void func(void);
-
函数内部必须显式return;
// bad: void func(void) { // no return } // good: void func(void) { return; }
-
函数内对于有明确返回值的调用函数的返回值,需要进行判断,并进行异常处理;
// bad: OPERATE_RET foo(void) { BYTE_T cur_channel = 0; wf_get_cur_channel(&cur_channel); ... } // good: OPERATE_RET foo(void) { OPERATE_RET rt = OPRT_OK; BYTE_T cur_channel = 0 rt = wf_get_cur_channel(&cur_channel)