C++
编程规范
Steve McConnell
:
“始终站在代码阅读者和使用者的角度去编写和组织你的程序。
”
(一)
文件结构
头文件(
.h/.hpp
):程序声明
定义文件(
.cpp/.cxx
):程序实现
l
1.1
版权和版本的声明
头文件开始处应列出:版权说明、程序功能、作者、版本号、生成日期、
与其它文件的关系等。
l
1.2
头文件的结构
头文件
为了防止头文件被重复引用,用 ifndef/define/endif
结构包含预处理块。
用#include <filename.h>
引用标准库头文件。
用#include
“filename.h
”引用非标准头文件。
类的声明头文件中,尽量将构造函数、析构函数、主要的 public
函数、次要的public
函数、private
函数、private
数据成员按先后顺序分层声明。
头文件中只将头文件自己必需的 include
文件包含进来。
除非十分必要(如用内联函数提高效率),头文件中只存放“声明”而不存放“定义”
不提倡使用全局变量,尽量不在头文件中出现
“extern int value;
”这类声明。
l
1.3
定义文件的结构
cpp
定义文件中不要 include
无关的头文件。
类的 cpp
定义文件中各个函数的编排顺序为:构造函数、析构函数、主要公有成员函数、次要的公有成员函数、私有成员函数
(二)
程序板式
l
2.1
空行
分隔程序段落的作用,使程序布局更加清晰。空行不会浪费内存,空行的原则:相对独立的程序块之间、变量说明之后必须加空行。
在每个类声明之后、每个函数定义结束之后都要加空行。
在函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
l
2.2
代码行
不把多个短语句写在一行中,即一行只写一条语句,或只定义一个变量(说明:逻辑上紧密关联的变量放在一行声明)。这样的代码容易阅读,并且便于写注释。
if
、for
、do
、while
、case
、switch
、default
等语句自占一行,执行
语句不得紧跟其后。不论执行语句有多少都要加{}
。这样可以防止书写失误。
l
2.3
代码行内的空格
清晰性
.
关键字之后要留空格。象 const
、virtual
、inline
、case
等关键字之后至少要留一个空格,否则无法辨析关键字。象 if
、for
、while
等关键字之后应留一个空格再跟左括号‘(’,以突出关键字。
函数名之后不要留空格,紧跟左括号‘(’,以与关键字区别。
‘(’向后紧跟,‘)’、‘,’、‘;
’向前紧跟,紧跟处不留空格。
‘,’之后要留空格,如 Function(x, y, z)
。如果‘;
’不是一行的结束符号,其后要留空格,如 for (initialization; condition; update)
。
赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=
”、“+=
”
“>=
”、“<=
”、“+
”、“*
”、“%
”、“&&
”、“||
”、“<<
”,
“^
”等二元操作符的前后应当加空格。
一元操作符如“!
”、“~
”、“++
”、“--
”、“&
”(地址运算符)等前后不加空格。
象“[]”、“.
”、“->
”这类操作符前后不加空格。
对于表达式比较长的 for
语句和 if
语句,为了紧凑起见可以适当地去掉一些空格,如 for (i=0; i<10; i++)
和 if ((a<=b) && (c<=d))
l
2.4
对齐
程序的分界符‘{
’和‘}
’应独占一行并且位于同一列,同时与引用它们的语句左对齐。如果出现嵌套的{}
,则使用缩进对齐。(大多数开发工具已实现自动对齐)
{ }
之内的代码块在‘{
’右边数格处左对齐。
l
2.5
长行拆分
代码行不要过长,最大长度宜控制在 80
个字符以内。
长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
(三)
标识符命名
l
通用规则
Microsoft
“匈牙利”法,
标识符的命名要清晰、明了,有明确含义,可望文知义,
可读性。
l
变量名命名规范
变量的命名规范:范围描述+
类型描述+
含义描述。(注:常用的循环控制变量如 i, j, k
等不在此列)
l
其他标识符命名规范
类的名称以大写的“C
”为前缀,如 class CHtmlParser
。
结构体类型的命名以“_st
”为前缀
FILE
型指针变量以“fp
”为前缀,如 FILE *fpHtmlFile;
常量或宏定义全用大写,用下划线分割。如const int MAX_LENGTH = 100;
指针类型的变量应用“p
”进行标明。如上述的“char *m_pchTextBuf
”;若为指向指针的指针,则用两个 p
,如“char **ppchText;
”
程序中不要出现仅靠大小写区分的相似的标识符。
程序中不要出现名称完全相同的局部变量,尽管因两者的作用域不同而不会发生编译错误,但会使人误解,降低可读性。
用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
说明:下面是一些在软件中常用的反义词组。
add / remove begin / end create / destroy insert / delete
first / last get / set increment / decrement add / delete
lock / unlock open / close min / max old / new
start / stop next / previous source / target show / hide
send / receive source / destination cut / paste up / down
尽量避免名字中出现数字编号,如 Value1,Value2
等
类名和函数名用大写字母开头的单词组合而成。而变量和参数则用小写字母开头的单词组合而成。
(四)
代码注释
注释就是最好的文档,最有助于对程序的阅读理解。准确、易懂、简洁。
/*…*/
和
//
。
一般情况下,源程序有效注释量应在 20
%以上,但不宜太多造成喧宾夺
主。如果代码本来就是清楚的,则不必加注释。
边写代码边注释,修改代码同时修改相应的注释,保证注释与代码的一
致性。不再有用的注释要删除。
注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有
害。
注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,但不可放在下方。如放于上方则需与其上面的代码用空行隔开。
头文件中的函数声明前应有详尽的描述性注释(功能,参数,返回,备注)。
对于所有有物理含义的变量、常量,如果其命名不是充分自注释的,在声明时都必须加以注释,说明其物理含义。
数据结构声明(
包括数组、结构体、类、枚举等)
,如果其命名不是充分自注释的,必须加以注释。
全局变量要有较详细的注释,包括对其功能、取值范围、哪些函数存取它以及存取时注意事项等的说明。(建议:最好能避免使用全局变量)。
对于 switch
语句下的 case
语句,如果因为特殊情况需要处理完一个 case
后进入下一个 case
处理(即不使用 break
),必须在该 case
语句处理完、下一个 case
语句前加上明确的注释。
(五)
表达式与基本语句
l
运算符的优先级
如果代码行中的运算符比较多,为了防止产生歧义并提高可读性,应当用括号明确表达式的操作顺序。
l
复合表达式
如
a = b = c = 0
这样的表达式称为复合表达式。
不要编写太复杂的复合表达式。不要有多用途的复合表达式。不要把程序中的复合表达式与“真正的数学表达式”混淆(if
(a<b<c
)!=if(a<b&&b<c)
).
l
If
语句
不可将布尔变量直接与 TRUE
、FALSE
或者 1
、0
进行比较。
应当将整型变量用“==
”或“!=
”直接与 0
比较.
不可将浮点变量用“==
”或“!=
”与任何数字比较。(
精度问题)
应当将指针变量用“==
”或“!=
”与 NULL
比较。
对于 if
中的
“==
”比较,建议将变量放在“==
”之后,如“if (NULL==p)
”。这样可以通过编译器有效防止少写一个等号的问题,如果写成“if(p==NULL)
”但少写一个等号,编译器难以发现这个问题,而人的肉眼也很难看到,造成的 BUG
很难清除。
l
Case
语句
每个 case
语句的结尾不要忘了加 break
,否则将导致多个分支重叠(除非有意使多个分支重叠,此时必须加上注释进行说明)。
不要忘记最后那个 default
分支。即使程序真的不需要 default
处理,也应该保留语句 default : break;
这样做并非多此一举,而是为了防止别人误以为你忘了default
处理。
l
Goto
语句
争议不断,少用、慎用
goto
语句,而不是禁用。
l
其他规则建议
避免使用不易理解的数字,用有意义的标识来替代,尤其是代码中经常重复出现的数字。涉及物理状态或者含有物理意义的常量,不应直接使用数字,必须用有意义的枚举或宏常量或 const
常量来代替。这样可以让你的代码变得更可读、更可靠、并且更容易维护。
为便于代码的阅读和理解,源程序中关系较为紧密的代码应尽可能相邻。
需要对外公开的常量和宏定义放在头文件中,不需要对外公开的常量和宏定义放在定义文件的头部。为便于管理,可以把不同模块共用的常量和宏定义集中存放在一个公共的头文件中。
===========================================================================
(六)
变量与类
l
变量的使用规范
ü
去掉没必要的或多余的全局变量。全局变量是增大模块间耦合的原因之一,应减少没必要的公共变量以降低模块间的耦合度。
ü
禁止局部变量与全局变量同名。
ü
在定义变量的同时初始化该变量(就近原则),尤其是指针变量。
ü
严禁使用未经初始化的变量作为右值。特别是引用未经赋值的指针,经常会引起系统崩溃。
ü
在靠近第一次使用变量的位置声明和初始化该变量。
ü
尽可能缩短变量的“存活”时间,即一个变量存在期间所跨越的语句总数。存活时间很长的变量会降低程序的可读性,因为阅读者在同一时间内需要考虑的代码行数越多,就越难理解代码。
2
在循环开始之前再去初始化该循环里使用的变量,而不是在该循环所在函数的开始处去初始化这些变量。
2
确保使用了所有已声明的变量,否则就存在多余的变量,应该去掉。
2
在对变量声明的同时,应对其含义、作用及取值范围进行注释说明,同时若有必要还应说明与其它变量的关系。
2
明确全局变量与操作此全局变量的函数的关系,如访问、修改及创建等
(
有利于程序的进一步优化、单元测试、系统联调以及代码维护等。这种关系的说明可在注释或文档中描述。
)
2
当向全局变量传递数据时,要十分小心,若有必要应进行合法性检查,防止赋予不合理的值或越界等现象发生,以提高代码的可靠性、稳定性。
l
类的使用规范
ü
尽可能限制类中各成员的可访问性,这是促成类封装性的原则之一。
ü
要在类的
public
域中暴露成员数据,否则会破坏封装性,限制你对这个类的控制能力。
ü
防止析构函数忘记释放内存或其他资源。
ü
消除类中无关的信息,如从未调用过的成员函数或从未用过的数据成员。
2
尽可能在所有的构造函数中初始化所有的数据成员。
2
类的接口应尽可能隐藏其内部实现细节。
2
限制类的数据成员的数目,不应超过
7
个。
l
其他建议
2
编程时,要注意数据类型的强制转换。
(七)
函数设计
l
函数的参数
ü
参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用
void
填充;如果函数没有返回值,也必须用
void
填充。
ü
参数命名要恰当并具有“自注释”效果,顺序要合理。
ü
如果参数是指针或引用,且仅作输入用,则应在类型前加
const
,以防止该参数在函数体内被意外修改。
ü
如果输入参数以值传递的方式传递对象,则宜改用“
const &
”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高运行效率。
ü
函数中应该使用了所有的参数,如果有一个参数在函数体内未使用,则说明该参数多余,应去掉(除非有意保留该参数用于以后扩展)。
2
避免函数有太多的参数,
参数个数尽量控制在
5
个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。
2
参数的编排顺序一般按照“输入-修改-输出”的顺序。
2
如果有几个函数用了类似的一些参数,应该让这些参数的排列顺序保持一致。
l
函数体
ü
在对外提供的
public
函数体的“入口处”,对参数的有效性进行检查(建议对内部私有的
private
函数也进行参数有效性检查,除非你能确保参数取值必定合法)
2
不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄、成员变量等。
ü
在函数体的“出口处”,对
return
语句的正确性和效率进行检查。
(return
语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁
;
要搞清楚返回的究竟是“值”、“指针”还是“引用”
;
如果函数返回值是一个对象,要考虑
return
语句的效率
return String(s1 + s2)
和
String temp(s1 + s2); return temp;
效率问题
)
ü
对函数体内所调用的其他函数(标准库函数或用户自定义的函数)的返回值要仔细、全面地处理,比如检查
fopen
的返回是否为
NULL
等。
ü
避免
fopen
之后忘记了
fclose
类似的配对操作,否则将导致
IO
资源耗尽。
2
函数名应准确描述函数的功能。避免使用无意义或含义不清的动词如
process
、
handle
等为函数命名,因为这些动词并没有说明要具体做什么。
2
保持函数功能的强内聚性,即函数的功能要单一,不要设计多用途的函数。
2
函数体的规模要小,尽量控制在
100
行代码之内;如果规模过大,可适当地把一段或多段关系密切的代码拿出来构成一个或多个函数。
2
消除函数中多余的语句,防止程序中的垃圾代码。程序中的垃圾代码不仅占用额外的空间,而且还会影响程序功能与性能,给程序的测试、维护等造成不必要的麻烦。
2
如果多段代码重复做同一件事情,那么在函数的划分上可能存在问题。(构建新的函数)
2
除非为某些算法或功能的实现方便,减少函数本身或函数间的递归调用。(降低可理解性,占用较多的系统资源(如栈空间),有可能导致堆栈溢出,难进行程序测试)。
2
尽量避免函数带有“记忆”功能,相同的输入应当产生相同的输出。带有
“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在
C/C++
语言中,函数的
static
局部变量是函数的“记忆”存储器。建议尽量少用
static
局部变量,除非必需。
2
用于出错处理的返回值一定要清楚和全面地进行说明,让使用者不容易忽视或误解错误情况。
l
使用断言(
assert
)
Assert
是
debug
版本中起作用的宏,检查“不应该”发生的情况,是无害测试手段。
ü
使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
ü
不能用断言来检查最终产品肯定会出现且必须处理的错误情况。
2
在函数的入口处,使用断言检查参数的有效性(合法性)。
2
在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
(八)
内存管理
l
内存分配方式简介
从静态存储区域分配(编译时分配,程序整个运行期间都存在。如全局变量,
static
变量。)
在栈上创建(函数内局部变量,函数执行结束时这些存储单元自动被释放,效率很高,但是分配的内存容量有限)
从堆上分配(动态内存分配,
malloc/free(
标准库函数
), new/delete
(运算符),)
|
内存分配方式
l
常见的内存错误及其对策
内存分配未成功,却使用了它。用
if (p == NULL)
检查。
内存分配虽然成功,但是尚未初始化就引用它。
内存分配成功并且已经初始化,但操作越过了内存的边界。
忘记了释放内存,造成内存泄露。
释放了内存却继续使用它。(及时赋值为
NULL
)
l
内存使用规范
ü
用
malloc/calloc
或
new
申请内存之后,应该立即检查指针值是否为
NULL
,防止使用指针值为
NULL
的内存(此时程序将崩溃)。
ü
防止将未被初始化的内存作为右值使用。
ü
避免数组或指针的下标越界,特别要当心发生“多
1
”或者“少
1
”操作。
ü
动态内存的申请与释放必须配对,防止内存泄漏。
ü
malloc/realloc/calloc
申请的内存使用
free
释放,
new
申请的内存使用
delete
释放,不能搞混了。
ü
使用
new []
申请的内存必须用
delete[]
进行释放,如果
delete
没有加上
[]
则只调用了数组中第一个对象的析构函数,极可能造成内存泄漏。
ü
用
free
或
delete
释放内存后,立即将指针设置为
NULL
,防止产生“野指针”。
ü
防止函数内的
break/continue/goto/return
等语句跳过了必要的内存释放操作,造成内存泄漏。
(九)
其他编程经验
l
程序效率方面的考虑
程序效率指的是程序占用资源的情况,如
CPU
、内存、外存、网络资源等,一般指时间效率和空间效率,前者是指运行速度,后者是指程序占用内存或者外存的状况。
ü
不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。
ü
以提高程序的全局效率为主,提高局部效率为辅。
ü
在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关紧要之处优化。
ü
先优化数据结构和算法,再优化执行代码。
ü
有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷。例如多花费一些内存来提高性能。
ü
不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码。
2
循环体内工作量最小化。应仔细考虑循环体内的语句是否可以放在循环体之外,使循环体内工作量最小,从而提高程序的时间效率。
2
在多重循环中,应将最忙的循环放在最内层,这样可以减少
CPU
切入循环层的次数,并减少一些语句的执行次数。
l
一些有益的建议
2
当心那些视觉上不易分辨的操作符发生书写错误。我们经常会把“==”误写成“=”,象“
||
”、“
&&
”、“
<=
”、“
>=
”这类符号也很
容易发生“丢
1
”失误。然而编译器却不一定能自动指出这类错误。
2
当心变量的初值、缺省值错误,或者精度不够。
2
当心变量发生上溢或下溢;当心数组的下标越界。
2
当心忘记编写错误处理程序,当心错误处理程序本身有误。
2
当心文件
I/O
有错误。
2
如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的代码,应当重新编写。
2
尽量使用标准库函数,不要“发明”已经存在的库函数。
2
尽量不要使用与具体硬件或软件环境关系密切的变量。
2
把编译器的选择项设置为最严格状态并重视编译器给出的每条
warning
。
2
编写代码时要注意随时保存,并定期备份,防止由于断电、硬盘损坏等原因造成代码丢失。
2
同一个软件产品(或项目组)内,最好使用相同的编译器,并使用相同的设置选项。
2
用宏定义表达式时,要使用完备的括号。
2
有可能的话,
if
语句尽量加上
else
分支,对没有
else
分支的语句要小心对待;
switch
语句必须有
default
分支。
2
不使用与硬件或操作系统关系很大的语句或者第三方提供的非标准类库如
MFC
,而使用
C/C++
标准库或语句,以提高软件的可移植性和可重用性。
(十)
终极建议
永远记住:“始终站在代码阅读者和使用者的角度去编写和组织你的程序。
”
―― Steve McConnell