注: 如涉及侵权,请联系作者删除。
文件结构
【规则 1-2-1】为了防止头文件被重复引用,应当用 ifndef/define/endif 结构产生预处理块。
【规则 1-2-2】用 #include <filename.h>
格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。
【规则 1-2-3】用#include “filename.h”
格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。
【建议 1-2-1】头文件中只存放声明而不存放定义。
【建议 1-2-2】不提倡使用全局变量,尽量不要在头文件中出现象 extern int value
这类声明。
程序版式
【规则 2-1-1】在每个类声明之后、每个函数定义结束之后都要加空行。
【规则 2-1-2】在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
【规则 2-2-1】一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样 的代码容易阅读,并且方便于写注释。
【规则 2-2-2】if、for、while、do
等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}。这样可以防止书写失误。
【建议 2-2-1】尽可能在定义变量的同时初始化该变量(就近原则)。
【规则 2-3-1】关键字之后要留空格。像 const、virtual、inline、case
等关键字之后至少要留一个空格,否则无法辨析关键字。像 if、for、while
等关键字之后应留一个 空格再跟左括号 (
,以突出关键字。
【规则 2-3-2】函数名之后不要留空格,紧跟左括号(
,以与关键字区别。
【规则 2-3-3】 (
向后紧跟,) , ;
向前紧跟,紧跟处不留空格。
【规则 2-3-4】,
之后要留空格,如 Function(x, y, z)
。如果;
不是一行的结束符号,其后要留空格,如 for (initialization; condition; update)
。
【规则 2-3-5】赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符, 如=
、+=
、 >=
、<=
、+
、*
、%
、&&
、||
、<<
、^
等二元操作符的前后应当加空格。
【规则 2-3-6】一元操作符如!
、~
、++
、--
、&
(地址运算符)等前后不加空格。
【规则 2-3-7】像[]
、.
、->
这类操作符前后不加空格。
【建议 2-3-1】对于表达式比较长的 for
语句和if
语句,为了紧凑起见可以适当地去 掉一些空格,如 for (i=0; i<10; i++)
和 if ((a<=b) && (c<=d))
【规则 2-4-1】程序的分界符{
和}
应独占一行并且位于同一列,同时与引用它们的语句左对齐。
【规则 2-4-2】{ }
之内的代码块在{
右边数格处左对齐。
【规则 2-5-1】代码行最大长度宜控制在80个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印。
【规则 2-5-2】长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
【规则 2-6-1】应当将修饰符 *
和 &
紧靠变量名。
【规则 2-7-1】注释是对代码的提示,而不是文档。程序中的注释不可喧宾夺主, 注释太多了会让人眼花缭乱。注释的花样要少。
【规则 2-7-2】如果代码本来就是清楚的,则不必加注释。否则多此一举,令人厌烦。 例如 i++; // i 加 1
,多余的注释。
【规则 2-7-3】边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
【规则 2-7-4】注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有害。
【规则 2-7-5】尽量避免在注释中使用缩写,特别是不常用缩写。
【规则 2-7-6】注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。
【规则 2-7-8】当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
变量命名
【规则 3-1-1】标识符应当直观且可以拼读,可望文知意,不必进行解码。
【规则 3-1-2】标识符的长度应当符合min-length && max-information
原则。
【规则 3-1-3】命名规则尽量与所采用的操作系统或开发工具的风格保持一致。
【规则 3-1-4】程序中不要出现仅靠大小写区分的相似的标识符。
【规则 3-1-5】程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解。
【规则 3-1-6】变量的名字应当使用名词
或者形容词+名词
。
如:float newValue
【规则 3-1-7】全局函数的名字应当使用“动词”或者动词+名词
(动宾词组)。 类的成员函数应当只使用动词
,被省略掉的名词就是对象本身。
如:void DrawBox();
【规则 3-1-8】用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
【建议 3-1-1】尽量避免名字中出现数字编号,如 Value1,Value2
等。
【规则 3-2-1】类名和函数名用大写字母开头的单词组合而成。
【规则 3-2-2】变量和参数用小写字母开头的单词组合而成。
【规则 3-2-3】常量全用大写的字母,用下划线分割单词。
【规则 3-2-4】静态变量加前缀 s_
(表示 static)。
【规则 3-2-5】如果不得已需要全局变量,则使全局变量加前缀 g_
(表示 global)。
【规则 3-2-6】类的数据成员加前缀 m_
(表示 member),这样可以避免数据成员与 成员函数的参数同名。
【规则 3-2-7】为了防止某一软件库中的一些标识符和其它软件库中的冲突,可以为各种标识符加上能反映软件性质的前缀。例如三维图形标准 OpenGL 的所有库函数均以 gl
开头,所有常量(或宏定义)均以 GL
开头。
表达式和基本语句
【规则 4-1-1】如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。
【规则 4-2-1】不要编写太复杂的复合表达式。
【规则 4-2-2】不要有多用途的复合表达式。
【规则 4-2-3】不要把程序中的复合表达式与“真正的数学表达式”混淆。
【规则 4-3-1】不可将布尔变量直接与 TRUE
、FALSE
或者 1
、0
进行比较。
【规则 4-3-2】应当将整型变量用 ==
或!=
直接与 0
比较。
【规则 4-3-3】不可将浮点变量用==
或!=
与任何数字比较。 (直接比较容易出bug)
【规则 4-3-4】应当将指针变量用==
或!=
与 NULL
比较。
【建议 4-4-1】在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。
for (col=0; col<5; col++ )
{
for (row=0; row<100; row++)
{
sum = sum + a[row][col];
}
}
【建议 4-4-2】如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。
【规则 4-5-1】不可在for
循环体内修改循环变量,防止for
循环失去控制。
【建议 4-5-1】建议for
语句的循环控制变量的取值采用半开半闭区间写法。
【规则 4-6-1】每个 case
语句的结尾不要忘了加 break
,否则将导致多个分支重叠 (除非有意使多个分支重叠)。
【规则 4-6-2】不要忘记最后那个 default
分支。即使程序真的不需要 default
处理, 也应该保留语句 default : break;
这样做并非多此一举,而是为了防止别人误以为你忘了default
处理。
【建议 4-7-1】慎用 goto
语句。
常量
【规则 5-1-1】 尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。
【规则 5-2-1】在 C++ 程序中只使用 const
常量而不使用宏常量,即 const
常量完全取代宏常量。
NOTE:
a: const
常量有数据类型,而宏常量没有数据类型,编译器可以对前者进行类型安全检查,而对后者只是字符替换,没有类型安全检查,并且可能产生意向不到的错误(边际效应)
b: 有些集成化的调试工具可以对 const
常量进行调试,但是不能对宏常量进行调试。
【规则 5-3-1】需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
【规则 5-3-2】如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。
函数设计
【规则 6-1-1】参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。 如果函数没有参数,则用 void
填充。
【规则 6-1-2】参数命名要恰当,顺序要合理。
【规则 6-1-3】如果参数是指针,且仅作输入用,则应在类型前加 const
,以防止该指针在函数体内被意外修改。
【规则 6-1-4】如果输入参数以值传递的方式传递对象,则宜改用const &
方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
【建议 6-1-1】避免函数有太多的参数,参数个数尽量控制在5个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。
【建议 6-1-2】尽量不要使用类型和数目不确定的参数。
【规则 6-2-1】不要省略返回值的类型。
【规则 6-2-2】函数名字与返回值类型在语义上不可冲突。
【规则 6-2-3】不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而 错误标志用 return
语句返回。
【建议 6-2-1】有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。
【建议 6-2-2】如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传 递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。
【规则 6-3-1】在函数体的“入口处”,对参数的有效性进行检查。
【规则 6-3-2】在函数体的“出口处”,对 return 语句的正确性和效率进行检查。
(1)return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数 体结束时被自动销毁。
(2)要搞清楚返回的究竟是“值”、“指针”还是“引用”。
(3)如果函数返回值是一个对象,要考虑 return 语句的效率。
【建议 6-4-1】函数的功能要单一,不要设计多用途的函数。
【建议 6-4-2】函数体的规模要小,尽量控制在 50 行代码之内。
【建议 6-4-3】尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。
在 C/C++语言中,函数的 static
局部变量是函数的“记忆”存储器。建议尽量少用 static
局部变量,除非必需。
【建议 6-4-4】不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。
【建议 6-4-5】用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。
【规则 6-5-1】使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
【规则 6-5-2】在函数的入口处,使用断言检查参数的有效性(合法性)。
【建议 6-5-1】在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?” 一旦确定了的假定,就要使用断言对假定进行检查。
【建议 6-5-2】一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。
引用的一些规则如下:
(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
(2)不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以是 NULL)。
(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。
如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。
内存管理
内存分配方式有三种
- 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
- 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
内存错误及对策
- 内存分配未成功,却使用了它。(常用解决办法是, 在使用内存之前检查指针是否为 NULL。如果指针 p 是函数的参数,那么在函数的入口 处用 assert(p!=NULL)进行检查。如果是用 malloc 或 new 来申请内存,应该用 if(p==NULL) 或 if(p!=NULL)进行防错处理。)
- 内存分配虽然成功,但是尚未初始化就引用它。(无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不 可省略,不要嫌麻烦。)
- 内存分配成功并且已经初始化,但操作越过了内存的边界。特别是在 for 循环语 句中,循环次数很容易搞错,导致数组操作越界。
- 忘记了释放内存,造成内存泄露。(动态内存的申请与释放必须配对,程序中 malloc 与 free 的使用次数一定要相同,否 则肯定有错误(new/delete 同理))
- 释放了内存却继续使用它。(1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内 存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。 (2)函数的 return 语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。 (3)使用
free
或delete
释放了内存后,没有将指针设置为NULL
。导致产生“野指针”。
【规则 7-2-1】用 malloc
或 new
申请内存之后,应该立即检查指针值是否为 NULL
。 防止使用指针值为 NULL
的内存。
【规则 7-2-2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
【规则 7-2-3】避免数组或指针的下标越界,特别要当心发生“多 1”或者“少 1” 操作。
【规则 7-2-4】动态内存的申请与释放必须配对,防止内存泄漏。
【规则 7-2-5】用 free
或delete
释放了内存之后,立即将指针设置为 NULL
,防止产生“野指针”。
malloc
与 free
是 C++/C
语言的标准库函数,new/delete
是 C++
的运算符。它们都可用于申请动态内存和释放内存。对于非内部数据类型的对象而言,光用 maloc/free
无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于 malloc/free
是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free
。 因此 C++
语言需要一个能完成动态内存分配和初始化工作的运算符 new
,以及一个能完成清理与释放内存工作的运算符 delete
。注意 new/delete
不是库函数。
C++函数的高级特性
成员函数被重载的特征:
- 相同的范围(在同一个类中);
- 函数名字相同;
- 参数不同;
- virtual 关键字可有可无。
覆盖是指派生类函数覆盖基类函数,特征是:
- 不同的范围(分别位于派生类与基类);
- 函数名字相同;
- 参数相同;
- 基类函数必须有 virtual 关键字。
【规则 8-3-1】参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
【规则 8-3-2】如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。
在 C++
运算符集合中,有一些运算符是不允许被重载的。这种限制是出于安全方面的考虑,可防止错误和混乱。
- 不能改变
C++
内部数据类型(如int,float
等)的运算符。 - 不能重载
.
,因为.
在类中对任何成员都有意义,已经成为标准用法。 - 不能重载目前 C++运算符集合中没有的符号,如
#,@,$
等。原因有两点,一是难以理解,二是难以确定优先级。 - 对已经存在的运算符进行重载时,不能改变优先级规则,否则将引起混乱。
C++ 语言支持函数内联,其目的是为了提高函数的执行效率(速度)。
关键字 inline
必须与函数定义体放在一起才能使函数成为内联,仅将 inline
放在函数声明前面不起任何作用。
类的构造函数、析构函数与赋值函数
构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却在函数体{}
之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。 构造函数初始化表的使用规则:
- 如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
- 类的
const
常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化(参见 5.4 节)。 - 类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。
构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承 关系,在编写上述基本函数时应注意以下事项:
- 派生类的构造函数应在其初始化表里调用基类的构造函数。
- 基类与派生类的析构函数应该为虚(即加
virtual
关键字)。 - 在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。
类的继承与组合
【规则 10-1-1】如果类 A 和类 B 毫不相关,不可以为了使 B 的功能更多些而让 B 继承 A 的功能和属性。
【规则 10-1-2】若在逻辑上 B 是 A 的一种(a kind of ),则允许 B 继承 A 的功能和属性。
【规则 10-1-2】看起来很简单,但是实际应用时可能会有意外,继承的概念在程序 世界与现实世界并不完全相同。
所以更加严格的继承规则应当是:若在逻辑上 B 是 A 的一种,并且 A 的所有功 能和属性对 B 而言都有意义,则允许 B 继承 A 的功能和属性。
【规则 10-2-1】若在逻辑上 A 是 B 的一部分(a part of),则不允许 B 从 A 派生, 而是要用 A 和其它东西组合出 B。
其他经验
const
更大的魅力是它可以修饰函数的参数、返回值,甚至函数的定义体。如果参数作输出用,不论它是什么数据类型,也不论它采用指针传递还是引用传递,都不能加const
修饰,否则该参数将失去输出功能。
【规则 11-2-1】不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。
【规则 11-2-2】以提高程序的全局效率为主,提高局部效率为辅。
【规则 11-2-3】在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关 紧要之处优化。
【规则 11-2-4】先优化数据结构和算法,再优化执行代码。
【规则 11-2-5】有时候时间效率和空间效率可能对立,此时应当分析那个更重要, 作出适当的折衷。例如多花费一些内存来提高性能。
【规则 11-2-6】不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码。
【建议 11-3-1】当心那些视觉上不易分辨的操作符发生书写错误。
我们经常会把==
误写成=
,象||
、 &&
、 <=
、 >=
这类符号也很容易发生丢 1失误。然而编译器却不一定能自动指出这类错误。
【建议 11-3-2】变量(指针、数组)被创建之后应当及时把它们初始化,以防止把未被初始化的变量当成右值使用。
【建议 11-3-3】当心变量的初值、缺省值错误,或者精度不够。
【建议 11-3-4】当心数据类型转换发生错误。尽量使用显式的数据类型转换(让人 们知道发生了什么事),避免让编译器轻悄悄地进行隐式的数据类型转换。
【建议 11-3-5】当心变量发生上溢或下溢,数组的下标越界。
【建议 11-3-6】当心忘记编写错误处理程序,当心错误处理程序本身有误。
【建议 11-3-7】当心文件 I/O 有错误。
【建议 11-3-8】避免编写技巧性很高代码。
【建议 11-3-9】不要设计面面俱到、非常灵活的数据结构。
【建议 11-3-10】如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的 代码,应当重新编写。
【建议 11-3-11】尽量使用标准库函数,不要“发明”已经存在的库函数。
【建议 11-3-12】尽量不要使用与具体硬件或软件环境关系密切的变量。
【建议 11-3-13】把编译器的选择项设置为最严格状态。
【建议 11-3-14】如果可能的话,使用 PC-Lint、LogiScope 等工具进行代码审查。
【引用】
- 高质量 C++/C 编程指南,林锐