一 排版
1,关系密切的逻辑语句要尽量集中在一起
2,缩进:if else case for while语句需要缩进;case语句与所属的switch语句对齐;所有{}需要缩进,extern "C", namespace 块 除外,case语句除外;
3,空格:关键字 if else switch case for while 之后要加空格;如果 ,;后面没有立即换行, 即后面有变量或语句时, 要在后面加空格; 小括号内侧不能有空格, 函数调用(或宏)的名字与括号之间不能有空格;一元操作符 & * + - ~ ! ++ -- 要紧贴对应的变量,不能有空格; 二元操作符 = + - * / % & | ^ == != >= <= > < ? : 两侧要加空格;结构体成员操作符 . -> 前后不加空格
4,一行只写一条语句,不允许把多个短语句写在一行中
5,大括号问题:}必须独占一行;{可以独占一行,且与上一语句的起始位置对齐;{,}的相对位置在整个模块中必须保持一致。
6,每行代码不应超过80列,如果某些行需要超出80列应该折成多行显示。
二 注释
1,声明注释:
1)文件头:在头文件(*.h,*.hpp,*.inc等)和源文件头部应注释说明该其功能;
2) 函数头:函数头部应注释说明其功能及各参数、返回值的含义(无参构造函数、析构函数、重载的运算符函数可无需注释);
3) 全局变量:全局变量应注释说明其功能;
4) 常量: 所有常量定义都应注释说明其功能;
5) 类型:所有类型定义(包括struct,class,enum,union),都应注释说明其功能;
6) 宏定义:所有宏定义应注释说明其功能,如果宏有参数,必须说明参数的用法;
2,语句注释:
1) if语句的各个分支,注释说明条件和具体功能;
2) for/while/do的头部,注释说明循环条件和具体功能;
3) switch头部,注释说明判断条件和具体功能;
3,确定不使用的功能代码要删除,或通过注释或者
if 0关闭
三 标识符
1,命名风格:
1) UNIX的全小写加下划线的风格,如:create_file;
2) 匈牙利命名法(大小写混排),如:CreateFile;
3) Java风格,如 createFile
2,命名要求:
1) 不使用拼音;
2) 不使用无意义的字母组合;
3) 除循环变量可使用i、j、k,指针变量可使用p之外,不使用单字符的名字;
4) 不使用下划线开头;
5) 非静态全局变量使用g_开头;
6) 静态全局变量使用s_开头;
7) 类成员变量使用m_开头,等同于C结构体的(没成员函数的)可以例外,union可以例外;
8) 局部变量不加前缀;
3,使用include包含的文件名全部使用小写。Windows界面相关的代码文件允许例外。
4,魔数
不允许使用0,1,-1之外的魔数,有需要用到数字的地方,请用命名常量代替。如果作为标识(比如状态标识)时,0,1,-1也不允许直接使用。所谓魔数,指以字面值形式出现的数值常量(不包括字符串常量),比如3,-4,256,3.14,0.628。
5,变量名称要和用途匹配:
1) 变量的名字和实际用途相符,不使用和实际用途完全无关或相反的命名;
2) 在变量的作用域内,一个变量不用作多个用途;
3) 如果变量有多个取值范围,且各取值范围代表不同意思,须保证各个取值范围之间不得有重叠;
6,减小标识符的作用域和可见性:
1) 不允许在头文件中定义非静态全局变量,仅仅声明除外;
2) 不被别的编译单元访问的全局变量,必须声明为静态全局变量;
3) 不被别的编译单元访问的函数,必须加上static声明;
4) 不被别的类访问的成员函数,必须声明为private函数;
5) 只被派生类访问的成员函数,必须声明为protected函数;
7,函数声明:
1) 函数声明必须和函数定义的保持原型一致;
2) 引用其它模块或.c/.cpp文件提供的有外部链接特性的函数(extern函数), 应使用include头文件的方式引用其函数声明,不允许自行声明,不允许在.c/.cpp中直接声明;
3) 提供给.c文件使用的函数声明,必须放在extern "C" {}域内,并通过宏防止问题;
四 函数
1,一个函数不超过100行,工具自动生成的除外。对既有的第三方代码(比如内核代码)进行修改可例外(新增函数依然不得超过100行)。
2,函数参数:
1) 函数调用传递大对象(超过8个字节大小,或者其构造函数会分配资源)时不使用按值传递。返回值允许例外。
2) 不允许在函数参数中使用布尔类型(包括使用数值类型仿制的布尔类型)。如有需要用到这类标志性参数,可用枚举代替,或者分拆成多个函数实现,具体见示例。
3) 函数参数中不得定义数组参数,应使用指针代替数组。请注意:如果参数是数组的指针不违反本条款。如果数组定义没有指定数组长度,也可例外,如:int main(int argc, char *argv[])
4) 函数参数个数不超过5个(<=5)。
3,返回值:
1) 不返回本函数内定义的非静态局部变量的地址(包括以指针或引用形式返回)。也不允许通过出参或全局变量、类成员变量的方式返回;
2) 在linux平台,如果返回值是int/short/long,且用于标识出错,则统一使用<0表示出错,>=0表示成功。回调函数等有约束的函数可以例外(比如main的返回值);
3) 返回值为BOOL类型的函数,只允许返回TRUE和FALSE两种取值;
4) 在函数返回值中使用的函数指针,必须使用typedef定义的别名。
4,重复代码提炼成函数:超过5行(不算空白行、注释行、只有大括号的行)的重复代码应提炼成函数(特殊情况也可提炼为宏)。
五 宏定义
1,除非有特殊理由,否则宏使用全大写加下划线的方式命名。(有特殊理由的须注释说明原因)
2,括号使用:
1) 用宏定义表达式时,要使用完备的括号。保证该宏的所有可能的使用方法(即包含潜在的某些使用方法)都不会发生运算符优先级问题;
2) 如果宏定义中包含多条语句或者包含有if语句,须将这些语句放在大括号中,建议使用do{…}while(0)的形式;
3,使用宏时,不允许参数中出现会发生副作用(即会改变某些变量的值或程序的运行环境)的表达式。比如不允许使用MAX(++a, b+=2)。
4,如果能通过下述方式替代宏,且程序功能不会发生变化,则不允许使用宏:
(A)C++的常量定义或枚举;(注:C++常量和枚举都可用作编译期常量,用于数组长度定义等需要编译期常量的场合)
(B)函数或内联函数;(注:需要性能保证的多条语句可以封装成内联函数)
5,宏内部如果需要定义局部变量,必须防止该变量和上下文中的变量名字冲突,建议加上特殊前缀或后缀。
六 结构体
1,结构体对齐:
1) 保证结构体(C++类也一样)至少是4字节对齐的。长度大等于2的成员的偏移位置必须能被2整除,长度大等于4的成员的偏移位置必须能被4整除。
2) 需要跨进程传递的结构体,必须保证pack(1)和pack(4)编译后是一样的内存结构(即各成员的偏移是一样的)。如果结构体中含有64位(如double,long long)的数据成员,必须保证pack(1)和pack(8)编译后是一样的内存结构;有一个技巧可以较容易达到这个要求,即结构内成员按成员的对齐系数(alignment modulus)从大到小的顺序排列。
2,需要在异构平台上传递的结构,不直接使用随平台不一样而导致长度不一的数据类型,如:int,long,unsigned long;可使用在不同平台保持长度一致的各种类型别名,如:int32_t,U32,WORD。
3,变长结构体:
变长结构体,指那种含有元素个数可变的数组成员的结构体。
1) 长度可变的数组成员必须是结构体的最后一个成员;
2) 如果该数组成员的元素个数定义为0,则结构体大小并不包含该数组成员的大小;
3) 该结构体必须使用动态分配,并预留足够的内存空间(该结构体大小加上数组成员的大小);
4) 该结构体必须是POD(plain old data)类型;如果结构体不充当变长结构体使用(比如只访问结构体前面固定长度字段),则可以例外。
七 语句
1,括号使用:
1) 同时出现 &、^、| 这三种运算符中的任意两种(或&、^同时出现两次);
2) 同时出现位运算符(& ^ |)和比较运算符(< <= > >= == !=)
3) 同时出现&&和||;
4) 同时出现移位运算符(<< >>)和比较运算符(< <= > >= == !=)
5) 同时出现比较运算符中(< <= > >= == !=)的任意两种(或一种出现两次);
6) 同时出现位运算符(& ^ |)和逻辑运算符(&& ||);
7) 同时出现移位运算符(<< >>)和算术运算符(+ - * / %)
如果一个上述运算符没有同时出现在一个操作数(或高优先级表达式)的两边,可以不做要求。
2,goto使用限制:只允许在同一个块作用域内跳转,或者跳转到上层的块作用域。不得用于跳转到更深的块作用域或者其它平行的块作用域。不允许使用goto在switch的多个case语句/default语句之间跳转。
3,loop可以在循环体外进行的耗时计算不放入循环体中。
4,不使用过于复杂的表达式,如确实有必要这样写须注释说明该表达式的意思。鼓励把复杂表达式分拆成多句书写;
1) 所谓过于复杂的表达式,指一个运算数某一边的运算符个数大等于2个。单目运算符+,-,*,!,~,&,sizeof及括号[],()不计算在内。 如:*stat_poi ++ += 1;应拆分成*stat_poi += 1;++stat_poi;
2) ?:运算符不允许嵌套使用,如:ret = a < b ? (a < c ? a : c) : (b < c ? b : c);
5,switch/case语句:
1) 每个case语句必须以break语句(或continue/goto/return/longjmp/exit等流程转移语句)结束。如果不需要break,必须在末尾注释说明。 如果该case标签后没有任何处理语句可以例外。没有处理语句的多个case标签可以写在一行。
2) 每个switch语句都必须要有default标签。
6,控制结构(if/for/while/switch等)的嵌套:
1) 不使用过深的嵌套:循环嵌套不超过3层,总共不超过5层;
2) 如果if子句和else子句行数相差超过3行,须保证else子句比if子句长。 如果该条件语句有多个条件,可以例外(即存在else if子句)。
八 错误处理
1,参数合法性检查:
外部接口:指会被其它模块调用的函数;内部函数:指不会被其他模块调用的函数;
1) 外部接口(指函数)在使用参数前需检查参数合法性。不使用该参数或该参数的任何取值都是合法值不会引起程序异常可以例外。参数有效性检查不要用断言等只在调试版本生效的方法。应当保证在release版本下检查处理措施依然有效,在发现不合法的参数时执行错误处理(比如返回标识错误的值,抛出异常,执行错误处理流程),保证程序健壮性。
2) 所有内部函数的函数入口处必须通过断言检查所有参数的合法性。不使用该参数或该参数的所有取值都是合法值,可以不检查该参数。
3) 无论外部接口还是内部函数,发现参数异常都必须输出供错误诊断用的调试信息。断言本身会输出诊断信息,故使用断言检测异常无需额外打印调试信息。
2,数据合法性检查:从外部读取的数据必须检查合法性,不可直接使用,外部数据指通过文件、进程间通讯设施或界面所获取的输入。不使用该数据或该数据所有取值都是合法值可以例外。外部数据的检查不应使用断言等仅在调试版本生效的措施。发现数据不满足要求时,必须输出供错误诊断用的调试信息。
3,断言中禁止对变量赋值或改变变量的值,如assert (size++ > 100), 禁止调用有副作用的函数。有副作用的函数,指函数内会更改变量值、改变系统环境、进行IO。
4,返回值检查:
1) API:对于返回新分配资源(句柄或指针)或者返回出错标识(比如使用FALSE,负数,0指针标识失败)的系统API(包括标准库、WIN32 API、MFC、ATL、POSIX API,第三方库),必须检查并处理失败情况。
2) 内部函数:对于内部函数(非系统API),如果有返回值,且返回值会用于标识失败情况(比如使用FALSE,负数,0指针标识失败),必须检查并处理失败情况。
九 资源管理
1,防止泄露:
1) 在某函数内分配的资源必须在该函数内释放;
2) 如果函数分配了资源但不能马上释放,必须:
(A) 必须有机制供调用者获取新分配的资源(通过返回值、出参、全局变量、类变量等);
(B) 必须提供与之对应的能保证释放资源的函数(或者能通过delete,free等API直接释放),并在函数头的注释中说明负责释放的函数名称。
如果需要释放的资源是类的成员且负责释放的函数是析构函数,可不说明。
2,使用配套的资源释放函数释放资源:对于各类动态分配得到的资源,必须使用与之配套的释放函数释放。
3,避免重复释放:
1) 句柄或指针在资源释放结束后应置为无效值:
(A) delete/free后需要将指针置为0,不使用delete,free释放非堆内存;
(B) close关闭的文件描述符必须置为-1;
(C) fclose,pclose关闭的FILE指针必须置为0;
(D) CloseHandle关闭的句柄必须置为INVALID_HANDLE_VALUE;
(E) 其它函数释放的资源必须置为对应的无效标识;
例外情况:
(A) 析构函数中可以例外;
(B) 即将退出程序时可以例外;
(C) 如果是局部变量且马上退出函数可以例外;
(D) 如果释放后马上指向其它有效资源的可以例外;
这些例外情况需保证已释放资源不再被引用到。
2) 释放资源前需判断句柄/指针的有效性,保证已经置为无效值的指针或句柄不会重复释放。能保证资源处于有效状态的可以例外。如果释放函数本身能保证0指针或无效句柄释放的安全性,也可以例外,比如free(NULL)是安全的,无需if (!p) free(p)。
3)资源的分配和释放必须配对。即:
(A) 分配和释放应配对出现在同一个函数中,在父函数中分配的资源不应交给子函数释放,具体可参考注释;
(B) 一个分配不可对应多个释放操作;
(C) 如果封装了资源的分配,就应封装资源的释放,反过来也成立。这两个封装过的资源分配和释放操作应配对出现在同一个函数中。
4,对于标准输入、标准输出、标准错误输出这三个文件,如果有必要关闭的话,必须将其重新打开,
定向到空文件描述符(比如/dev/null)。
十 内存
1,变量初始化:
1) 对堆和栈上分配的变量、内存块进行了初始化;
例外情况:
(A) 定义处和第一次赋值处相隔不超过5行,这两处之间未访问过该变量,并且没有出现过任何访问了该变量的控制结构(如if/for/while/switch/do等)。
(B) 效率原因。这种情况须注释说明原因,且保证不出现读未初始化数据的问题。对于结构体、对象、字符串、数组等内存块,可以通过以下几种方法初始化:
(A) 使用初始化列表,如:char buf[BUFSIZE] = {0};
(B) 使用构造函数、拷贝构造函数进行初始化;
(C) 使用memset,bzero等可以将内存块设置初始值的函数将整个内存块设置为某个初值,或者使用memcpy等函数拷贝一块合法内存,保证整个内存块数据都为已初始化数据;
(D) 使用赋值运算符对结构体和对象类型初始化,如:
struct person customer;
customer = s_default_person;
(E) 使用strcpy等能保证该内存块是合法字符串(有'\0'结尾)的函数初始化;
(F) 使用分配时即初始化的函数(如calloc)进行内存分配;
(G) 使用其它自定义的函数,只要保证该数据块所有成员都已初始化为有效数据,或成为一个合法字符串(即'\0'之后的部分可以无需赋值);
2) 作为出参使用的指针参数,必须在返回之前进行赋值。如明确无需赋值,需注释说明。[2011-8-26 update]
3) 对于资源句柄(包括指针,下同),应初始化为一个无效标识值,或者通过分配得到的资源句柄,或者确定的可安全引用的资源句柄。
注意: 不要初始化为未获得授权的资源句柄,比如:把文件描述符直接初始化为0,或者将指针指向了不能安全访问的内存或变量(0除外)。
2,指针算术:
1) 结构体类型的指针如果要指向下一个结构体头部,只要对指针加1,而不是加结构体长度;
2) 如果结构体是不定长的结构体,应该将指针先转换成char*类型,然后加需要偏移的字节长度;
3,不使用memcmp(或者其它按位比较方法)比较两结构是否相等(C++类也一样)。确认安全的允许例外,但须在类的注释中说明。
4,禁止把字符串转成整数进行比较。
5,不可修改常量字符串。
6,字符串格式化:
1) 保证fprintf/sprintf/snprintf/printf参数的格式化控制符和实参的一致(gcc可以检查出部分此类问题)
2) 通过外部数据得到的字符串不直接作为格式化参数(系统配置文件中的日志信息可以例外)。
7,防止字符串缺结束符:通过进程间通讯措施(比如:mmap/read/recv/recvfrom/fread/copy_from_user等)读取的内存块,如块尾是一个字符串,需在末尾补'\0',避免写入端没有写入'\0'结束符导致错误。另有协定,可保证不出错误者例外,但应注释说明。
8,字符串长度计算:
1)不可直接假定字符串长度,需使用strlen计算得到,分配容纳字符串的缓冲区,必须给'\0'结束符预留空间;
2)不可通过sizeof计算常量指针指向的字符串的长度;作为特例:字面值常量的长度允许使用sizeof计算得到(sizeof计算得到的长度已经包含了'\0'结束符)。通过sizeof(pstr)计算字符串长度是错误的,通过sizeof("hello")-1计算字符串长度是允许的。
9,变量大小、偏移计算:
1)变量大小计算:计算变量大小,必须使用sizeof,不允许人为假定变量大小。只要可能,就应该测量变量的大小,而不是测量类型的大小。
2)成员偏移计算:计算结构内成员的偏移使用offsetof(或和该宏等价的措施),不许人为假定成员偏移。
10,关于alloca:
1)禁止使用C的变长数组;
2)禁止使用alloca/_alloca分配内存。建议使用std::vector或者malloc替代以上两种用法。
11,关于字符串转整数/浮点数:
不使用atoi,atol读取数字,除非对输入合法性没有要求的场合。所使用的读取函数必须保证数字不被截断、不丢失精度。建议:
(A) 有符号整数建议使用strtol读取;
(B) 无符号整数建议使用strtoul读取;
(C) 浮点数建议使用strtod读取;
(D) 有符号的64位整数建议使用strtoll读取;
(E) 无符号的64位整数建议使用strtoull读取;
十一 并发
1,信号处理:
长时间运行的linux程序必须处理信号,必须处理或忽略的信号有SIGTERM,SIGINT,SIGPIPE,SIGBUS,SIGSEGV,SIGABRT。
1) 对于SIGPIPE信号,需忽略,或者保证处理之后程序仍能正常运行;
2) 对于SIGBUS、SIGSEGV信号,应打印堆栈,保留现场信息供后续调试;
3) 对于SIGCHLD信号,应保持系统默认行为,即SIG_DFL。如果能保证后续不调用和waitpid相关的函数(如system,pclose),或者程序逻辑不依赖waitpid的返回值,可以例外。对于库代码,特别注意不要改变SIGCHLD的行为,以免影响库调用者的一些程序逻辑。
2,信号处理函数:
信号处理函数中不调用不可重入函数。不可重入的函数典型特征有:
(A) 内部使用了全局变量/静态变量,如:malloc,printf;
(B) 内部使用了可能导致死锁的机制,如:localtime,localtime_r;
(C) 调用了其它不可重入函数;
建议采用的信号处理方法:在信号处理函数中仅仅设置信号标识(或计数),在主循环中判断信号标识执行相应的信号处理。
3,不暴力终止线程:除非程序退出,否则不采用暴力方式终止线程。推荐通过在线程函数中return的方式结束线程,但不强制要求,确认安全时使用pthread_exit/ExitThread退出线程也可以。
4,子进程/子线程的后事处理:
1) 子进程终止后必须通过waitpid/wait等待结束,避免子进程成为僵尸进程;
2) 子线程必须使用pthread_join等待结束,或者使用pthread_detach使子线程成为detached状态,避免线程资源泄露;
5,互斥锁的使用:
1) 使用互斥锁之前,必须对互斥锁的结构体或对象进行初始化,或调用初始化函数;
2) 使用互斥锁之后,必须使用销毁函数对互斥锁的结构体或对象进行销毁。使用PTHREAD_MUTEX_INITIALIZER初始化的可以例外;
6,锁定区域内睡眠:在互斥锁锁定区域内不调用阻塞进程或者引发进程睡眠的系统调用,如果确实需要调用,须注释说明。
7,非递归锁的使用:
1)非递归锁不用于递归函数;
2)非递归锁的锁定区域内不调用其它使用相同锁的函数。
(linux下的pthread_mutex_t默认是非递归锁,windows的临界区是递归锁)
8,死锁:
1) 不解锁返回:锁定区域内不允许出现不解锁的返回(包括抛出异常)
2) 锁的相互等待:如果两段代码同时使用两把相同的锁,不允许出现相互等待的现象。
9,线程创建:
1) 失败处理:
创建线程必须判断并处理失败情况;
说明:windows下创建线程可通过_beginthread/_beginthreadex/CreateThread/AfxBeginThread/线程类,Linux下创建线程可通过pthread_create。[2010-10-26]
2) 启动时序控制:
不可假定线程的执行顺序(除非创建时使用了CREATE_SUSPENDED等控制线程执行顺序的标志),不可简单使用sleep/usleep等不可靠方法来控制线程的执行顺序。
3) Windows平台线程创建方法:
(A)在MFC中,创建界面线程使用AfxBeginThread或线程类,不使用_beginthread/_beginthreadex和CreateThread;
(B)其它情况创建线程使用_beginthreadex(MFC中创建工作线程或非MFC程序),不直接使用CreateThread;
10,需要同步的访问:
如果多个线程同时访问同一个变量,以下情况需要同步:
(A) 一个线程线程先读后写,另一个线程有写;
(B) 一个线程写,另一个线程写了再读;(这种情况应避免,可以读临时值)
如果多个线程同时访问多个相联系的变量,只要一个线程有写,整个访问区间都应该同步保护,防止数据结构不一致。此时对每个变量单独使用原子变量或者同步保护是不行的。
十二 危险的库特性
1,在以错误号标识错误类型的API调用和错误号获取代码之间,不允许出现其它可能影响错误号的代码。这些类型的API,包括Win32 API,socket API,标准C库函数。
2,对同一文件或标准io流,不混用这三种机制:
(A) 标准C库的文件IO(如: printf,fprintf,fseek,fgets);
(B) POSIX IO(如:open,ftruncate,lseek);
(C) C++ iostream(如:ostream, istream, fstream);
确定没有问题的可以例外,但需注释说明。
3,标准库(或posix)中存在一些历史遗留的不安全函数,这些函数标准库已经提供了对应的安全版本。对于这类函数,必须使用其安全版本,包括:
(A) 提供了后缀为_r的替代函数的不可重入函数:strtok,localtime,asctime,ctime,gmtime;
(B) 不判断输入长度的函数:gets;
十三 危险的语言特性
1,自增/自减运算:
表达式计算结果不能依赖于副作用计算发生的时机。
(A) 同一语句中不得对同一变量使用多次自增或自减运算符。
(B) 不允许在一个表达式中既对该变量赋值,又对该变量使用自增/自减运算符。
2,函数调用参数列表中,参数值的计算不得有顺序依赖性。比如:Call(a = b, ++a);Call(foo1(), foo2());其中foo1和foo2的执行顺序不同会造成不同结果。
3,不直接使用char类型的变量做数组的索引。char类型既可能是有符号的(值的范围:-128~127),也可能没符号(值的范围:0~255)。需要将char变量当成int等类型使用前,必须先将char类型转化为unsigned char类型。不使用getch,fgetc,getchar,getc返回的int型变量做数组索引(除非已经确定值大等于0)。
4,用作除数的变量需保证不为0。求余运算符的右操作数也需保证不为0。如果该变量来自不可信的输入(外部输入或者其它模块传递的参数),必须先判断是否为0,为0时不作为除数参与运算。
5,void*类型的指针和其它类型的指针之间必须使用强制转换。
6,移位运算的右操作数(即移动位数)必须大等于0并小于左操作数的位数。
十四 工具检查
1,所有C/C++代码须通过cppcheck检查,检查时须打开所有的检查选项。除可以明确是误报的以外,不允许出现任何BUG及风格问题。(第三方代码除外)
2,所有C/C++代码须通过C++test检查。除可以明确是误报的以外,不允许出现任何警告及错误(第三方代码除外)。扫描需使用公司预置选项,如果需要额外关闭某些检查选项,需提前取得RDM书面认可。