1 文件与目录
1.1 文件命名
- 1 文件与目录的命名可以使用的字符为
[A-Z, a-z, 0-9, ., _, -]
。 - 2 文件扩展名使用小写字母,如
.c
、.h
。 - 3 文件的命名要准确清晰地表达其内容,同时文件名应该精炼,在文件名中可以适当地使用缩写。
例如:
- 模块名称简写+文件名称,如mpMain.c、mpDisp.h
- 直接使用文件名称,如main.c、disp.h
- 4 文件命名采用小驼峰命名法。
1.2 目录
- 5 一个软件包或者逻辑组件的所有头文件和源文件建议放在一个单独的目录下,以便查找。
- 6 对于头(源)文件的段落安排,建议如下:
- 文件头注释 <适用于头文件和源文件>
- 防止重复应用头文件的设置 <适用于头文件>
- 头文件包含部分 <适用于头文件和源文件>
- 宏定义部分 <适用于头文件和源文件>
- 枚举常量声明部分 <适用于头文件和源文件>
- 类型声明和定义部分,如
struct
、union
、typedef
等 <适用于头文件和源文件>- 全局变量声明部分 <适用于头文件和源文件>
- 文件级变量声明 <适用于源文件>
- 全局函数声明 <适用于头文件>
- 文件级函数声明 <适用于源文件>
- 函数实现,按函数声明的顺序 <适用于源文件>
- 文件尾注释 <适用于头文件和源文件>
- 7 包含头文件时使用相对路径,禁止使用绝对路径
示例:
#include "/project/inc/hello.h" /* NG: 不应使用绝对路径 */
#include "../inc/hello.h" /* OK: 可以使用相对路径 */
- 8 在引用头文件时,使用
<>
来引用预定义或者特定目录的头文件,使用""
来引用当前目录或者路径相对于当前目录的头文件。
示例:
#include <stdio.h> /* 标准头文件 */
#include <projdefs.h> /* 工程指定目录头文件 */
#include "global.h" /* 当前目录头文件 */
#include "inc/config.h" /* 路径相对于当前目录的头文件 */
- 9 为了防止头文件被重复引用,应当用
#ifndef/#define/#endif
结构产生预处理块。
示例:
#ifndef __DISP_H__ /* 文件名前名加两个下划线“__”,后面加“_H__” */
#define __DISP_H__
//......
#endif
- 10 头文件中只存放 “声明”而不存放 “定义”。
示例:
/* 模块1头文件:module1.h */
extern int a=5; /* OK: 在模块1的.h文件中声明变量 */
uint8_t g_ucPara; /* NG: 在模块1的.h文件中定义全局变量g_ucPara*/
2 排版
- 1 程序块要采用缩进风格编写,缩进的空格数为4个。
- 2 相对独立的程序块之间、变量说明之后 必须加空行。
示例:
void demoFunc( void )
{
uint8_t i;
/* 此处为空行, 局部变量和语句间空一行 */
/* 功能块1 */
for ( i = 0; i < 10; i++ )
{
//...
}
/* 此处为空行,不同的功能块间空一行*/
/*功能块2*/
for ( i = 0; i < 20; i++ )
{
//...
}
}
- 3 较长的语句或函数过程参数( >80字符)要分成多行书写,长表达式要在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进行适当的缩进,使排版整齐,语句可读。
示例:
if( ( ucParam1 == 0 ) && ( ucParam2 == 0 ) && ( ucParam3 == 0 )
|| ( ucParam4 == 0 ) )<----长表达式需要换行书写
{
//...
}
- 4 不允许把多个短语句写在一行中,即一行只写一条语句。
示例:
rect.length = 0; rect.width = 0; // NG: 一行多条语句
rect.length = 0; // OK: 一行一条语句
rect.width = 0;
5 对齐缩进为 4个空格字符。
6 函数或过程的开始、结构的定义及循环、判断等语句中的代码都要采用缩进风格,case语句下的情况处理语句也要遵从语句缩进要求。
7 程序块的分界符(如大括号
{
、}
)应各独占一行并且位于同一列,同时与引用它们的语句左对齐。在函数体的开始、类的定义、结构的定义、枚举的定义以及if
、for
、do
、while
、switch
、case
语句中的程序都要采用如上的缩进方式。对于与规则不一致的现存代码,应优先保证同一模块中的风格一致性。
示例:
void func( void )
{
for(...){ /* NG: {需要单独占用一行 */
//.../*programcode*/
}
for(...)
{ /* 规范的写法 */
//.../*programcode*/
}
}
8 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格,给操作符留空格时不要连续留两个以上空格。
(1) 逗号、分号只在后面加空格。
int_32 a, b, c;
- (2) 比较操作符,赋值操作符
=
、+=
,算术操作符+
、%
,逻辑操作符&&
、&
,位域操作符<<
、^
等双目操作符的前后加空格。
if( current_time >= MAX_TIME_VALUE )
{
a = b + c;
}
a *= 2;
a = b ^ 2;
- (3)
!
、~
、++
、--
、&
(地址运算符)等单目操作符前后不加空格。
*p = 'a'; /* 内容操作"*"与内容之间 */
flag = !isEmpty; /* 非操作"!"与内容之间 */
p = &mem; /* 地址操作"&"与内容之间 */
i++; /* "++","--"与内容之间 */
- (4)
->
、.
前后不加空格。
p->id = pid; /* "->"指针前后不加空格 */
- (5)
if
、for
、while
、switch
等与后面的括号间应加空格,使if
等关键字更为突出、明显, 函数名与其后的括号之间不加空格,以与保留字区别开。
if ( ( a >= b ) && ( c > d ) )
3 注释
- 1 一般情况下,源程序有效注释量必须在20%以上。
- 2 在文件的开始部分,应该给出关于文件版权、内容简介、修改历史等项目的说明。具体的格式请参见如下的说明。在创建代码和每次更新代码时,都必须在文件的历史记录中标注版本号、日期、作者、更改说明等项目。其中的版本号的格式为两个数字字符和一个英文字母字符。数字字符表示大的改变,英文字符表示小的修改。如果有必要,还应该对其它的注释内容也进行同步的更改。注意:注释第一行星号要求为76个,结尾行星号为1个。
/****************************************************************************
*Copyright(C),2010-2011,武汉汉升汽车传感系统有限责任公司
*文件名: main.c
*内容简述:*
*文件历史:
*版本号日期作者说明
*01a2010-07-29王江河创建该文件
*01b2010-08-20王江河改为可以在字符串中发送回车符
*02a2010-12-03王江河增加文件头注释
*/
- 3 对于函数,在函数实现之前,应该给出和函数的实现相关的足够而精练的注释信息。内容包括本函数功能介绍,调用的变量、常量说明,形参说明,特别是全局、全程或静态变量(慎用静态变量),要求对其初值,调用后的预期值作详细的阐述。具体的书写格式和包含的各项内容请参见如下的例子。
示例:
下面这段函数的注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。
/****************************************************************************
*函数名:SendToCard()
*功能:向读卡器发命令,如果读卡器进入休眠,则首先唤醒它
*输入:全局变量gaTxCard[]存放待发的数据
*全局变量gbTxCardLen存放长度
*输出:无
*/
4 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
5 注释的内容要清楚、明了,含义准确,防止注释二义性。
说明:错误的注释不但无益反而有害。注释主要阐述代码做了什么(What),或者如果有必要的话,阐述为什么要这么做(Why),注释并不是用来阐述它究竟是如何实现算法(How)的。
- 6 避免在注释中使用缩写,特别是非常用缩写。
说明:在使用缩写时或之前,应对缩写进行必要的说明。
- 7 注释应与其描述的代码靠近,对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面,如放于上方则需与其上面的代码用空行隔开。
例1:不规范的写法
void func( void )
{
/*获取复本子系统索引和网络指示器*/
//NG: 不规范的写法,此处不应该空行
repssnInd = ssnData[index].repssnIndex;
repssnNi = ssnData[index].ni;
}
例2:不规范的写法
void func( void )
{
repssnInd = ssnData[index].repssnIndex;
repssnNi = ssnData[index].ni;
/* 获取复本子系统索引和网络指示器 */ // NG: 不规范的写法,应该在语句前注释
}
例3:规范的写法
void func( void )
{
/*获取复本子系统索引和网络指示器*/
repssnInd = ssnData[index].repssnIndex;
repssnNi = ssnData[index].ni;
}
例4:不规范的写法,显得代码过于紧凑
void func( void )
{
/*获取复本子系统索引*/
repssnInd = ssnData[index].repssnIndex;
/* *获取复本子系统网络指示器 */ // NG: 代码与上一行注释之间需要空行
repssnNi = ssnData[index].ni;
}
例5:规范的写法
void func( void )
{
/*获取复本子系统索引*/
repssnInd = ssnData[index].repssnIndex;
/* *获取复本子系统网络指示器 */
repssnNi = ssnData[index].ni;
}
- 8 注释与所描述内容进行同样的缩排。
- 9 对变量的定义和分支语句(条件分支、循环语句等)必须编写注释。
说明:这些语句往往是程序实现某一特定功能的关键,对于维护人员来说,良好的注释帮助更好的理解程序,有时甚至优于看设计文档。
- 10 对于switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。
说明:这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。示例(注意斜体加粗部分):
switch ( cmd )
{
case CMD_DOWN:
ProcessDown();
break;
case CMD_FWD:
ProcessFwd();
if (...)
{
...
break;
}
else
{
ProcessCFW_B(); /*now jump into case CMD_A*/
}
case CMD_A:
ProcessA();
break;
default:
break;
}
- 11 注释格式尽量统一,建议使用“/……/”,因为C++注释“//”并不被所有C编译器支持。
- 12 注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能非常流利准确的用英文表达。
说明:注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。
4 标识符命名
- 1 标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解。
说明:较短的单词可通过去掉“元音”形成缩写;较长的单词可取单词的头几个字母形成缩写;一些单词有大家公认的缩写。
示例:如下单词的缩写能够被大家基本认可。
temp可缩写为 tmp;
flag可缩写为 flg;
statistic可缩写为 stat;
increment可缩写为 inc;
message可缩写为 msg;
2 命名中若使用特殊约定或缩写,则要有注释说明。
说明:应该在源文件的开始之处,对文件中所使用的缩写或约定,特别是特殊的缩写,进行必要的注释说明。3 自己特有的命名风格,要自始至终保持一致,不可来回变化。
说明:个人的命名风格,在符合所在项目组或产品组的命名规则的前提下,才可使用。(即命名规则中没有规定到的地方才可有个人命名风格)。4 对于变量命名,禁止取单个字符(如i、j、k...),建议除了要有具体含义外,还能表明其变量类型、数据类型等,但i、j、k作局部循环变量是允许的。
5 命名规范采用小驼峰命名法
18 除非必要,不要用数字或较奇怪的字符来定义标识符。
19 除了编译开关/头文件等特殊应用,应避免使用
_EXAMPLE
、_TEST_
之类以下划线开始和结尾的定义。
5 可读性
- 1 注意运算符的优先级,并用括号明确表达式的操作顺序,避免使用默认优先级。
表达式正确的写法示例:
word = ( high << 8 ) | low;
if ( ( a | b ) && ( a & c ) )
if( ( a | b ) < ( c & d ) )
错误的写法示例:
word = high << 8 | low;
if ( a | b && a & c )
if ( a | b < c & d ) /* 造成了判断条件出错 */
- 2 避免使用不易理解的数字,用有意义的标识来替代。涉及物理状态或者含有物理意义的常量,不应直接使用数字,必须用有意义的枚举或宏来代替。
示例:如下的程序可读性差。
if ( trunk[index].trunkState == 0 )<----不规范的写法,应使用有意义的标识
{
trunk[index].trunkState=1;<----不规范的写法,应使用有意义的标识.../*programcode*/
}
应改为如下形式。
enum trunkState_e
{
TRUNK_IDLE=0,
TRUNK_BUSY=1
};
if ( trunk[index].trunkState == TRUNK_IDLE )
{
trunk[index].trunkState=TRUNK_BUSY;
//.../*programcode*/
}
- 3 不要使用难懂的技巧性很高的语句,除非很有必要时。
说明:高技巧语句不等于高效率的程序,实际上程序的效率关键在于算法。示例:如下表达式,考虑不周就可能出问题,也较难理解。
*stat_poi+++ = 1;
*++stat_poi += 1;
应分别改为如下。
*stat_poi += 1;
stat_poi++; /* 此二语句功能相当于 *stat_poi+++ = 1; */
++stat_poi;
*stat_poi += 1; /* 此二语句功能相当于*++stat_poi += 1; */
6 变量、结构、常量、宏
- 1 变量定义
变量按作用空间可以分为全局变量与局部变量(临时变量)静态变量,其空前前缀为g_,t_,s_;
变量按类型可以分为逻辑型、字符型、整型、长整型、字符串、浮点数、结构类型、指针,其类型前缀为b,c,I,g,s,f,str,p。
规范要求
变量的命名采用空间前缀+类型前缀+驼峰法;举例
uchar g_cMeterAddr; //全局变量,单字节,表地址
uchar g_iTime; //全局变量,双字节,时间
uchar t_cLength; //局部变量,单字节,长度
uchar g_bKeyInputOver; //全局变量,逻辑变量,键盘输入结束
uchar g_sCommBuff[5]; //全局变量,字符串,通讯缓冲
uchar t_fValue; //局部变量,浮点数,数据
uchar g_strDCBTbl; //全局变量,结构类型,表
uchar* g_pCommBuff; //全局变量,指针,通讯缓冲
- (1) 为了方便书写及记忆,变量类型需要采用类似如下重定义:
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned long int uint32_t;
typedef signed char int8_t;
typedef signed short int16_t;
typedef signed long int int32_t;
#define __IO volatile
- (2) 常用类型与前缀
对于一些常见类型的变量,应在其名字前标注表示其类型的前缀。前缀用小写字母表示。前缀的使用请参照下列表格中说明
变量类型 | 前缀 | 举例 |
---|---|---|
uint8_t | uc | ucSum |
int8_t | c | cSum |
uint16_t | us | usParaWord |
int16_t | s | sParaWord |
uint32_t | ul | ulParaWord |
int32_t | l | lParaWord |
uint8_t * | ucp | ucpWrite |
int8_t * | cp | cpWrite |
uint16_t* | usp | uspWrite |
int16_t* | sp | spWrite |
uint32_t * | ulp | ulpWrite |
int32_t * | lp | lpWrite |
uint8_t[] | uca | ucaNum[5] |
int8_t[] | ca | caNum[5] |
uint16_t[] | usa | usaNum[5] |
int16_t[] | sa | saNum[5] |
uint32_t[] | ula | ulaNum[5] |
int32_t[] | la | laNum[5] |
结构体 | t | tPara |
- (3) 对于几种变量类型组合,前缀可以迭加。
- 2 变量作用域的前缀
为了清晰的标识变量的作用域,减少发生命名冲突,应该在变量类型前缀之前再加上表示变量作用域的前缀,并在变量类型前缀和变量作用域前缀之间用下划线‘-’隔开。具体的规则如下:
- (1) 对于全局变量(global variable),在其名称前加“g”和变量类型符号前缀。
uint32_t g_ulParaWord;
uint8_t g_ucByte;
- (2) 对于静态变量(static variable),在其名称前加“s”和变量类型符号前缀。
static uint32_t s_ulParaWord;
static uint8_t s_ucByte;
(3) 函数内部等局部变量前不加作用域前缀。
(4) 对于常量,当可能发生作用域和名字冲突问题时,以上几条规则对于常量同样适用。注意,虽然常量名的核心部分全部大写,但此时常量的前缀仍然用小写字母,以保持前缀的一致性。
3 对于结构体命名类型,表示类型的名字,所有名字以小写字母“tag”开头,之后每个英文单词的第一个字母大写(包括第一个单词的第一个字母),其他字母小写,结尾_t标识。单词之间不使用下划线分隔,结构体变量以t开头。
/*结构体命名类型名*/
typedef struct tagBillQuery_t
{
//...
}billQuery_t;
/*结构体变量定义*/
billQuery_t tBillQuery;
- 4 对于枚举定义全部采用大写,结尾_e标识。
typedef enum
{
KB_F1=0, /* F1键代码 */
KB_F2, /* F2键代码 */
KB_F3 /* F3键代码 */
}keyCODE_e;
- 5 常量、宏、模版的名字应该全部大写。如果这些名字由多个单词组成,则单词之间用下划线分隔。宏指所有用宏形式定义的名字,包括常量类和函数类;常量也包括枚举中的常量成员。
#define LOG_BUF_SIZE 8000
- 6 不推荐使用位域。
6 函数
- 1 函数的命名规则。每一个函数名前缀需包含模块名,模块名为小写,与函数名区别开。
如:uartReceive(串口接收)
备注:对于非常简单的程序,可以不加模块名。
- 2 函数的的形参都以下划线
a_
开头,已示与普通变量进行区分。
/******************************************************************************
* 函数名:uartConvUartBaud
*功能:波特率转换
* 输入:_ulBaud:波特率
*输出:无
* 返回:uint32-转换后的波特率值
*/
uint32_t uartConvUartBaud( uint32_t a_ulBaud )
{
uint32_t ulBaud;
ulBaud = a_ulBaud * 2; /*计算波特率*/
//......
return ulBaud;
}
3 一个函数仅完成一件功能。
4 函数名应准确描述函数的功能。避免使用无意义或含义不清的动词为函数命名。使用动宾词组为执行某操作的函数命名。
说明:避免用含义不清的动词如process、handle等为函数命名,因为这些动词并没有说明要具体做什么。
示例:参照如下方式命名函数。
void printRecord( uint32_t recInd );
int32_t inputRecord( void );
uint8_t getCurrentColor( void );
- 5 检查函数所有参数输入的有效性。
说明:如果约定由调用方检查参数输入,则应使用assert()之类的宏,来验证所有参数输入的有效性。
-6 检查函数所有非参数输入的有效性,如数据文件、公共变量等。
说明:函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入之前,应进行必要的检查。
- 7 防止将函数的参数作为工作变量。
说明:将函数的参数作为工作变量,有可能错误地改变参数内容,所以很危险。对必须改变的参数,最好先用局部变量代之,最后再将该局部变量的内容赋给该参数。
- 8 避免设计五个以上参数函数,不使用的参数从接口中去掉。
说明:目的减少函数间接口的复杂度,复杂的参数可以使用结构传递。
- 9 在调用函数填写参数时,应尽量减少没有必要的默认数据类型转换或强制数据类型转换。
说明:因为数据类型转换或多或少存在危险。
- 10 避免使用BOOL参数。
说明:原因有二,其一是BOOL参数值无意义,TURE/FALSE的含义是非常模糊的,在调用时很难知道该参数到底传达的是什么意思;其二是BOOL参数值不利于扩充。还有NULL也是一个无意义的单词。
11 函数的返回值要清楚、明了。除非必要,最好不要把与函数返回值类型不同的变量,以编译系统默认的转换方式或强制的转换方式作为返回值返回。
12 防止把没有关联的语句放到一个函数中。
说明:防止函数或过程内出现随机内聚。随机内聚是指将没有关联或关联很弱的语句放到同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,同时也使函数或过程的功能不明确。使用随机内聚函数,常常容易出现在一种应用场合需要改进此函数,而另一种应用场合又不允许这种改进,从而陷入困境。在编程时,经常遇到在不同函数中使用相同的代码,许多开发人员都愿把这些代码提出来,并构成一个新函数。若这些代码关联较大并且是完成一个功能的,那么这种构造是合理的,否则这种构造将产生随机内聚的函数。示例:如下函数就是一种随机内聚。
void initVar( void )
{
rect.length = 0;
rect.width = 0; /*初始化矩形的长与宽*/
point.x = 10;
point.y = 10; /*初始化“点”的坐标*/
}
矩形的长、宽与点的坐标基本没有任何关系,故以上函数是随机内聚。应如下分为两个函数:
void initRect( void )
{
rect.length = 0;
rect.width = 0; /*初始化矩形的长与宽*/
}
void initPoint( void )
{
point.x = 10;
point.y = 10; /*初始化“点”的坐标*/
}
- 13 减少函数本身或函数间的递归调用。
说明:递归调用特别是函数间的递归调用(如A->B->C->A),影响程序的可理解性;递归调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。故除非为某些算法或功能的实现方便,应减少没必要的递归调用。
14 改进模块中函数的结构,降低函数间的耦合度,并提高函数的独立性以及代码可读性、效率和可维护性。优化函数结构时,要遵守以下原则:
(1) 能影响模块功能的实现。
(2) 仔细考查模块或函数出错处理及模块的性能要求并进行完善。
(3) 通过分解或合并函数来改进软件结构。
(4) 考查函数的规模,过大的要进行分解。
(5) 降低函数间接口的复杂度。
(6) 不同层次的函数调用要有较合理的扇入、扇出。
(7) 函数功能应可预测。
(8) 提高函数内聚。(单一功能的函数内聚最高)
说明:对初步划分后的函数结构应进行改进、优化,使之更为合理。