工作中养成良好的代码编写规范
一、文件结构
每个 C++/C 程序通常分为两个文件。一个文件用于保存程序的声明(declaration),
称为头文件。另一个文件用于保存程序的实现(implementation),称为定义(definition)
文件。
C++/C 程序的头文件以“.h”为后缀,C 程序的定义文件以“.c”为后缀,C++程序
的定义文件通常以“.cpp”为后缀(也有一些系统以“.cc”或“.cxx”为后缀)。
1、版权和版本的说明
版权和版本的声明位于头文件和定义文件的开头(参见示例 1-1),每个源文件都应该有文件头说明,主要内容有:
(1)版权信息。
(2)文件名称,摘要。
(3)当前版本号,作者/修改者,完成日期。
(4)版本历史信息。
示例1-1 版权和版本的声明
/*
* Copyright (c) 2021,Shenzhen xxxxxx Co.,Ltd
* All rights reserved.
*
* Filename:filename.h
* Description:简要描述本文件的内容
*
* Version:1.1
* Author:作者(或修改者)名字
* Date:2021.07.01
*
* Version:1.0
* Author:原作者(或修改者)名字
* Date:2021.05.10
*/
2、头文件的结构
头文件由三部分内容组成:
(1)头文件开头处的版权和版本声明(参见示例 1-1)。
(2)预处理块。
(3)函数和类结构声明等。
假设头文件名称为 graphics.h,头文件的结构参见示例 1-2。为了防止头文件被重复引用,应当用 ifndef/define/endif 结构产生预处理块;用 #include <filename.h> 格式来引用标准库的头文件,用#include"filename.h"格式来引用非标准库的头文件;头文件中只存放 “声明” 不存放 “定义”,将成员函数的定义与声明分开;不提倡使用全局变量,尽量不要在头文件中出现像 extern int Value这类声明。
示例 1-2 C++/C 头文件的结构
// 版权和版本声明见示例 1-1,此处省略。
#ifndef _GRAPHICS_H _// 防止 graphics.h 被重复引用
#define _GRAPHICS_H_
#include <math.h> // 引用标准库的头文件
…
#include “myheader.h” // 引用非标准库的头文件
…
void Function1(…); // 全局函数声明
…
class Box // 类结构声明
{
…
};
#endif
3、定义文件的结构
定义文件有三部分内容:
(1) 定义文件开头处的版权和版本声明(参见示例 1-1)。
(2) 对一些头文件的引用。
(3) 程序的实现体(包括数据和代码)。
假设定义文件的名称为 graphics.cpp,定义文件的结构参见示例 1-3。如果一个软件的头文件数目比较多(如超过十个),通常应将头文件和定义文件分别保存于不同的目录,以便于维护,例如可将头文件保存于 include 目录,将定义文件保存于 source 目录(可以是多级目录);如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其声明,为了加强信息隐藏,这些私有的头文件需要和定义文件存放于同一目录下。
示例 1-3 C++/C 定义文件的结构
// 版权和版本声明见示例 1-1,此处省略。
#include “graphics.h” // 引用头文件
…
// 全局函数的实现体
void Function1(…)
{
…
}
// 类成员函数的实现体
void Box::Draw(…)
{
…
}
二、程序的版式
版式虽然不会影响程序的功能,但会影响可读性。程序的版式追求清晰、美观,是程序风格的重要构成因素。
1、空行
空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。在每个类声明之后、每个函数定义结束之后都要加空行。参见示例 2-1(a)。在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。参见示例 2-1(b)。
示例 2-1(a) 函数之间的空行
// 空行
void Function1(…)
{
…
}
// 空行
void Function2(…)
{
…
}
// 空行
void Function3(…)
{
…
}
示例 2-1(b) 函数内部的空行
// 空行
while (condition)
{
statement1;
// 空行
if (condition)
{
statement2;
}
else
{
statement3;
}
// 空行
statement4;
}
2、代码行
一行代码只做一件事情,如只定义一个变量,或只写一条语句;if、for、while、do 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{},这样可以防止书写失误。在定义变量的同时初始化该变量(就近原则),如果变量的引用处和其定义处相隔比较远,变量的初始化很容易被忘记。如果引用了未被初始化的变量,可能会导致程序错误。本建议可以减少隐患。例如
int width = 10; // 定义并初绐化 width
int height = 10; // 定义并初绐化 height
int depth = 10; // 定义并初绐化 depth示例 2-2(a)为风格良好的代码行
//示例 2-2(a)
int width; // 宽度
int height; // 高度
int depth; // 深度
x = a + b;
y = c + d;
z = e + f;
if (width < height)
{
dosomething();
}
for (initialization; condition; update)
{
dosomething();
}
// 空行
other();
3、空格与缩进
- 关键字之后要留空格。象 const、virtual、inline、case 等关键字之后
至少要留一个空格,否则无法辨析关键字。 - 函数名之后不要留空格,紧跟左括号‘(’。
- ‘(’向后紧跟,‘)’、‘,’、‘;’向前紧跟,紧跟处不留空格
- ‘,’之后要留空格,如 Function(x, y, z)。如果‘;’不是一行的结束符号,其后要留空格,如 for (initialization; condition; update)。
- 赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。
- 一元操作符如“!”、“~”、“++”、“--”、“&”(地址运算符)等前后不加空格。
- “[]”、“.”、“->”这类操作符前后不加空格。
- 对于表达式比较长的 for 语句和 if 语句,为了紧凑起见可以适当地去掉一些空格,如 for (i=0; i<10; i++)和 if ((a<=b) && (c<=d))
- 每行缩进 4 个空格。如果地位相等,则不需要缩进;如果属于某一个代码的内部代码,
就需要缩进
4、对齐
程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用它们的语句左对齐;{ }之内的代码块在 ‘{’ 右边数格处左对齐;
5、长行拆分
代码行最大长度宜控制在 70 至 80 个字符以内,代码行不要过长;长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
6、修饰符位置
应当将修饰符 * 和 & 紧靠变量名,例如:
char *name;
int *x, y; // 此处 y 不会被误解为指针。
7、注释
C 语言的注释符为“/*…*/”。C++语言中,程序块的注释常采用“/*…*/”,行注释一
般采用“//…”。注释通常用于:
(1)版本、版权声明;
(2)函数接口说明;
(3)重要的代码行或段落提示。
虽然注释有助于理解代码,但注意不可过多地使用注释。注释是对代码的“提示”,而不是文档。程序中的注释不可喧宾夺主,注释太多了会让人眼花缭乱,注释的花样要少;边写代码边注释,修改代码同时修改相应的注释,尤其是对参数、返回值、异常、核心的逻辑等修改,以保证注释与代码的一致性,不再有用的注释要删除;注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方,尽可能使用右方(尾部)注释;当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读;及时清理不再使用的代码段或配置信息,避免程序过度臃肿、代码冗余,谨慎注释掉代码,要在上方详细说明,而不是简单地注释掉,如果无用则删除;典型的算法前都要有注释,尾部注释使用“//”、头部使用块注释“/* */”。
三、命名规则
标识符应当直观且可以拼读,可望文知意,不必进行“解码”。标识符采用英文单词或其组合,便于记忆和阅读,切忌使用汉语拼音来命名。程序中的英文单词一般不会太复杂,用词应当准确,例如不要把 CurrentValue 写成 NowValue。
标识符的长度应当符合“min-length && max-information”原则。几十年前老 ANSI C 规定名字不准超过 6 个字符,现今的 C++/C 不再有此限制。一般来说,长名字能更好地表达含义,所以函数名、变量名、类名长达十几个字符不足为怪。单字符的名字也是有用的,常见的如 i,j,k,m,n,x,y,z 等,它们通常可用作函数内的局部变量。
太长的单词可以使用缩写或者助记符形式,基本规则是取其中的 3-4 个字母,具体是:
(1)首尾两个字母,中间取每个音节的首个字母(1-2 个);
(2)或者取最前面的 3-4 个字母;
(3)一般不取元音字母;
(4)如果已经有了公认的缩写形式,则以公认的形式为准,不受上面 3 条规则限制。
单词 | 缩写 | 单词 | 缩写 |
Argument | Arg | Buffer | Buf |
Clear | Clr | Clock | Clk |
Compare | Cmp | Configuration | Cfg |
Context | Ctx | Control | Ctl |
Delay | Dly | Device | Dev |
Disable | Dis | Display | Disp |
Enable | En | Error | Err |
Function | Fn | Hexadecimal | Hex |
High Priority Task | HPT | I/O System | IOS |
Initialize | Init | Mailbox | Mbox |
Manager | Mgr | Manual | Man |
Maximum | Max | Message | Msg |
Minimum | Min | Multiplex | Mux |
Operation System | OS | Overflow | Ovf |
Parameter | Param | Pointer | Ptr |
Previous | Prev | Priority | Prio |
Read | Rd | Ready | Rdy |
Register | Reg | Schedule | Sched |
Semaphore | Sem | Stack | Stk |
Synchronize | Sync | Timer | Tmr |
Trigger | Trig | Write | Wr |
标识符应采用驼峰规则,即“大小写”混排的方式,所有单词首字母都要大写,如 AddChild。程序中不要出现仅靠大小写区分的相似的标识符。程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解。变量的名字应当使用“名词”或者“形容词+名词”。全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。宏全用大写的字母,用下划线分割单词。
四、运算符的优先级
1、运算符
如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。成对的符号一定要成对书写,如()、{}等,以免漏掉右括号。
2、表达式
不要编写太复杂的复合表达式。不要有多用途的复合表达式。不要把程序中的复合表达式与“真正的数学表达式”混淆。复杂的表达式需提取出去,做成中间变量。避免使用自增减表达式。可以使用宏来定义符号常量,但应尽量减少用宏来定义表达式,定义表达式时,要使用完备的括号。
3、if语句
不可将布尔变量直接与 TRUE、FALSE 或者 1、0 进行比较。根据布尔类型的语义,零值为“假”(记为 FALSE),任何非零值都是“真”(记为TRUE)。
假设布尔变量名字为 flag,它与零值比较的标准 if 语句如下:
if (flag) // 表示 flag 为真
if (!flag) // 表示 flag 为假
应当将整型变量用“==”或“!=”直接与 0 比较。假设整型变量的名字为 value,它与零值比较的标准 if 语句如下:
if (value == 0)
if (value != 0)
不可将浮点变量用“==”或“!=”与任何数字比较,应该设法转化成“>=”或“<=”形式。假设浮点变量的名字为 x,应当将
if (x == 0.0) // 隐含错误的比较
转化为
if ((x>=-EPSINON) && (x<=EPSINON))
其中 EPSINON 是允许的误差(即精度)。
应当将指针变量用“==”或“!=”与 NULL 比较。需采用 if (NULL == p) 这样的格式、而非 if(p == NULL)格式。
避免采用取反逻辑运算符。取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑写法。
4、循环语句的效率
在多重循环中,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。
5、for语句的循环控制变量
不可在 for 循环体内修改循环变量,防止 for 循环失去控制。for 语句的循环控制变量的取值采用“半开半闭区间”写法,即 0 <= i < n; 而非 0 <= i <= n-1;
6、switch语句
每个 case 语句的结尾不要忘了加 break。不要忘记最后那个 default 分支。即使程序真的不需要 default 处理,也应该保留语句 default : break;
7、goto语句
主张少用、慎用 goto 语句,最好不用。
五、常量
常量是一种标识符,它的值在运行期间恒定不变。C 语言用 #define (称为宏常量)和 const (称为 const 常量)来定义常量。
1、常量的使用
尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。
2、const 与 #define比较
C 语言可以用 const 来定义常量,也可以用 #define 来定义常量。但是前者比后者有更多的优点:
(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
(2) 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
3、常量定义规则
需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。例如:
const float cnfRadius = 100.0f;
const float cnfDiameter = cnfRadius * 2;
六、函数设计
函数接口的两个要素是参数和返回值。C 语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。
1、函数头注释
每个函数都要有函数头注释。注释内容包含功能说明(brief)、函数名(function name)、参数说明(param)、返回值(return)等。
/*!
*@brief Initialises an i2c device and its control structure.
* function name: sk_hdi_i2c_init
* @param: pInitParas: A pointer to an initialisation parameter block
* @return SK _NO_ERROR: no errors
*/
2、参数的规则
参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用 void 填充。参数命名要恰当,顺序要合理。参数的顺序应将目的参数放在前面,源参数放在后面。如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该指针在函数体内被意外修改。避免函数有太多的参数,参数个数尽量控制在 5 个以内。不要使用类型和数目不确定的参数。不要出现与业务无关的输入参数。应明确规定对接口函数参数的合法性检查由函数的调用者、还是由接口函数本身负责。缺省是由函数编写者负责,除非函数内部无法判断参数的合法性(比如指针所指向的空间大小,这时调用者必须保证空间足够)。
非调度函数应减少或限制控制参数,尽量只使用数据参数。此规则的目的是防止函数间的控制耦合。调度函数是指根据输入的消息类型或控制命令,来启动相应的函数,而本身并不完成具体功能。控制参数是指改变函数功能行为的参数,即函数要根据此参数来决定具体怎么工作。非调度函数的控制参数增加了函数间的控制耦合,很可能使函数间的耦合度增大,并使函数的功能不唯一。
如下函数构造不太合理:
int add_sub(int a, int b, unsigned char add_sub_flg)
{
if (add_sub_flg == INTEGER_ADD)
{
return (a + b);
}
else
{
return (a - b);
}
}
更好的做法是分成两个函数:
int add(int a, int b)
{
return (a + b);
}
int sub(int a, int b)
{
return (a - b);
}
在调用函数填写参数时,应尽量减少没有必要的默认数据类型转换或强制数据类型转换,因为数据类型转换或多或少存在危险(建议:将编译器的所有警告开关全部打开,让编译器帮助发现此类危险)。
3、返回值的规则
不要省略返回值的类型。规定任何 C++/ C 函数都必须有类型。如果函数没有返回值,那么应声明为 void 类型。返回应该返回的数据。函数名字与返回值类型在语义上不可冲突。不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用 return 语句返回。
函数除了有正常的返回值,也应该有表示出错情况的返回值。如果二者不可兼得,应该使用异常机制。有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。
4、函数内部实现的规则
不同功能的函数其内部实现各不相同,看起来似乎无法就“内部实现”达成一致的观点,但可以在函数体的“入口处”和“出口处”从严把关,从而提高函数的质量。
在函数体的“入口处”,对参数的有效性进行检查。在函数体的“出口处”,对 return 语句的正确性和效率进行检查。
注意事项如下:
(1)return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
(2)要搞清楚返回的究竟是“值”、“指针”还是“引用”。
(3)如果函数返回值是一个对象,要考虑 return 语句的效率。例如 return String(s1 + s2); 我们不要将
return int(x + y); // 创建一个临时变量并返回它
写成
int temp = x + y;
return temp;
5、其他规则
- 函数的功能要单一,不要设计多用途的函数。一个复杂的功能可由多个功能单一的函数实现。
- 函数体的规模要小,尽量控制在 100 行代码之内(不包含注释和空行)。
- 尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。
- 不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。
- 用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。
- 循环、分支层次不要超过 5 层。
- 共用代码应提取出来定义成独立的工具类函数。
- 复杂的逻辑需提取出去做成“帮助函数”。
- 将可能的变化封装成函数。
- 明确函数功能,精确(而不是近似)地实现函数设计。
- 尽量不使用递归函数,尽可能从算法上消除递归,能用迭代的、坚决不能使用递归。如果不能消除递归,则用自行管理堆栈的形式来消除形式上的递归。
- 不要编写依赖于其他函数内部实现的函数,此为函数独立性的基本要求。
- 功能不明确较小的函数,特别是仅有一个上级函数调用它时,应考虑把它合并到上级函数中、而不必单独存在。
- 对于所调用函数的错误返回码要仔细、全面的处理。
6、使用断言
- 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
- 在函数的入口处,使用断言检查参数的有效性(合法性)。
- 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
- 防错设计的编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。
- 正式的软件产品中应将断言及其他调试代码去掉(即将所有调试开关关掉)。
七、指针与数组
- 避免数组或指针的下标越界,特别要当心发生“多 1”或者“少 1”操作。
- 使用指针前需先判断指针是否为 NULL。
- 用 free 或 delete 释放了内存之后,立即将指针设置为 NULL,防止产生“野指针”。
- 常量字符串的内容是不可以被修改的。
- 不能对数组名进行直接复制与比较。
- 如果函数的参数是一个指针,不要指望用该指针去申请动态内存。
- 如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”。
- 指针变量在创建的同时应当被初始化,要么将指针设置为 NULL,要么让它指向合法的内存。
- 指针操作不能超越了变量的作用范围。
八、其他规则
- 不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。
- 以提高程序的全局效率为主,提高局部效率为辅。
- 在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关紧要之处优化
- 先优化数据结构和算法,再优化执行代码。
- 有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷。例如多花费一些内存来提高性能、或者以空间换效率。
- 不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码。
- 当心那些视觉上不易分辨的操作符发生书写错误。
我们经常会把“==”误写成“=”,像“||”、“&&”、“<=”、“>=”这类符号也很容易发生“丢 1”失误。然而编译器却不一定能自动指出这类错误。 - 变量(指针、数组)被创建之后应当及时把它们初始化,以防止把未被初始化的变量当成右值使用。
- 当心变量的初值、缺省值错误,或者精度不够。
- 当心数据类型转换发生错误。尽量使用显式的数据类型转换(让人们知道发生了什么事),避免让编译器轻悄悄地进行隐式的数据类型转换。
- 当心变量发生上溢或下溢,数组的下标越界
- 当心忘记编写错误处理程序,当心错误处理程序本身有误
- 当心文件 I/O 有错误
- 避免编写技巧性很高代码
- 不要设计面面俱到、非常灵活的数据结构
- 如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的代码,应当重新编写
- 尽量使用标准库函数和公共函数,不要“发明”已经存在的库函数
- 尽量不要使用与具体硬件或软件环境关系密切的变量
- 把编译器的选择项设置为最严格状态
- 如果可能的话,使用 PC-Lint、LogiScope 等工具进行代码审查。
- 可读性第一、效率第二。
- 先将眼前的问题解决掉、解决好,再考虑将来的扩展问题
- 先写出可用的代码,反复推敲,再考虑是否需要重用的问题。
- 先写出可用、简单、明显没有 bug 的代码,再考虑测试的问题
- 高内聚、低耦合。
- 仅引用需要的头文件
- 系统运行之处,要初始化有关变量及运行环境,防止未经初始化的变量被引用
九、总结
以上为目前工作中总结出的编码规范,后续会不断完善,如有问题欢迎指正与补充