1. 背景
无论如何,标准是团队开发的保证之一,而且标准欢迎争吵和变化。我们使代码易于管理的方法之一是增强代码一致性,让别人可以读懂你的代码是很重要的,保持统一编程风格意味着可以轻松根据“模式匹配”规则推断各种符号的含义。创建通用的、必需的习惯用语和模式可以使代码更加容易理解,在某些情况下改变一些编程风格可能会是好的选择,但我们还是应该遵循一致性原则,尽量不这样去做。
2. 环境
统一开发人员的开发环境,包括文本编辑环境、SHELL环境,通常我们使用VIM的UTF-8编码环境,使用4个空格代替Tab进行缩进。
3. 命名
头文件(.h 文件)和程序文件(.c文件)文件名全部使用小写字母或数字,以下划线(_)进行分隔,且尽量保证头文件和程序文件的一一对应。
即: ngm_模块名_类别名
1 2 3 | #示例: ngm_cstring.h ngm_cstring.c |
4. 编码
4.1 头文件
4.1.1 #define的保护
头文件使用 #define 防止头文件被多重包含。
即:_Project_Path_File_H_
注:为保证唯一性,头文件的命名应基于其所在项目源代码树的绝对路径。
1 2 3 4 5 | #示例: #ifndef _NGM_CSTRING_H_ #define _NGM_CSTRING_H_ ...... #endif |
4.1.2 头文件依赖
使用前置声明尽量减少.h文件中#include的数量。
当一个头文件被包含的同时也引入了一项新的依赖,只要该头文件被修改,代码就要重新编译。如果你的头文件包含了其他头文件,这些头文件的任何改变也将导致那些包含了你的头文件的代码重新编译。因此,我们宁可尽量少包含头文件,尤其是那些包含在其他头文件中的。
注:对于内部调用的函数,即使定义到新的程序文件中,但其声明在调用此函数的程序文件开头,除非需要将此内部函数作为公共接口,对于同一个程序文件中的内部调用函数提倡使用static进行修饰,防止外部的错误调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #示例: //file a.h #include struct a_t { char a; }; //file a.c int b_func(...); int a_func(...) { statment; b_func(); } //file b.c int b_func(...) { statment; } |
4.1.3 函数说明
采用doxygen生成帮助文档,遵循doxygen文件说明格式。
1. 每个 .h 文件必须添加注释,包括头部和函数声明。
1 2 3 4 5 6 7 8 9 | #示例: /** * @file a.h * 描述:XXXX */ /** * a_func函数功能说明 */ int a_func( int a, char *b); |
注:由于函数的详细说明写在函数实现部分,所以在头文件中定义不强制要求详细说明,只需描述函数的基本功能。
2. 每个 .c 文件必须添加注释,包括头部和函数定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #示例: /** * @file a.c * 描述: XXXX */ /** * @brief a_func函数功能说明 * @param[in] a 参数a说明 * @param[in] b 参数b说明 * @return 返回值说明 * @notes 注意事项说明 */ int a_func( int a, char *b) { ...... } |
3. 代码中关键部分必须添加注释。
4. 将代码按照模块进行分类。
定义一个组的方法有:@defgroup 组id(唯一的),组描述;@addtogroup也是创建组,两者的区别是addtogroup的组id如果不存在,则自动创建,如果存在,则加入改组;@ingroup 组id 属于某个组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 示例: /** * @file ngm_doxygen.h * @brief ngm modules define * @remark 此头文件必须包含在 */ 定义core模块 /** * @defgroup ngm_core * @{ */ /** * @} */ 需要加入 ngm_core 组的文件中增加 /** * @addtogroup ngm_core * @{ */ ...... /** * @} */ |
4.1.4 函数参数顺序
定义参数时,参数顺序为:类对象、输入参数、输出参数。
4.2 程序文件
4.2.1 函数定义的描述
函数定义的描述使用doxygen格式进行详细描述。
4.2.2 函数定义的风格
函数定义采用ASNI C方式,即函数参数在圆括号内进行类型定义。避免使用 K&R 方式,同时确保声明和定义格式统一。
1 2 3 4 5 | 示例: int func( int a, char *b) { ...... } |
4.2.3 全局变量与局部变量
程序库中通常禁止定义全局变量,尽量使用函数参数传递的方式进行变量值的共享。
4.2.4 静态函数(供内部调用)
程序文件中供内部调用的函数(非公共接口函数),尽量使用static进行内部声明的修饰,防止外部程序的错误调用。
4.2.5 包含头文件顺序
我们通常采用:(a)系统头文件;(b)标准头文件;(c)第三方头文件;(d)自己的头文件
4.2.6 编写短小函数
倾向于选择短小、精炼的函数。长函数有时是恰当的,因此对于函数长度并没有严格限制。如果函数超过80行,可以考虑在不影响程序结构的情况下将其分割一下。即使一个长函数现在工作的非常好,一旦有人对其修改,有可能出现新的问题,甚至导致难以发现的bugs。使函数尽量短小、简单,便于他人阅读和修改代码。在处理代码时,你可能会发现复杂的长函数,不要害怕修改现有代码:如果证实这些代码使用、调试困难,或者你需要使用其中的一小块,考虑将其分割为更加短小、易于管理的若干函数。分割函数时需要注意一下,不要使函数嵌套太深,这样不利于阅读,定位bug也不方便。
5. 注释
5.1 注释风格
统一使用 /* …… */,并且注释语言统一使用中文。
5.2 函数注释
5.2.1 函数声明注释
函数声明只描述函数的基本功能,详细描述统一放置于函数定义中。
5.2.2 函数定义注释
函数定义描述函数详细说明,以及函数功能和实现要点。如使用的漂亮代码、实现的简要步骤、如此实现的理由、为什么前半部分要加锁而后半部分不需要。函数定义处注释的基本内容:
(1) inputs(输入)及outputs(输出);
(2) 函数内部是否分配了空间,是否需要由调用者释放;
(3) 参数是否可以为NULL或其他返回值;
(4) 是否存在函数使用的性能隐忧(performance implications);
(5) 如果函数是可重入的(re-entrant),其同步前提(synchronization assumptions)是什么?
5.3 变量注释
通常变量名本身足以很好说明变量用途,特定情况下,需要额外注释说明。
注:对于业务相关变量尽量加上注释。
5.4 实现注释
对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。
5.5. 单行及多行注释
采用单行或多行注释,置于实现代码句(块)的上方。
注:变量声明的注释可置于变量后方。
1 2 3 4 5 6 7 8 9 10 11 | #示例: if (condition) { int num_errors; /* 内部错误计数 */ /* 以下代码的运行注释 */ ...... /** * 以下代码的运行注释 * XXXX */ ...... } |
6. 格式
6.1 行长度
一行不应该超过 80 列,如果超过 80 列了,想办法折断语句。
6.2 空格还是制表符
对于缩进是一直引起争议的,我们将tab设置为4个空格,程序内部的缩进每次使用一个Tab(即4个空格)。
6.3 函数与声明
返回类型和函数名在同一行,合适的话,参数也放在同一行。
6.4 花括号的争议
即使是最优秀的程序员也会在这个地方引起剧烈争论,就是 {} 规则。
(1) 这里明确指出,两种方法(即 K&R 或 BSD)都是可以接受的,但是你不能在一个文件中引入新的风格或混用两种风格。
(2) if, else, while/do, for 等之后, 总是使用 {} 包括起来,即使只有一个语句。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #示例: #(1)Apache风格 if (condition) { ...... } else { ...... } #(2)MySQL风格 if (condition) { ...... } else { ...... } #(3)PHP风格 if (condition) { ...... } else { ...... } |
6.5 函数名后面括号的争议
不要在函数名和括号之间保留空格。
1 2 3 4 | #不推荐: func () #推荐: func() |
6.6 条件/循环/开关语句
(1) if/else, while/do, switch/case 等操作符之后留一个空格。
(2) 更提倡不在圆括号中添加空格。
(3) 通常情况下右圆括号和左大括号(如果使用的话)间也要有个空格
1 2 3 4 | #不推荐: if (condition); for (;;); #推荐: if (condition); for (;;); |
6.7 逗号和分号后留有空格
逗号和分号后面要留有空格。
1 2 3 4 | #不推荐: func( int a, char *b); for (i=0;i<n;i++); #推荐: func( int a, char *b); for (i = 0; i < n; i++); |
6.8 一元操作符前后留有空格
= 、+ 、-、/、 * 等一元操作符前后留有空格
1 2 3 4 | #不推荐: var=a*5; #推荐: var = a * 5; |
6.9 条件表达式
(1) 通常将常量值置于 == 的左方,避免程序书写错误导致成为赋值语句。
(2) 比较时的常量值采用:整数用0,实数用0.0,指针用NULL,字符(串)用’\0′。
(3) 尽量避免在条件表达式中使用 ! 操作符。例如: if (!p) {};
(4) 逻辑与&&(逻辑或||)操作符总是位于行首,避免一行多于80个字符。
7. 声明
7.1 每行声明的变量数量
推荐一行一个变量。因为这样以利于写注释,此时注释可以写在变量后面或是上方。
7.2 初始化
尽量在声明局部变量的同时初始化。唯一不这么做的理由是变量的初始值依赖于某些先前发生的计算。
1 2 3 4 5 6 7 8 9 10 11 12 | #示例: #整数初始化: int i = 0; #字符数组初始化: char buf[32] = {0}; #指针初始化: char *ptr = NULL; #结构初始化: 采用init或 new 的方式。 |
7.3 变量声明布局
只在函数定义(或是代码块)的开始处声明变量。避免在函数中间或首次使用变量时进行声明,虽然C99支持这种声明。
8. 语句
8.1 简单语句
同一行内只写一行语句,使用分号结束。
1 2 3 4 5 | #不推荐: i++; j++; #推荐: i++; j++; |
8.2 复合语句
尽量避免使用嵌套的复合语句。
1 2 3 4 5 | #不推荐: if (NULL == (fp = fopen (filename, "r" ))) #推荐: fp = fopen (filename, "r" ); if (NULL == fp) |
8.3 返回语句
return表达式中不要使用圆括号。
8.4 条件运算符
尽量避免使用条件运算符(? :)。
8.5 条件语句
if-else条件语句总是使用 {} 花括号括起来,即使只有一条语句,并且避免else的悬挂。
1 2 3 4 5 6 7 8 9 10 11 12 13 | #示例: if (condition) { /* 注释 */ …… } else if (condition) { /* 注释 */ …… } else { /* 注释 */ …… } |
8.6 循环语句
8.6.1 for循环语句
(1) 循环体使用花括号 {} 括起来,并且设置initialization,condition,update。
(2) 空循环体应使用花括号 {} 或 continue,而不是一个简单的分号。
8.6.2 while/do-while循环语句
(1) 循环体使用花括号 {} 括起来,并且设置 condition 条件.
(2) 对于true 或 1的真条件进行注释,同时注意条件值的增长。
(3) 空循环体应使用花括号 {} 或 continue,而不是一个简单的分号。
8.7 开关选择语句
(1) switch语句可以使用大括号分块;空循环体应使用{}或continue。
(2) 每个case必须以break结束,并且switch 的 default case 总应该存在。
8.8 自增/自减操作符
尽量避免自增/自减操作的嵌套赋值。统一只用后置自增自减操作符(除非程序特殊需要),因为目前的智能编译器对于前置和后置的操作符已经可以生成同样效率的汇编代码。
8.9 sizeof操作符
尽可能使用sizeof(varname)代替sizeof(type)。使用sizeof(varname)是因为当变量类型改变时代码自动同步,有些情况下sizeof(type)或许有意义,还是要尽量避免,如果变量类型改变的话不能同步。
8.10 const的使用
在函数声明时,对于输入变量参数尽量使用const加以修饰。
9. 空白与空行
首要原则:水平留白的使用因地制宜,不要在行尾添加无谓的留白,空行越少越好。
空行的目的:将逻辑相关的代码段分隔开,以提高可读性。这不仅仅是规则而是原则问题了:不是非常有必要的话就不要使用空行。尤其是:不要在两个函数定义之间空超过2行,函数体头、尾不要有空行,函数体中也不要随意添加空行。
基本原则是:同一屏可以显示越多的代码,程序的控制流就越容易理解。当然,过于密集的代码块和过于疏松的代码块同样难看,取决于你的判断,但通常是越少越好。
10. 函数、变量命名规则
最重要的一致性规则是命名管理,命名风格直接可以直接确定命名实体是:类型、变量、函数、常量、宏等等,无需查找实体声明,我们大脑中的模式匹配引擎依赖于这些命名规则。
命名规则具有一定随意性,但相比按个人喜好命名,一致性更重要,所以不管你怎么想,规则总归是规则
函数命名、变量命名、文件命名应具有描述性,不要过度缩写,类型和变量应该是名词,函数名可以用“命令性”动词。
10.1 函数
函数名全部采用小写字母或数字,使用下划线(_)进行分隔。
即:ngm_模块名_类别(功能)名_操作名
1 2 | 示例: ngm_mime_header_parse(...); |
10.2 变量
变量名一律小写,单词间以下划线相连,尽量减少缩写。
基本数据类型定义的变量通常采用具有明确意义的单词或是缩写,系统typedef声明的数据类型定义的变量通常采用类型缩写作为前缀,尽量做到变量名的清晰和简单。
变量命名约定:
(1) 结构类型:以结构简写开头,如:bf_dispname
(2) 静态变量:以 s_ 开头
(3) 全局变量:以 g_ 开头
10.3 常量
const常量在名称前加c:c_username。
10.4 结构
所有结构体以typedef进行定义,并采用 *_t 的结构体标识方式。
1 2 3 4 | #示例: typedef struct _ngm_cstring_t { ...... } ngm_cstring_t; |
10.5 枚举
枚举值应全部为大写字母或数字,单词间以下划线相连。枚举名采用骆驼命名方式。
1 2 3 4 5 | #示例: enum NgmTableErrors { NGM_RET_OK; NGM_RET_ERROR; }; |
10.6 宏定义
宏定义应全部为大写字母或数字,单词以下划线相连。
1 2 3 | #示例: NGM_RET_SUCCESS 0; NGM_RET_FAILED -1; |
联系客服