C++/C编程规范


第三工作室编程规范 ——C++/C 篇 第一版 www.thirdstudio.com.cn 目 录 1 文件结构......................................................................................................................................... 1 1.1 版权和版本的声明......................................................................................................... 1 1.2 头文件的结构................................................................................................................. 1 【规则 1-2-1】....................................................................................................................... 2 【规则 1-2-2】....................................................................................................................... 2 【规则 1-2-3】....................................................................................................................... 2 【规则 1-2-4】....................................................................................................................... 2 【规则 1-2-5】....................................................................................................................... 2 1.3 定义文件的结构............................................................................................................. 2 1.4 头文件的作用................................................................................................................. 3 1.5 目录结构......................................................................................................................... 3 2 程序的版式..................................................................................................................................... 4 2.1 空行................................................................................................................................. 4 【规则 2-1-1】....................................................................................................................... 4 【规则 2-1-2】....................................................................................................................... 4 【规则 2-1-3】....................................................................................................................... 4 【规则 2-1-4】....................................................................................................................... 4 【规则 2-1-5】....................................................................................................................... 5 2.2 代码行内的空格............................................................................................................. 5 【规则 2-2-1】....................................................................................................................... 5 【规则 2-2-2】....................................................................................................................... 5 【规则 2-2-3】....................................................................................................................... 5 【规则 2-2-4】....................................................................................................................... 5 【规则 2-2-5】....................................................................................................................... 5 【规则 2-2-6】....................................................................................................................... 5 【规则 2-2-7】....................................................................................................................... 5 【规则 2-2-8】....................................................................................................................... 6 2.3 对齐................................................................................................................................. 6 【规则 2-3-1】....................................................................................................................... 6 【规则 2-3-2】....................................................................................................................... 6 2.4 长行拆分......................................................................................................................... 7 【规则 2-4-1】....................................................................................................................... 7 【规则 2-4-2】....................................................................................................................... 7 2.5 修饰符的位置................................................................................................................. 8 【规则 2-5-1】....................................................................................................................... 8 2.6 注释................................................................................................................................. 8 【规则 2-6-1】....................................................................................................................... 9 【规则 2-6-2】....................................................................................................................... 9 【规则 2-6-3】....................................................................................................................... 9 【规则 2-6-4】....................................................................................................................... 9 【规则 2-6-5】....................................................................................................................... 9 【规则 2-6-6】....................................................................................................................... 9 【规则 2-6-7】....................................................................................................................... 9 2.7 类的版式......................................................................................................................... 9 3 命名规则....................................................................................................................................... 11 1 www.thirdstudio.com.cn 3.1 共性规则....................................................................................................................... 11 【规则 3-1-1】..................................................................................................................... 11 【规则 3-1-2】..................................................................................................................... 11 【规则 3-1-3】..................................................................................................................... 11 【规则 3-1-4】..................................................................................................................... 11 【规则 3-1-5】..................................................................................................................... 11 【规则 3-1-6】..................................................................................................................... 11 【规则 3-1-7】..................................................................................................................... 11 【规则 3-1-8】..................................................................................................................... 12 【规则 3-1-9】..................................................................................................................... 12 3.2 简单的Windows 应用程序命名规则.......................................................................... 12 【规则 3-2-1】..................................................................................................................... 12 【规则 3-2-2】..................................................................................................................... 12 【规则 3-2-3】..................................................................................................................... 12 【规则 3-2-4】..................................................................................................................... 12 【规则 3-2-5】..................................................................................................................... 12 【规则 3-2-6】..................................................................................................................... 12 【规则 3-2-7】..................................................................................................................... 12 4 表达式和基本语句....................................................................................................................... 14 4.1 运算符的优先级........................................................................................................... 14 【规则 4-1-1】..................................................................................................................... 14 4.2 复合表达式................................................................................................................... 14 【规则 4-2-1】..................................................................................................................... 15 【规则 4-2-2】..................................................................................................................... 15 【规则 4-2-3】..................................................................................................................... 15 4.3 if 语句......................................................................................................................... 15 4.3.1 布尔变量与零值比较........................................................................................... 15 【规则 4-3-1】............................................................................................................ 15 4.3.2 整型变量与零值比较........................................................................................... 15 【规则 4-3-2】............................................................................................................ 15 4.3.3 浮点变量与零值比较........................................................................................... 16 【规则 4-3-3】............................................................................................................ 16 4.3.4 指针变量与零值比较........................................................................................... 16 【规则 4-3-4】............................................................................................................ 16 4.3.5 对if 语句的补充说明 .......................................................................................... 16 4.4 循环语句的效率........................................................................................................... 17 【规则 4-4-1】..................................................................................................................... 17 【规则 4-4-2】..................................................................................................................... 17 4.5 for 语句的循环控制变量........................................................................................... 17 【规则 4-5-1】..................................................................................................................... 17 【规则 4-5-2】..................................................................................................................... 17 4.6 switch 语句................................................................................................................. 18 【规则 4-6-1】..................................................................................................................... 18 【规则 4-6-2】..................................................................................................................... 18 4.7 goto 语句..................................................................................................................... 18 5 常量............................................................................................................................................... 20 5.1 为什么需要常量........................................................................................................... 20 2 www.thirdstudio.com.cn 【规则 5-1-1】..................................................................................................................... 20 5.2 const 与 #define 的比较......................................................................................... 20 【规则 5-2-1】..................................................................................................................... 20 5.3 常量定义规则............................................................................................................... 20 【规则 5-3-1】..................................................................................................................... 20 【规则 5-3-2】..................................................................................................................... 20 5.4 类中的常量................................................................................................................... 20 6 函数设计....................................................................................................................................... 22 6.1 参数的规则................................................................................................................... 22 【规则 6-1-1】..................................................................................................................... 22 【规则 6-1-2】..................................................................................................................... 22 【规则 6-1-3】..................................................................................................................... 22 【规则 6-1-4】..................................................................................................................... 22 【规则 6-1-5】..................................................................................................................... 22 【规则 6-1-6】..................................................................................................................... 22 6.2 返回值的规则............................................................................................................... 23 【规则 6-2-1】..................................................................................................................... 23 【规则 6-2-2】..................................................................................................................... 23 【规则 6-2-3】..................................................................................................................... 23 【规则 6-2-4】..................................................................................................................... 23 【规则 6-2-5】..................................................................................................................... 23 6.3 函数内部实现的规则................................................................................................... 24 【规则 6-3-1】..................................................................................................................... 24 【规则 6-3-2】..................................................................................................................... 24 6.4 其它规则....................................................................................................................... 25 【规则 6-4-1】..................................................................................................................... 25 【规则 6-4-2】..................................................................................................................... 25 【规则 6-4-3】..................................................................................................................... 25 【规则 6-4-4】..................................................................................................................... 25 【规则 6-4-5】..................................................................................................................... 25 6.5 使用断言....................................................................................................................... 25 【规则 6-5-1】..................................................................................................................... 26 【规则 6-5-2】..................................................................................................................... 26 【规则 6-5-3】..................................................................................................................... 26 【规则 6-5-4】..................................................................................................................... 26 6.6 引用与指针的比较....................................................................................................... 26 7 内存管理....................................................................................................................................... 28 7.1 内存分配方式............................................................................................................... 28 7.2 常见的内存错误及其对策........................................................................................... 28 【规则 7-2-1】..................................................................................................................... 29 【规则 7-2-2】..................................................................................................................... 29 【规则 7-2-3】..................................................................................................................... 29 【规则 7-2-4】..................................................................................................................... 29 【规则 7-2-5】..................................................................................................................... 29 7.3 指针与数组的对比....................................................................................................... 29 7.3.1 修改内容............................................................................................................... 29 7.3.2 内容复制与比较................................................................................................... 29 3 www.thirdstudio.com.cn 7.3.3 计算内存容量....................................................................................................... 30 7.4 指针参数是如何传递内存的?................................................................................... 31 7.5 free 和delete 把指针怎么啦?................................................................................... 34 7.6 动态内存会被自动释放吗?....................................................................................... 34 7.7 杜绝“野指针” ............................................................................................................... 35 7.8 有了malloc/free 为什么还要new/delete ? ............................................................... 35 7.9 内存耗尽怎么办?....................................................................................................... 37 7.10 malloc/free 的使用要点............................................................................................... 38 7.11 new/delete 的使用要点 ............................................................................................... 39 7.12 林锐的心得体会........................................................................................................... 39 8 其它编程经验...............................................................................................................................39 8.1 使用const 提高函数的健壮性 .................................................................................... 39 8.1.1 用const 修饰函数的参数 .................................................................................... 40 8.1.2 用const 修饰函数的返回值 ................................................................................ 41 8.1.3 const 成员函数 .................................................................................................... 41 8.2 提高程序的效率........................................................................................................... 42 【规则 8-2-1】..................................................................................................................... 42 【规则 8-2-2】..................................................................................................................... 42 【规则 8-2-3】..................................................................................................................... 42 【规则 8-2-4】..................................................................................................................... 42 【规则 8-2-5】..................................................................................................................... 42 【规则 8-2-6】..................................................................................................................... 42 8.3 其他规则....................................................................................................................... 42 【规则 8-3-1】..................................................................................................................... 42 【规则 8-3-2】..................................................................................................................... 42 【规则 8-3-3】..................................................................................................................... 42 【规则 8-3-4】..................................................................................................................... 42 【规则 8-3-5】..................................................................................................................... 42 【规则 8-3-6】..................................................................................................................... 42 【规则 8-3-7】..................................................................................................................... 42 【规则 8-3-8】..................................................................................................................... 42 【规则 8-3-9】..................................................................................................................... 42 【规则 8-3-10】................................................................................................................... 42 【规则 8-3-11】 ................................................................................................................... 42 【规则 8-3-12】................................................................................................................... 42 【规则 8-3-13】................................................................................................................... 42 【规则 8-3-14】................................................................................................................... 42 参考文献............................................................................................................................................... 43 版本声明............................................................................................................................................... 43 4 www.thirdstudio.com.cn 1 文件结构 每个C++/C 程序通常分为两个文件。一个文件用于保存程序的声明(declaration),称为 头文件。另一个文件用于保存程序的实现(implementation),称为定义(definition)文件。 C++/C 程序的头文件以“.h”为后缀,C 程序的定义文件以“.c”为后缀,C++程序的定 义文件通常以“.cpp”为后缀(也有一些系统以“.cc”或“.cxx”为后缀)。 1.1 版权和版本的声明 版权和版本的声明位于头文件和定义文件的开头(参见示例1-1),主要内容有: (1)版权信息。 (2)文件名称,标识符,摘要。 (3)当前版本号,作者/修改者,完成日期。 (4)版本历史信息。 示例 1-1 版权和版本的声明 1.2 头文件的结构 头文件由三部分内容组成: (1)头文件开头处的版权和版本声明(参见示例1-1)。 (2)预处理块。 (3)函数和类结构声明等。 1 www.thirdstudio.com.cn 假设头文件名称为 graphics.h,头文件的结构参见示例1-2。 【规则1-2-1】为了防止头文件被重复引用,应当用ifndef/define/endif 结构产生预处理块。 【规则1-2-2】用 #include 格式来引用标准库的头文件(编译器将从标准库目录 开始搜索)。 【规则1-2-3】用 #include “filename.h” 格式来引用非标准库的头文件(编译器将从用户的工作 目录开始搜索)。 【规则1-2-4】头文件中只存放“声明”而不存放“定义”。在C++ 语法中,类的成员函数可以 在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,却造成了风格不 一致,弊大于利。建议将成员函数的定义与声明分开,不论该函数体有多么小。 【规则1-2-5】不提倡使用全局变量,尽量不要在头文件中出现象extern int value 这类声明。 示例 1-2 C++/C 头文件的结构 1.3 定义文件的结构 定义文件有三部分内容: (1) 定义文件开头处的版权和版本声明(参见示例1-1)。 (2) 对一些头文件的引用。 (3) 程序的实现体(包括数据和代码)。 假设定义文件的名称为 graphics.cpp,定义文件的结构参见示例1-3。 2 www.thirdstudio.com.cn 示例 1-3 C++/C 定义文件的结构 1.4 头文件的作用 早期的编程语言如Basic、Fortran 没有头文件的概念,C++/C 语言的初学者虽然会使用头 文件,但常常不明其理。这里对头文件的作用略作解释: (1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,这时只要向 用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不 必关心接口是怎么实现的。编译器会从库中提取相应的代码。 (2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明 不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。 1.5 目录结构 如果一个软件的头文件数目比较多(如超过十个),通常应将头文件和定义文件分别保存 于不同的目录,便于维护。 例如可将头文件保存于include 目录,而将定义文件保存于source 目录(可以是多级目录)。 如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明”。为了 加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。 3 www.thirdstudio.com.cn 2 程序的版式 版式虽然不会影响程序的功能,但会影响可读性。程序版式的清晰、美观,是程序风格的 重要构成因素。 2.1 空行 空行起着分隔程序段落的作用。空行得体将使程序的布局显得更加清晰。空行不会浪费内 存,建议使用空行。 【规则2-1-1】在每个类声明之后、每个函数定义结束之后都要加空行。参见示例2-1(a)。 【规则2-1-2】在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。 参见示例2-1(b )。 示例 2-1(a) 函数之间的空行 (b) 函数内部的空行 【规则2-1-3】一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易 阅读,并且便于写注释。 【规则2-1-4】if、for、while、do 等语句自占一行,执行语句不得紧跟其后。不论执行语句有 多少都应加{}。防止书写失误。 示例2-2(a)为风格良好的代码行,示例2-2(b)为风格不良的代码 4 www.thirdstudio.com.cn 示例 2-2(a) 风格良好的代码行 (b)为风格不良的代码行 【规则2-1-5】尽可能在定义变量的同时初始化该变量(就近原则)。因为如果变量的引用处和 其定义处相隔比较远,变量的初始化则很容易被忘记;如果引用了未被初始化的变量,则很可 能会导致程序错误。使用本规则可以减少隐患。例如 int width = 10; // 定义并初绐化width int height = 10; // 定义并初绐化height int depth = 10; // 定义并初绐化depth 2.2 代码行内的空格 【规则2-2-1】关键字之后要留空格。像const、virtual、inline、case 等关键字之后至少要留一 个空格,否则无法辨析关键字。像if、for、while 等关键字之后应留一个空格再跟左括号‘(’, 以突出关键字。 【规则2-2-2】函数名之后不要留空格,紧跟左括号‘(’,来与关键字区别。 【规则2-2-3】‘(’向后紧跟;‘)’、‘,’、‘;’向前紧跟,紧跟处不留空格。 【规则2-2-4】‘,’之后要留空格,如Function(x, y, z)。如果‘;’不是一行的结束符号,其后 要留空格,如for (initialization; condition; update)。 【规则2-2-5】赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=”、 “+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操 作符的前后应当加空格。 【规则2-2-6】一元操作符如“!”、“~”、“++”、“--”、“&”(地址运算符)等前后不 加空格。 【规则2-2-7】像“[]”、“.”、“->”这类操作符前后不加空格。 5 www.thirdstudio.com.cn 【规则2-2-8】对于表达式比较长的for 语句和if 语句,为了紧凑起见可以适当地去掉一些空格, 如for (i=0; i<10; i++)和if ((a<=b) && (c<=d)) 示例 2-3 代码行内的空格 2.3 对齐 【规则2-3-1】程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用它们的语句 左对齐。 【规则2-3-2】{ }之内的代码块在‘{’右边数格处左对齐。 示例2-4(a)为风格良好的对齐,示例2-4(b)为风格不良的对齐。 6 www.thirdstudio.com.cn 示例 2-4(a) 风格良好的对齐 (b) 风格不良的对齐 2.4 长行拆分 【规则2-4-1】代码行最大长度控制在80个字符以内。禁止代码行过长,否则不易阅读和打印。 【规则2-4-2】长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以此突出操 作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。 7 www.thirdstudio.com.cn 示例 2-5 长行的拆分 2.5 修饰符的位置 修饰符 * 和 & 应该靠近数据类型还是应该靠近变量名,是个有争议的活题。若将修饰符 * 靠近数据类型,例如:int* x; 从语义上讲此写法比较直观,即x是int 类型的指针。 而上述写法的弊端是容易引起误解,例如:int* x, y; 此处y 容易被误解为指针变量。建议 将x 和y 分行定义来避免误解。 【规则2-5-1】应当将修饰符 * 和 & 紧靠变量名。 例如: char *name; int *x, y; // 此处y 不会被误解为指针 2.6 注释 C 语言的注释符为“/*…*/”。C++语言中,程序块的注释常采用“/*…*/”,行注释一般 采用“//…”。注释通常用于: (1)版本、版权声明; (2)函数接口说明; (3)重要的代码行或段落提示。 8 www.thirdstudio.com.cn 虽然注释有助于理解代码,但注意不可过多地使用注释。参见示例2-6。 【规则2-6-1】注释是对代码的“提示”,而不是文档。程序中的注释不可喧宾夺主,注释太多 了会让人眼花缭乱。注释的花样要少。 【规则2-6-2】如果代码本来就是清楚的,则不必加注释。否则多此一举,令人厌烦。例如 i++; // i 加 1,多余的注释。 【规则2-6-3】边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。 并删除不再有用的注释。 【规则2-6-4】注释应当准确、易懂,防止注释有二义性。 【规则2-6-5】禁止在注释中使用缩写,特别是不常用的缩写。 【规则2-6-6】注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。 【规则2-6-7】当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅 读。 示例 2-6 程序的注释 2.7 类的版式 类可以将数据和函数封装在一起,其中函数表示了类的行为(或称服务)。类提供关键字 public、protected 和private,分别用于声明哪些数据和函数是公有的、受保护的或者是私有的, 这样可以达到信息隐藏的目的,即让类仅仅公开必须要让外界知道的内容,而隐藏其它一切内 容。禁止滥用类的封装功能。 类的版式主要有两种方式: (1)将private 类型的数据写在前面,而将public 类型的函数写在后面,如示例2-7(a)。采 用这种版式的程序员主张类的设计“以数据为中心”,重点关注类的内部结构。 (2)将public 类型的函数写在前面,而将private 类型的数据写在后面,如示例2-7(b)。采 用这种版式的程序员主张类的设计“以行为中心”,重点关注的是类应该提供什么样的接口(或 服务)。 建议采用“以行为中心”的书写方式,即首先考虑类应该提供什么样的函数。这是很多人 的经验——“这样做不仅让自己在设计类时思路清晰,而且方便别人阅读。因为用户最关心的 是接口。” 9 www.thirdstudio.com.cn 示例 2-7(a)以数据为中心版式 (b) 以行为为中心的版式 10 www.thirdstudio.com.cn 3 命名规则 比较著名的命名规则当推Microsoft 公司的“匈牙利”法,该命名规则的主要思想是“在 变量和函数名中加入前缀以增进人们对程序的理解”。例如所有的字符变量均以ch 为前缀,若 是指针变量则追加前缀p。如果一个变量由ppch 开头,则表明它是指向字符指针的指针。 “匈牙利”法最大的缺点是烦琐,例如 int i, j, k; float x, y, z; 倘若采用“匈牙利”命名规则,则应当写成 int iI, iJ, ik; // 前缀 i 表示int 类型 float fX, fY, fZ; // 前缀 f 表示float 类型 如此烦琐的程序会让绝大多数程序员无法忍受。 据考察,没有一种命名规则可以让所有的程序员赞同,程序设计教科书一般都不指定命名 规则。只要制定一种令大多数项目成员满意的命名规则,并在项目中贯彻实施即可。 3.1 共性规则 本节论述的共性规则是被大多数程序员采纳的,我们应当在遵循这些共性规则的前提下, 再扩充特定的规则,如3.2 节。 【规则3-1-1】标识符应当直观且可拼读,不需进行“解码”。标识符最好采用英文单词或其组 合,便于记忆和阅读。切忌使用汉语拼音来命名。程序中的英文单词一般不会太复杂,用词应 当准确。例如不应把CurrentValue 写成NowValue。 【规则3-1-2】标识符的长度应当符合“min-length && max-information”原则。几十年前老ANSI C 规定名字不准超过6 个字符,现今的C++/C 不再有此限制。一般来说,长名字能更好地表达 含义,所以函数名、变量名、类名长达十几个字符不足为怪。那么名字是否越长越好?不见得! 例如变量名maxval 就比maxValueUntilOverflow好用。单字符的名字也是有用的,常见的如i, j, k, m, n, x, y, z 等,它们通常可用作函数内的局部变量。 【规则3-1-3】命名规则尽量与所采用的操作系统或开发工具的风格保持一致。例如Windows 应 用程序的标识符通常采用“大小写”混排的方式,如AddChild。而Unix 应用程序的标识符通 常采用“小写加下划线”的方式,如add _child。禁止把这两类风格混在一起用。 【规则3-1-4】程序中禁止出现仅靠大小写区分的相似的标识符。 例如: int x, X; // 变量x 与 X 容易混淆 void foo(int x); // 函数foo 与FOO 容易混淆 void FOO(float x); 【规则3-1-5】程序中禁止出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同 且不会发生语法错误,但会使人误解。 【规则3-1-6】变量的名字应当使用“名词”或者“形容词+名词”。 例如: float value; float oldValue; float newValue; 【规则3-1-7】全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。类的成员 函数应当只使用“动词”,被省略掉的名词就是对象本身。 11 www.thirdstudio.com.cn 例如: DrawBox(); // 全局函数 box->Draw(); // 类的成员函数 【规则3-1-8】用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。 例如: int minValue; int maxValue; int SetValue(…); int GetValue(…); 【规则3-1-9】尽量避免名字中出现数字编号,如Value1,Value2 等,除非逻辑上的确需要编号。 3.2 简单的 Windows 应用程序命名规则 作者对“匈牙利”命名规则做了合理的简化,下述的命名规则简单易用,比较适合于Windows 应用软件的开发。 【规则3-2-1】类名和函数名用大写字母开头的单词组合而成。 例如: class Node; // 类名 class LeafNode; // 类名 void Draw(void); // 函数名 void SetValue(int value); // 函数名 【规则3-2-2】变量和参数用小写字母开头的单词组合而成。 例如: BOOL flag; int drawMode; 【规则3-2-3】常量全用大写的字母,用下划线分割单词。 例如: const int MAX = 100; const int MAX_LENGTH = 100; 【规则3-2-4】静态变量加前缀s_(表示static)。 例如: void Init(…) { static int s_initValue; // 静态变量 … } 【规则3-2-5】如果不得已需要全局变量,则在全局变量加前缀g_(表示global)。 例如: int g_howManyPeople; // 全局变量 int g_howMuchMoney; // 全局变量 【规则3-2-6】类的数据成员加前缀m_(表示member),这样可以避免数据成员与 成员函数的参数同名。 例如: void Object::SetValue(int width, int height) { m_width = width; m_height = height; } 【规则3-2-7】为了防止某一软件库中的一些标识符和其它软件库中的冲突,可以为 12 www.thirdstudio.com.cn 各种标识符加上能反映软件性质的前缀。例如三维图形标准OpenGL 的所有库函数均以gl 开 头,所有常量(或宏定义)均以GL 开头。 13 www.thirdstudio.com.cn 4 表达式和基本语句 表达式和语句都属于C++/C 的短语结构语法。它们看似简单,但使用时隐患比较多。本章 归纳了正确使用表达式和语句的一些规则与建议。 4.1 运算符的优先级 C++/C 语言的运算符有数十个,运算符的优先级与结合律如表4-1 所示。注意一元运算符 + - * 的优先级高于对应的二元运算符。 表 4-1 运算符的优先级与结合律 【规则4-1-1】如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的 优先级。 熟记表4-1 是比较困难的,所以为了防止产生歧义并提高可读性,应当用括号确定表达式 的操作顺序。例如: word = (high << 8) | low if ((a | b) && (a & c)) 4.2 复合表达式 如 a = b = c = 0 这样的表达式称为复合表达式。允许复合表达式存在的理由是: (1)书写简洁;(2)可以提高编译效率。(但要防止滥用复合表达式。) 14 www.thirdstudio.com.cn 【规则4-2-1】禁止编写太复杂的复合表达式。 例如: i = a >= b && c < d && c + f <= g + h ; // 复合表达式过于复杂 【规则4-2-2】禁止使用有多用途的复合表达式。 例如: d = (a = b + c) + r ; 该表达式既求a 值又求d 值。应该拆分为两个独立的语句: a = b + c; d = a + r; 【规则4-2-3】禁止把程序中的复合表达式与“真正的数学表达式”混淆。 例如: if (a < b < c) // a < b < c 是数学表达式而不是程序表达式 它并不表示 if ((a=”或“<=”形式。假设浮点变量的 名字为x,应当将if (x == 0.0) // 隐含错误的比较转化为 if ((x>=-EPSINON) && (x<=EPSINON)) 其中EPSINON 是允许的误差(即精度)。 4.3.4 指针变量与零值比较 【规则4-3-4】应当将指针变量用“==”或“!=”与NULL 比较。 指针变量的零值是“空”(记为NULL)。尽管NULL 的值与0 相同,但是两者意义不同。假 设指针变量的名字为p,它与零值比较的标准if 语句如下: if (p == NULL) // p 与NULL 显式比较,强调p 是指针变量 if (p != NULL) 不要写成 if (p == 0) // 容易让人误解p 是整型变量 if (p != 0) 或者 if (p) // 容易让人误解p 是布尔变量 if (!p) 4.3.5 对 if 语句的补充说明 对于 if (NULL == p) 这样的格式,不是程序写错了,而是程序员为了防止将 if (p == NULL) 误写成 if (p = NULL),而有意把p 和NULL 颠倒。编译器认为 if (p = NULL) 是合法的,但是 会指出 if (NULL = p)是错误的,因为NULL不能被赋值。 程序中有时会遇到if/else/return 的组合,应该将如下不良风格的程序 if (condition) return x; return y; 改写为 if (condition) { return x; } else { return y; } 或者改写成更加简练的 return (condition ? x : y); 16 www.thirdstudio.com.cn 4.4 循环语句的效率 C++/C 循环语句中,for 语句使用频率最高,while 语句其次,do 语句很少用。本节重点 论述循环体的效率。提高循环体效率的基本办法是降低循环体的复杂性。 【规则4-4-1】在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最 外层,以减少CPU 跨切循环层的次数。例如示例4-4(b)的效率比示例4-4(a)的高。 示例 4.4(a)低效率:长循环在最外层 (b) 高效率:长循环在最内层 【规则 4-4-2】如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外 面。示例 4-4(c)的程序比示例 4-4(d)多执行了N-1 次逻辑判断。并且由于前者总是进行逻辑判 断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,从而降低了效率。如 果N 非常大,最好采用示例 4-4(d)的写法,可以提高效率。如果N 非常小,两者效率差别并不 明显,采用示例 4-4(c)的写法比较好,因为程序更加简洁。 (c) 效率低但程序简洁 (d) 效率高但程序不简洁 4.5 for 语句的循环控制变量 【规则4-5-1】禁止在for 循环体内修改循环变量,以此防止for 循环失去控制。 【规则4-5-2】建议for 语句的循环控制变量的取值采用“半开半闭区间”写法。示例4-5(a)中的 17 www.thirdstudio.com.cn x 值属于半开半闭区间“0 =< x < N”,起点到终点的间隔为N,循环次数为N。 示例4-5(b)中的x 值属于闭区间“0 =< x <= N-1”,起点到终点的间隔为N-1,循环次数为 N。相比之下,示例4-5(a)的写法更加直观,尽管两者的功能是相同的。 示例4-5(a) 循环变量属于半开半闭区间 (b) 循环变量属于闭区间 4.6 switch 语句 有了if 语句为什么还要switch 语句? switch 是多分支选择语句,而if 语句只有两个分支可供选择。虽然可以用嵌套的if 语句来实现 多分支选择,但那样的程序冗长难读。这是switch 语句存在的理由。switch 语句的基本格式是: switch (variable) { case value1 : ⋯ break; case value2 : ⋯ break; ⋯ default : ⋯ break; } 【规则4-6-1】每个case 语句的结尾不要忘了加break,否则将导致多个分支重叠(除非有意使 多个分支重叠)。 【规则4-6-2】切莫忘记最后那个default 分支。即使程序真的不需要default 处理,也应该保留 语句 default : break; 这样做是为了防止别人误以为你忘了default 处理。 4.7 goto 语句 自从提倡结构化设计以来,goto 就成了有争议的语句。首先,由于goto 语句可以灵活跳 转,如果不加限制,它的确会破坏结构化设计风格。其次,goto 语句经常带来错误或隐患。它 可能跳过了某些对象的构造、变量的初始化、重要的计算等语句,例如: goto state; String s1, s2; // 被goto 跳过 int sum = 0; // 被goto 跳过 ⋯ state: ⋯ 如果编译器不能发觉此类错误,每用一次goto 语句都可能留下隐患。很多人建议废除 C++/C 的goto 语句,以绝后患。但实事求是地说,错误是程序员自己造成的,不是goto 的过 18 www.thirdstudio.com.cn 错。goto 语句至少有一处可显神通,它能从多重循环体中咻地一下子跳到外面,用不着写很多 次的break 语句; 例如 { ⋯ { ⋯ { ⋯ goto error; } } } error: ⋯ 建议慎用goto 语句。 19 www.thirdstudio.com.cn 5 常量 常量是一种标识符,它的值在运行期间恒定不变。C 语言用 #define 来定义常量(称为宏 常量)。C++ 语言除了 #define 外还可以用const 来定义常量(称为const 常量)。 5.1 为什么需要常量 如果不使用常量,直接在程序中填写数字或字符串,将会有什么麻烦? (1) 程序的可读性(可理解性)变差。程序员自己会忘记那些数字或字符串是什么意思,用 户则更不能得知它们从何处来、表示什么。 (2) 在程序的很多地方输入同样的数字或字符串,难保不发生书写错误。 (3) 如果要修改数字或字符串,则需要在很多地方改动,既麻烦又容易出错。 【规则5-1-1】 尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。 例如: #define MAX 100 /* C 语言的宏常量 */ const int MAX = 100; // C++ 语言的const 常量 const float PI = 3.14159; // C++ 语言的const 常量 5.2 const 与 #define 的比较 C++ 语言可以用const 来定义常量,也可以用 #define 来定义常量。但是前者比后 者有更多的优点: (1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安 全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会 产生意料不到的错误(边际效应)。 (2) 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调 试。 【规则5-2-1】在C++ 程序中只使用const 常量而不使用宏常量,即const 常量完 全取代宏常量。 5.3 常量定义规则 【规则5-3-1】需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。 为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。 【规则5-3-2】如果某一常量与其它常量密切相关,则应在定义中包含这种关系,而不应给出一 些孤立的值。 例如: const float RADIUS = 100; const float DIAMETER = RADIUS * 2; 5.4 类中的常量 有时我们希望某些常量只在类中有效。由于#define 定义的宏常量是全局的,不能达到目 的,于是想当然地觉得应该用const 修饰数据成员来实现。const 数据成员的确是存在的,但 20 www.thirdstudio.com.cn 其含义却不是我们所期望的。const 数据成员只在某个对象生存期内是常量,而对于整个类而 言却是可变的,因为类可以创建多个对象,不同的对象其const 数据成员的值可以不同。 不能在类声明中初始化const 数据成员。以下用法是错误的,因为类的对象未被创建时, 编译器不知道SIZE 的值是什么。 class A {⋯ const int SIZE = 100; // 错误,企图在类声明中初始化const 数据成员 int array[SIZE]; // 错误,未知的SIZE }; const 数据成员的初始化只能在类构造函数的初始化表中进行,例如 class A {⋯ A(int size); // 构造函数 const int SIZE ; }; A::A(int size) : SIZE(size) // 构造函数的初始化表 { ⋯ } A a(100); // 对象 a 的SIZE 值为100 A b(200); // 对象 b 的SIZE 值为200 怎样才能建立在整个类中都恒定的常量呢?应该用类中的枚举常量来实现。例如 class A {⋯ enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量 int array1[SIZE1]; int array2[SIZE2]; }; 枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的 隐含数据类型是整数,其最大值有限,且不能表示浮点数(如PI=3.14159) 21 www.thirdstudio.com.cn 6 函数设计 函数是C++/C程序的基本功能单元,其重要性不言而喻。函数设计的细微缺点很容易导致 该函数被错用,所以仅仅使函数的功能正确是不够的。本章重点论述函数的接口设计和内部实 现的一些规则。 函数接口的两个要素是参数和返回值。C 语言中,函数的参数和返回值的传递方式有两种: 值传递(pass by value)和指针传递(pass by pointer)。C++语言中多了引用传递(pass by reference)。由于引用传递的性质像指针传递,而使用方式却像值传递,容易引起混乱,请先 阅读6.6 节“引用与指针的比较”。 6.1 参数的规则 【规则6-1-1】参数的书写要完整,禁止只写参数的类型而省略参数名字。函数没有参数时,用 void 填充。 例如: void SetValue(int width, int height); // 良好的风格 void SetValue(int, int); // 不良的风格 float GetValue(void); // 良好的风格 float GetValue(); // 不良的风格 【规则6-1-2】参数命名要恰当,顺序要合理。 例如编写字符串拷贝函数StringCopy,它有两个参数。如果把参数名字起为str1 和str2, 例如 void StringCopy(char *str1, char *str2); 那么我们就不清楚是应把str1 拷贝到str2 中,还是应倒过来。介于此,我们可以把参数名 字起得更有意义,如叫strSource 和strDestination。这样从名字上就可以看出应该把strSource 拷贝到strDestination。还有一个问题就是,这两个参数哪一个应该在前面哪一个应该在后面? 参数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。如果将 函数声明为: void StringCopy(char *strSource, char *strDestination); 别人在使用时可能会不假思索地写成如下形式: char str[20]; StringCopy(str, “Hello World”); // 参数顺序颠倒 【规则6-1-3】参数是指针时,且仅作输入使用时,则应在类型前加const,以防止该指针在函数 体内被修改。 例如: void StringCopy(char *strDestination,const char *strSource); 【规则6-1-4】如果输入参数以值传递的方式传递对象,则改用“const &”方式来传递,这样可以 省去临时对象的构造和析构过程,从而提高效率。 【规则6-1-5】禁止函数有太多的参数,参数个数尽量控制在5个以内。如果参数太多,则使用 时很容易将参数类型或顺序搞错。 【规则6-1-6】禁止使用类型和数目不确定的参数。 C 标准库函数printf 是采用不确定参数的典型代表,其原型为: int printf(const char *format[, argument] …); 这种风格的函数在编译时丧失了严格的类型安全检查。 22 www.thirdstudio.com.cn 6.2 返回值的规则 【规则6-2-1】禁止省略返回值的类型。 C 语言中,凡不加类型说明的函数,一律自动按整型处理。这样做容易被误解为void 类型。 C++语言有很严格的类型安全检查,不允许上述情况发生。由于C++程序可以调用C 函数, 为了避免混乱,规定任何C++/ C 函数都必须有类型。如果函数没有返回值,那么应声明为void 类型。 【规则6-2-2】函数名字与返回值类型在语义上不可冲突。 违反这条规则的典型代表是C 标准库函数getchar。 例如: char c; c = getchar(); if (c = = EOF) … 按照getchar 名字的意思,将变量c 声明为char 类型是很自然的事情。但getchar 的确不 是char 类型,而是int 类型,其原型如下: int getchar(void); 由于c 是char 类型,取值范围是[-128,127],如果宏EOF 的值在char 的取值范围之外, 那么if 语句将总是失败。导致本例错误的责任并不在用户,是函数getchar 误导了使用者。 【规则6-2-3】禁止将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用 return 语句返回。回顾上例,C 标准库函数的设计者为什么要将getchar 声明为int 类型呢? 在正常情况下,getchar 的确返回单个字符,但如果getchar 碰到文件结束标志或发生读错误, 它必须返回一个标志EOF。为了区别于正常的字符,只好将EOF 定义为负数(通常为-1)。因 此函数getchar 就成了int 类型。 我们在实际工作中,经常会碰到上述问题。为了避免出现误解,应该将正常值和错误标志 分开。即:正常值用输出参数获得,而错误标志用return 语句返回。 函数getchar 可以改写成 BOOL GetChar(char *c); 虽然gechar 比GetChar 灵活,例如 putchar(getchar()); 但是如果getchar 用错了,它的灵 活性又有什么用呢? 【规则6-2-4】有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返 回值。 例如字符串拷贝函数strcpy 的原型: char *strcpy(char *strDest,const char *strSrc); strcpy 函数将strSrc 拷贝至输出参数strDest 中,同时函数的返回值又是strDest。这样做 并非多此一举,可以获得如下灵活性: char str[20]; int length = strlen( strcpy(str, “Hello World”) ); 【规则6-2-5】当函数的返回值是一个对象时,有些场合用“引用传递”替换“值传递”可以提高效 率;而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。 例如: class String {… // 赋值函数 String & operate=(const String &other); // 相加函数,如果没有friend 修饰则只许有一个右侧参数 friend String operate+( const String &s1, const String &s2); private: char *m_data; } 23 www.thirdstudio.com.cn String 的赋值函数operate = 的实现如下: String & String::operate=(const String &other) { if (this == &other) return *this; delete m_data; m_data = new char[strlen(other.data)+1]; strcpy(m_data, other.data); return *this; // 返回的是 *this 的引用,无需拷贝过程 } 对于赋值函数,应当用“引用传递”的方式返回String 对象。如果用“值传递”的方式,虽然 功能仍然正确,但由于return 语句要把 *this 拷贝到保存返回值的外部存储单元之中,增加了 不必要的开销,降低了赋值函数的效率。例如: String a,b,c; … a = b; // 如果用“值传递”,将产生一次 *this 拷贝 a = b = c; // 如果用“值传递”,将产生两次 *this 拷贝 String 的相加函数operate + 的实现如下: String operate+(const String &s1, const String &s2) { String temp; delete temp.data; // temp.data 是仅含‘\0’的字符串 temp.data = new char[strlen(s1.data) + strlen(s2.data) +1]; strcpy(temp.data, s1.data); strcat(temp.data, s2.data); return temp; } 对于相加函数,应当用“值传递”的方式返回String 对象。如果改用“引用传递”,那么函数 返回值是一个指向局部对象temp 的“引用”。由于temp 在函数结束时被自动销毁,将导致返回 的“引用”无效。例如: c = a + b; 此时 a + b 并不返回期望值,c 什么也得不到,留下了隐患。 6.3 函数内部实现的规则 不同功能的函数其内部实现各不相同,看起来似乎无法就“内部实现”达成一致的观点。但 根据经验,我们可以在函数体的“入口处”和“出口处”从严把关,从而提高函数的质量。 【规则6-3-1】在函数体的“入口处”,对参数的有效性进行检查。很多程序错误是由非法参数引 起的,我们应该充分理解并正确使用“断言”(assert)来防止此类错误。详见6.5 节“使用断言”。 【规则6-3-2】在函数体的“出口处”,对return 语句的正确性和效率进行检查。如果函数有返回 值,那么函数的“出口处”是return 语句。我们不要轻视return语句。如果return 语句写得不好, 函数要么出错,要么效率低下。 注意事项如下: (1)return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自 动销毁。例如 char * Func(void) { char str[] = “hello world”; // str 的内存位于栈上 … 24 www.thirdstudio.com.cn return str; // 将导致错误 } (2)应搞清楚返回的究竟是“值”、“指针”还是“引用”。 (3)如果函数返回值是一个对象,应考虑return 语句的效率。 例如 return String(s1 + s2); 这是临时对象的语法,表示“创建一个临时对象并返回它”。不要以为它与“先创建一个局部 对象temp 并返回它的结果”是等价的,如 String temp(s1 + s2); return temp; 实质不然,上述代码将发生三件事。首先,temp 对象被创建,同时完成初始化;然后拷 贝构造函数把temp 拷贝到保存返回值的外部存储单元中;最后,temp 在函数结束时被销毁(调 用析构函数)。然而“创建一个临时对象并返回它”的过程是不同的,编译器直接把临时对象创 建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率。 类似地,我们不应将 return int(x + y); // 创建一个临时变量并返回它 写成 int temp = x + y; return temp; 由于内部数据类型如int,float,double 的变量不存在构造函数与析构函数,虽然该“临时变量 的语法”不会提高多少效率,但是程序更加简洁易读。 6.4 其它规则 【规则6-4-1】函数的功能要单一,禁止设计多用途的函数。 【规则6-4-2】函数体的规模要小,应控制在50 行代码之内。 【规则6-4-3】函数应带有“记忆”功能。相同的输入应当产生相同的输出。 带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状 态”。这样的函数既不易理解又不利于测试和维护。在C/C++语言中,函数的static 局部变量是 函数的“记忆”存储器。建议尽量少用static 局部变量,除非必需。 【规则6-4-4】不仅应检查输入参数的有效性,还应检查通过其它途径进入函数体内的变量的有 效性,例如全局变量、文件句柄等。 【规则6-4-5】用于出错处理的返回值必须清晰,让使用者不容易忽视或误解错误情况。 6.5 使用断言 程序一般分为Debug 版本和Release 版本,Debug 版本用于内部调试,Release 版本发行 给用户使用。 断言assert 是仅在Debug 版本起作用的宏,它用于检查“不应该”发生的情况。示例6-5 是 一个内存复制函数。在运行过程中,如果assert 的参数为假,那么程序就会中止(一般地还会 出现提示对话,说明在什么地方引发了assert)。 25 www.thirdstudio.com.cn 示例6-5 复制不重叠的内存块 assert 不是一个仓促拼凑起来的宏。为了不在程序的Debug 版本和Release 版本引起差别, assert 不应该产生任何副作用。所以assert 不是函数,而是宏。程序员可以把assert 看成一个 在任何系统状态下都可以安全使用的无害测试手段。如果程序在assert处终止了,并不是说含有 该assert 的函数有错误,而是调用者出了差错,assert 可以帮助我们找到发生错误的原因。 很少有比跟踪到程序的断言,却不知道该断言的作用更让人沮丧的事了。你花了很多时间, 不是为了排除错误,而只是为了弄清楚这个错误到底是什么。有的时候,程序员偶尔还会设计 出有错误的断言。所以如果搞不清楚断言检查的是什么,就很难判断错误是出现在程序中,还 是出现在断言中。幸运的是这个问题很好解决,只要加上清晰的注释即可。这本是显而易见的 事情,可是很少有程序员这样做。难以理解的断言常常被程序员忽略,甚至被删除。 【规则6-5-1】使用断言捕捉不应该发生的非法情况。避免混淆非法情况与错误情况之间的区别, 后者是必然存在的并且是必须作出处理的。 【规则6-5-2】在函数的入口处,使用断言检查参数的有效性(合法性)。 【规则6-5-3】在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定 了的假定,就要使用断言对假定进行检查。 【规则6-5-4】一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错 误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。 6.6 引用与指针的比较 引用是C++中的概念,编程者容易把引用和指针混淆一起。以下程序中,n 是m 的一个引 用(reference),m 是被引用物(referent)。 int m; int &n = m; n 相当于m 的别名(绰号),对n 的任何操作就是对m 的操作。所以n 既不是m 的拷贝, 也不是指向m 的指针,其实n 就是m 它自己。 引用的一些规则如下: (1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。 (2)不能有NULL 引用,引用必须与合法的存储单元关联(指针则可以是NULL)。 (3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。 以下示例程序中,k 被初始化为i 的引用。语句k = j 并不能将k 修改成为j 的引用,只是 把k 的值改变成为6。由于k 是i 的引用,所以i 的值也变成了6。 int i = 5; int j = 6; 26 www.thirdstudio.com.cn int &k = i; k = j; // k 和i 的值都变成了6; 引用的主要功能是传递函数的参数和返回值。C++语言中,函数的参数和返回值的传递方 式有三种:值传递、指针传递和引用传递。 以下是“值传递”的示例程序。由于Func1 函数体内的x 是外部变量n 的一份拷贝,改变x 的 值不会影响n, 所以n 的值仍然是0。 void Func1(int x) { x = x + 10; } … int n = 0; Func1(n); cout << “n = ” << n << endl; // n = 0 以下是“指针传递”的示例程序。由于Func2 函数体内的x 是指向外部变量n 的指针,改变 该指针的内容将导致n 的值改变,所以n 的值成为10。 void Func2(int *x) { (* x) = (* x) + 10; } … int n = 0; Func2(&n); cout << “n = ” << n << endl; // n = 10 以下是“引用传递”的示例程序。由于Func3 函数体内的x 是外部变量n 的引用,x 和n 是 同一个东西,改变x 等于改变n,所以n 的值成为10。 void Func3(int &x) { x = x + 10; } … int n = 0; Func3(n); cout << “n = ” << n << endl; // n = 10 对比上述三个示例程序,会发现“引用传递”的性质像“指针传递”,而书写方式像“值传递”。 实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要使用“引用”? 答案是“用适当的工具做恰如其分的工作”。 指针能够毫无约束地操作内存中的任何东西,尽管指针功能强大,但是非常危险。如果的 确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。 27 www.thirdstudio.com.cn 7 内存管理 欢迎进入内存这片雷区。伟大的Bill Gates 曾经失言: 640K ought to be enough for everybody — Bill Gates 1981 程序员编写内存管理程序,经常会出现各种各样的问题。唯一的解决问题办法就是发现所 有潜伏的问题并且排除它们。本章的内容比一般教科书的内容要深入得多,请细心阅读,做到 真正地通晓内存管理。 7.1 内存分配方式 内存分配方式有三种: (1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整 个运行期间都存在。例如全局变量,static变量。 (2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数 执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高, 但是分配的内存容量有限。 (3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意个内存, 程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由我们决定,使用非常灵 活,但问题也最多。 7.2 常见的内存错误及其对策 发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时 才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。常见的内存错 误及其对策如下: 1、内存分配未成功,却使用了该内存区域。 编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使 用内存之前检查指针是否为NULL。如果指针p 是函数的参数,那么在函数的入口处用 assert(p!=NULL)进行检查。如果是用malloc或new 来申请内存,应该用if(p==NULL)或 if(p!=NULL)进行防错处理。 2、内存分配虽然成功,但是尚未初始化就引用它。 犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零, 导致引用初值错误(例如数组)。 内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,但有时可能杂乱无 章。所以无论用何种方式创建数组,都必须赋给初值,即便是赋零值也不可省略。 3、内存分配成功并且已经初始化,但操作越过了内存的边界。 例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for 循环语句中,循环次 数很容易搞错,导致数组操作越界。 4、忘记了释放内存,造成内存泄露。 含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到 错误。终有一次程序突然死掉,系统出现提示:内存耗尽。动态内存的申请与释放必须配对, 程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete 同理)。 5、释放了内存却继续使用它。 有三种情况: 28 www.thirdstudio.com.cn (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,防止产生“野指针”。 7.3 指针与数组的对比 C++/C 程序中,指针和数组在很多地方可以相互替换使用,这让人产生一种错觉,以为两 者是等价的。 数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不 是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。 指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针操作动态内 存。指针远比数组灵活,但也更危险。 下面以字符串为例比较指针与数组的特性。 7.3.1 修改内容 示例7-3-1 中,字符数组a 的容量是6 个字符,其内容为hello\0。a 的内容可以改变,如a[0]= ‘X’。指针p 指向常量字符串“world”(位于静态存储区,内容为world\0),常量字符串的内容 是不可以被修改的。从语法上看,编译器并不觉得语句p[0]= ‘X’有什么不妥,但是该语句企图 修改常量字符串的内容而导致运行错误。 示例7-3-1 修改数组和指针的内容 7.3.2 内容复制与比较 不能对数组名进行直接复制与比较。示例7-3-2 中,若想把数组a 的内容复制给数组b,不 能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy 进行复制。同理,比较b 和a 的内容是否相同,不能用if(b==a) 来判断,应该用标准库函数strcmp进行比较。 29 www.thirdstudio.com.cn 语句p = a 并不能把a 的内容复制指针p,而是把a 的地址赋给了p。要想复制a的内容,可 以先用库函数malloc 为p 申请一块容量为strlen(a)+1 个字符的内存,再用strcpy 进行字符串复 制。同理,语句if(p==a) 比较的不是内容而是地址,应该用库函数strcmp 来比较。 示例7-3-2 数组和指针的内容复制与比较 7.3.3 计算内存容量 用运算符sizeof 可以计算出数组的容量(字节数)。示例7-3-3(a)中,sizeof(a)的值是12 (注意别忘了’\0’)。指针p 指向a,但是sizeof(p)的值却是4。这是因为sizeof(p)得到的是一个指 针变量的字节数,相当于sizeof(char*),而不是p 所指的内存容量。C++/C 语言没有办法知道指 针所指的内存容量,除非在申请内存时记住它。 注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。示例7-3-3(b) 中,不论数组a 的容量是多少,sizeof(a)始终等于sizeof(char *)。 示例7-3-3(a) 计算数组和指针的内存容量 30 www.thirdstudio.com.cn 示例7-3-3(b) 数组退化为指针 7.4 指针参数是如何传递内存的? 如果函数的参数是一个指针,不应指望用该指针去申请动态内存。示例7-4-1 中, Test 函数的语句GetMemory(str, 200)并没有使str 获得期望的内存,str 依旧是NULL, 为什么? 示例7-4-1 试图用指针参数申请动态内存 毛病出在函数GetMemory 中。编译器总是要为函数的每个参数制作临时副本,指针 参数p 的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p 的内容,就导致 参数p 的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p 申请 了新的内存,只是把_p 所指的内存地址改变了,但是p 丝毫未变。所以函数GetMemory 并不能输出任何东西。事实上,每执行一次GetMemory 就会泄露一块内存,因为没有用 free 释放内存。 如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见示例7-4-2。 31 www.thirdstudio.com.cn 示例7-4-2 用指向指针的指针申请动态内存 由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这 种方法更加简单,见示例7-4-3。 示例7-4-3 用函数返回值来传递动态内存 用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return 语句用错了。这 里强调不要用return 语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见示 32 www.thirdstudio.com.cn 例7-4-4。 示例7-4-4 return 语句返回指向“栈内存”的指针 用调试器逐步跟踪Test4,发现执行str = GetString 语句后str 不再是NULL 指针,但是str 的 内容不是“hello world”而是垃圾。 如果把示例7-4-4 改写成示例7-4-5,会怎么样? 示例7-4-5 return 语句返回常量字符串 函数Test5 运行虽然不会出错,但是函数GetString2 的设计概念却是错误的。因为GetString2 内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候 调用GetString2,它返回的始终是同一个“只读”的内存块。 33 www.thirdstudio.com.cn 7.5 free 和 delete 把指针怎么啦? free 和delete(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本 身释放掉。 用调试器跟踪示例7-5,发现指针p 被free 以后其地址仍然不变(非NULL),只是该地址 对应的内存是垃圾,p 成了“野指针”。如果此时不把p 设置为NULL,会让人误以为p 是个合 法的指针。 如果程序比较长,我们有时记不住p 所指的内存是否已经被释放,在继续使用p 之前,通 常会用语句if (p != NULL)进行防错处理。很遗憾,此时if 语句起不到防错作用,因为即便p 不 是NULL 指针,它也不指向合法的内存块。 示例7-5 p 成为野指针 7.6 动态内存会被自动释放吗? 函数体内的局部变量在函数结束时自动消亡。很多人误以为示例7-6 是正确的。理由是p 是 局部的指针变量,它消亡的时候会让它所指的动态内存一起消亡。但这是错觉! 示例7-6 试图让动态内存自动释放 我们发现指针有一些“似是而非”的特征: (1)指针消亡了,并不表示它所指的内存会被自动释放。 (2)内存被释放了,并不表示指针会消亡或者成了NULL 指针。 这表明释放内存并不是一件可以草率对待的事。有人会想: 如果程序终止了运行,一切指针都会消亡,动态内存会被操作系统回收。既然如此, 34 www.thirdstudio.com.cn 在程序临终前,就可以不必释放内存、不必将指针设置为 NULL 了。终于可以偷懒而不会发生 错误了吧? 想得美。如果别人把那段程序取出来用到其它地方怎么办? 7.7 杜绝“野指针” “野指针”不是NULL 指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因 为用if 语句很容易判断。但是“野指针”是很危险的,if 语句对它不起作用。“野指针”的成因主 要有两种: (1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL 指针,它的缺省 值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为 NULL,要么让它指向合法的内存。 例如 char *p = NULL; char *str = (char *) malloc(100); (2)指针p 被free 或者delete 之后,没有置为NULL,让人误以为p 是个合法的指针。 参见7.5 节。 (3)指针操作超越了变量的作用范围。这种情况让人防不胜防,示例程序如下: class A { public: void Func(void){ cout << “Func of class A” << endl; } }; void Test(void) { A *p; { A a; p = &a; // 注意 a 的生命期 } p->Func(); // p 是“野指针” } 函数Test 在执行语句p->Func()时,对象a 已经消失,而p 是指向a 的,所以p 就成了“野 指针”。但奇怪的是我运行这个程序时居然没有出错,这可能与编译器有关。 7.8 有了 malloc/free 为什么还要 new/delete ? malloc 与free 是C++/C 语言的标准库函数,new/delete 是C++的运算符。它们都可用于 申请动态内存和释放内存。 对于非内部数据类型的对象而言,光用maloc/free 无法满足动态对象的要求。对象在创建 的同时要自动执行构造函数, 对象在消亡之前要自动执行析构函数。由于malloc/free 是库函 数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free。 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清 理与释放内存工作的运算符delete。注意new/delete 不是库函数。 我们先看一看malloc/free 和new/delete 如何实现对象的动态内存管理,见示例7-8。 35 www.thirdstudio.com.cn 示例7-8 用malloc/free 和new/delete 如何实现对象的动态内存管理 类Obj 的函数Initialize 模拟了构造函数的功能,函数Destroy 模拟了析构函数的功能。函 数UseMallocFree 中,由于malloc/free 不能执行构造函数与析构函数,必须调用成员函Initialize 和Destroy 来完成初始化与清除工作。函数UseNewDelete 则简单得多。 所以我们不要企图用malloc/free 来完成动态对象的内存管理,应该用new/delete。由于内 部数据类型的“ 对象”没有构造与析构的过程,对它们而言malloc/free 和new/delete 是等价的。 既然new/delete 的功能完全覆盖了malloc/free,为什么C++不把malloc/free 淘汰出局呢?这 是因为C++程序经常要调用C 函数,而C 程序只能用malloc/free 管理动态内存。 如果用free 释放“new 创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序 出错。如果用delete 释放“malloc 申请的动态内存”,理论上讲程序不会出错,但是该程序的可 读性很差。所以new/delete 必须配对使用,malloc/free 也一样。 36 www.thirdstudio.com.cn 7.9 内存耗尽怎么办? 如果在申请动态内存时找不到足够大的内存块,malloc 和new 将返回NULL 指针,宣告 内存申请失败。通常有三种方式处理“内存耗尽”问题。 (1) 判断指针是否为NULL,如果是则马上用return 语句终止本函数。 例如: void Func(void) { A *a = new A; if(a == NULL) { return; } … } (2) 判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。 例如: void Func(void) { A *a = new A; if(a == NULL) { cout << “Memory Exhausted” << endl; exit(1); } … } (3)为 new 和malloc 设置异常处理函数。例如Visual C++可以用_set_new_hander 函数为new 设置用户自己定义的异常处理函数,也可以让malloc 享用与new 相同的异常处理函数。详细内 容请参考C++使用手册。 上述(1)( 2)方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么方式(1) 就显得力不从心(释放内存很麻烦),应该用方式(2)来处理。 很多人不愿用exit(1),问:“不编写出错处理程序,让操作系统自己解决行不行?” 不行。如果发生“内存耗尽”这样的事情,一般说来应用程序已经无药可救。如果不用exit(1) 把坏程序杀死,它可能会害死操作系统。 有一个很重要的现象要告诉大家。对于32 位以上的应用程序而言,无论怎样使用malloc 与 new,几乎不可能导致“内存耗尽”。在Windows 98 下用Visual C++编写了测试程序,见示例7-9。 这个程序会无休止地运行下去,根本不会终止。因为32 位操作系统支持“虚存”,内存用完了, 自动用硬盘空间顶替。 可以得出这么一个结论:对于32 位以上的应用程序,“内存耗尽”错误处理程序毫无用处。 但是必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。 37 www.thirdstudio.com.cn 示例7-9 试图耗尽操作系统的内存 7.10 malloc/free 的使用要点 函数malloc 的原型如下: void * malloc(size_t size); 用malloc 申请一块长度为length 的整数类型的内存,程序如下: int *p = (int *) malloc(sizeof(int) * length); 应当把注意力集中在两个要素上:“类型转换”和“sizeof”。 (1)malloc 返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将 void * 转换成所需要的指针类型。 (2)malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数我 们通常记不住int, float 等数据类型的变量的确切字节数。例如int 变量在16 位系统下是2 个字节,在32 位下是4 个字节;而float 变量在16 位系统下是4 个字节,在32 位下也是4 个字节。最好用以下程序作一次测试: cout << sizeof(char) << endl; cout << sizeof(int) << endl; cout << sizeof(unsigned int) << endl; cout << sizeof(long) << endl; cout << sizeof(unsigned long) << endl; cout << sizeof(float) << endl; cout << sizeof(double) << endl; cout << sizeof(void *) << endl; 在malloc的“()”中使用sizeof 运算符是良好的风格,但要当心,写出 p = malloc(sizeof(p))这 样的程序来。 (3)函数free 的原型如下: void free( void * memblock ); 为什么free 函数不像malloc函数那样复杂呢?这是因为指针p 的类型以及它所指的内存的 容量事先都是知道的,语句free(p)能正确地释放内存。如果p是NULL 指针,那么free对p无论操 作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错 误。 38 www.thirdstudio.com.cn 7.11 new/delete 的使用要点 运算符new 使用起来要比函数malloc 简单得多,例如: int *p1 = (int *)malloc(sizeof(int) * length); int *p2 = new int[length]; 这是因为new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而 言,new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new 的语 句也可以有多种形式。例如 class Obj { public : Obj(void); // 无参数的构造函数 Obj(int x); // 带一个参数的构造函数 … } void Test(void) { Obj *a = new Obj; Obj *b = new Obj(1); // 初值为1 … delete a; delete b; } 如果用new 创建对象数组,那么只能使用对象的无参数构造函数。例如 Obj *objects = new Obj[100]; // 创建100 个动态对象 不能写成 Obj *objects = new Obj[100](1);// 创建100 个动态对象的同时赋初值1 在用delete 释放对象数组时,留意不要丢了符号‘[]’。例如 delete []objects; // 正确的用法 delete objects; // 错误的用法 后者相当于delete objects[0],漏掉了另外99 个对象。 7.12 林锐的心得体会 不少技术不错的C++/C 程序员,很少有人能拍拍胸脯说通晓指针与内存管理。我最初学习 C 语言时特别怕指针,导致我开发第一个应用软件(约1万行C 代码)时没有使用一个指针, 全用数组来顶替指针,实在蠢笨得过分。躲避指针不是办法,后来我改写了这个软件,代码量 缩小到原先的一半。 经验教训是: (1)越是怕指针,就越要使用指针。不会正确使用指针,肯定算不上是合格的程序员。 (2)必须养成“使用调试器逐步跟踪程序”的习惯,只有这样才能发现问题的本质。 8 其它编程经验 8.1 使用 const 提高函数的健壮性 看到const 关键字,C++程序员首先想到的可能是const 常量。这可不是良好的条件反射。 39 www.thirdstudio.com.cn 如果只知道用const 定义常量,那么相当于把火药仅用于制作鞭炮。const 更大的魅力是它可以 修饰函数的参数、返回值,甚至函数的定义体。 const 是constant 的缩写,“恒定不变”的意思。被const 修饰的东西都受到强制保护,可以 预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Useconst whenever you need”。 8.1.1 用 const 修饰函数的参数 如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”, 都不能加const 修饰,否则该参数将失去输出功能。 const 只能修饰输入参数: (1)如果输入参数采用“指针传递”,那么加const 修饰可以防止意外地改动该指针,起到保护 作用。 例如StringCopy 函数: void StringCopy(char *strDestination, const char *strSource); 其中strSource 是输入参数,strDestination 是输出参数。给strSource 加上const修饰后,如果函 数体内的语句试图改动strSource 的内容,编译器将指出错误。 (2)如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数 本来就无需保护,所以不要加const 修饰。 例如不要将函数void Func1(int x) 写成void Func1(const int x)。同理不要将函数void Func2(A a) 写成void Func2(const A a)。其中A 为用户自定义的数据类型。 (3)对于非内部数据类型的参数而言,象void Func(A a) 这样声明的函数注定效率比较底。因 为函数体内将产生A 类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过 程都将消耗时间。 为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅借用一下参数的 别名而已,不需要产生临时对象。但是函数void Func(A &a) 存在一个缺点:“引用传递”有可能 改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为void Func(const A &a)。 以此类推,是否应将void Func(int x) 改写为void Func(const int &x),以便提高效率?完全没 有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引 用传递”的效率几乎相当。 问题是如此的缠绵,我只好将“const &”修饰输入参数的用法总结一下,如表11-1-1所示。 示例8-1-1“const &”修饰输入参数的规则 40 www.thirdstudio.com.cn 8.1.2 用 const 修饰函数的返回值 (1)如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)的内容 不能被修改,该返回值只能被赋给加const 修饰的同类型指针。 例如函数 const char * GetString(void); 如下语句将出现编译错误: char *str = GetString(); 正确的用法是 const char *str = GetString(); (2)如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中, 加const 修饰没有任何价值。 例如不要把函数int GetInt(void) 写成const int GetInt(void)。 同理不要把函数A GetA(void) 写成const A GetA(void),其中A 为用户自定义的数据类型。 如果返回值不是内部数据类型,将函数A GetA(void) 改写为const A & GetA(void)的确能提 高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的拷贝”还是仅 返回“别名”就可以了,否则程序会出错。见6.2 节“返回值的规则”。 (3)函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的 是为了实现链式表达。 例如: class A { A & operate = (const A &other); // 赋值函数 }; A a, b, c; // a, b, c 为A 的对象 … a = b = c; // 正常的链式赋值 (a = b) = c; // 不正常的链式赋值,但合法 如果将赋值函数的返回值加const 修饰,那么该返回值的内容不允许被改动。上例中,语句 a = b = c 仍然正确,但是语句 (a = b) = c 则是非法的。 8.1.3 const 成员函数 任何不会修改数据成员的函数都应该声明为const 类型。如果在编写const 成员函数时,不 慎修改了数据成员,或者调用了其它非const 成员函数,编译器将指出错误,这无疑会提高程序 的健壮性。 以下程序中,类stack 的成员函数GetCount 仅用于计数,从逻辑上讲GetCount 应当为 const 函数。编译器将指出GetCount 函数中的错误。 class Stack { public: void Push(int elem); int Pop(void); int GetCount(void) const; // const 成员函数 private: int m_num; int m_data[100]; }; 41 www.thirdstudio.com.cn int Stack::GetCount(void) const { ++ m_num; // 编译错误,企图修改数据成员m_num Pop(); // 编译错误,企图调用非const 函数 return m_num; } const 成员函数的声明看起来怪怪的:const 关键字只能放在函数声明的尾部,大概是因为 其它地方都已经被占用了。 8.2 提高程序的效率 程序的时间效率是指运行速度,空间效率是指程序占用内存或者外存的状况。 全局效率是指站在整个系统的角度上考虑的效率,局部效率是指站在模块或函数角度上考 虑的效率。 【规则8-2-1】不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质 量因素的前提下,设法提高程序的效率。 【规则8-2-2】以提高程序的全局效率为主,提高局部效率为辅。 【规则8-2-3】在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关紧要之处优化。 【规则8-2-4】先优化数据结构和算法,再优化执行代码。 【规则8-2-5】有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折 衷。例如多花费一些内存来提高性能。 【规则8-2-6】不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码。 8.3 其他规则 【规则8-3-1】当心那些视觉上不易分辨的操作符发生书写错误。 我们经常会把“==”误写成“=”,象“||”、“&&”、“<=”、“>=”这类符号也很容易发生“丢1” 失误。然而编译器却不一定能自动指出这类错误。 【规则8-3-2】变量(指针、数组)被创建之后应当及时把它们初始化,以防止把未被初始化的 变量当成右值使用。 【规则8-3-3】当心变量的初值、缺省值错误,或者精度不够。 【规则8-3-4】当心数据类型转换发生错误。尽量使用显式的数据类型转换(让人们知道发生了 什么事),避免让编译器轻悄悄地进行隐式的数据类型转换。 【规则8-3-5】当心变量发生上溢或下溢,数组的下标越界。 【规则8-3-6】当心忘记编写错误处理程序,当心错误处理程序本身有误。 【规则8-3-7】当心文件I/O 有错误。 【规则8-3-8】避免编写技巧性很高代码。 【规则8-3-9】不要设计面面俱到、非常灵活的数据结构。 【规则8-3-10】如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的代码,应当 重新编写。 【规则8-3-11】尽量使用标准库函数,不要“发明”已经存在的库函数。 【规则8-3-12】尽量不要使用与具体硬件或软件环境关系密切的变量。 【规则8-3-13】把编译器的选择项设置为最严格状态。 【规则8-3-14】如果可能的话,使用PC-Lint、LogiScope 等工具进行代码审查。 42 www.thirdstudio.com.cn 参考文献 [Cline] Marshall P. Cline and Greg A. Lomow, C++ FAQs, Addison-Wesley, 1995 [Eckel] Bruce Eckel, Thinking in C++(C++ 编程思想,刘宗田 等译),机械工业出版 社,2000 [Maguire] Steve Maguire, Writing Clean Code(编程精粹,姜静波 等译),电子工业出 版社,1993 [Meyers] Scott Meyers, Effective C++, Addison-Wesley, 1992 [Murry] Robert B. Murry, C++ Strategies and Tactics, Addison-Wesley, 1993 [Summit] Steve Summit, C Programming FAQs, Addison-Wesley, 1999 版本声明 此版本在林锐编写的《高质量C++/C 编程指南》的基础上修改而成,基本上只是格式上的 变动,没有内容的更改。再次对林锐等编写者表示感谢和致敬。本版本只作为内部参考使用, 非商业目的。 43 www.thirdstudio.com.cn 附录A :C++/C 代码审查表 文件结构 重要性 审查项 结论 头文件和定义文件的名称是否合理? 头文件和定义文件的目录结构是否合理? 版权和版本声明是否完整? 重要 头文件是否使用了 ifndef/define/endif 预处理块? 头文件中是否只存放“声明”而不存放“定义” …… 程序的版式 重要性 审查项 结论 空行是否得体? 代码行内的空格是否得体? 长行拆分是否得体? “{” 和 “}” 是否各占一行并且对齐于同一列? 重要 一行代码是否只做一件事?如只定义一个变量,只写一条语句。 重要 If、for 、while 、do 等语句自占一行,不论执行语句多少都要加“{}”。 重要 在定义变量(或参数)时,是否将修饰符* 和& 靠紧变量名? 注释是否清晰并且必要? 重要 注释是否有错误或者可能导致误解? 重要 类结构的 public, protected, private 顺序是否在所有的程序中保持 一致? …… 命名规则 重要性 审查项 结论 重要 命名规则是否与所采用的操作系统或开发工具的风格保持一致? 44 www.thirdstudio.com.cn 标识符是否直观且可以拼读? 标识符的长度应当符合“min-length && max-information ” 原则? 重要 程序中是否出现相同的局部变量和全部变量? 类名、函数名、变量和参数、常量的书写格式是否遵循一定的规则? 静态变量、全局变量、类的成员变量是否加前缀? …… 表达式与基本语句 重要性 审查项 结论 重要 如果代码行中的运算符比较多,是否已经用括号清楚地确定表达式的操 作顺序? 是否编写太复杂或者多用途的复合表达式? 重要 是否将复合表达式与“真正的数学表达式”混淆? 重要 是否用隐含错误的方式写 if 语句? 例如 (1)将布尔变量直接与 TRUE 、FALSE 或者 1、0 进行比较。 (2)将浮点变量用“==”或“!=” 与任何数字比较。 (3)将指针变量用“==”或“!=”与 NULL 比较。 如果循环体内存在逻辑判断, 并且循环次数很大,是否已经将逻辑判断 移到循环体的外面? 重要 Case 语句的结尾是否忘了加 break? 重要 是否忘记写 switch 的 default 分支? 重要 使用 goto 语句时是否留下隐患? 例如跳过了某些对象的构造、变量的 初始化、重要的计算等。 …… 常量 重要性 审查项 结论 是否使用含义直观的常量来表示那些将在程序中多次出现的数字或字符 串? 在 C++ 程序中,是否用 const 常量取代宏常量? 重要 如果某一常量与其它常量密切相关,是否在定义中包含了这种关系? 45 www.thirdstudio.com.cn 是否误解了类中的 const 数据成员?因为 const 数据 成员只在某个对 象生存期内是常量,而对于整个类而言却是可变的。 …… 函数设计 重要性 审查项 结论 参数的书写是否完整?不要贪图省事只写参数的类型而省略参数名字。 参数命名、顺序是否合理? 参数的个数是否太多? 是否使用类型和数目不确定的参数? 是否省略了函数返回值的类型? 函数名字与返回值类型在语义上是否冲突? 重要 是否将正常值和错误标志混在一起返回?正常值应当用输出参数获得, 而错误标志用 return 语句返回。 重要 在函数体的“入口处”,是否用 assert 对参数的有效性进行检查? 重要 使用滥用了 assert? 例如混淆非法情况与错误情况,后者是必然存在 的并且是一定要作出处理的。 重要 return 语句是否返回指向“ 栈内存”的“指针”或者 “引用”? 是否使用 const 提高函数的健壮性?const 可以强制保护函数的参数、 返回值,甚至函数的定义体。“Use const whenever you need” …… 内存管理 重要性 审查项 结论 重要 用 malloc 或 new 申请内存之后,是否立即检查指针值是否为 NULL?(防 止使用指针值为 NULL 的内存) 重要 是否忘记为数组和动态内存赋初值?(防止将未被初始化的内存作为右 值使用) 重要 数组或指针的下标是否越界? 重要 动态内存的申请与释放是否配对?(防止内存泄漏) 重要 是否有效地处理了“内存耗尽”问题? 46 www.thirdstudio.com.cn 重要 是否修改“指向常量的指针”的内容? 重要 是否出现野指针?例如 (1)指针变量没有被初始化。 (2)用 free 或 delete 释放了内存之后,忘记将指针设置为 NULL。 重要 是否将 malloc/free 和 new/delete 混淆使用? 重要 malloc 语句是否正确无误?例如字节数是否正确? 类型转换是否正 确? 重要 在创建与释放动态对象数组时,new/delete 的语句是否正确无误? …… C++ 函数的高级特性 重要性 审查项 结论 重载函数是否有二义性? 重要 是否混淆了成员函数的重载、覆盖与隐藏? 运算符的重载是否符合制定的编程规范? 是否滥用内联函数?例如函数体内的代码比较长,函数体内出现循环。 重要 是否用内联函数取代了宏代码? 类的构造函数、析构函数和赋值函数 重要性 审查项 结论 重要 是否违背编程规范而让 C++ 编译器自动为类产生四个缺省的函数: (1)缺省的无参数构造函数; (2)缺省的拷贝构造函数; (3)缺省的析构函数; (4)缺省的赋值函数。 重要 构造函数中是否遗漏了某些初始化工作? 重要 是否正确的使用构造函数的初始化表? 重要 析构函数中是否遗漏了某些清除工作? 是否错写、错用了拷贝构造函数和赋值函数? 重要 赋值函数一般分四个步骤: (1)检查自赋值; (2)释放原有内存资源; (3)分配新的内存资源,并复制内容; (4)返回 *this。是否遗漏了重要步骤? 重要 是否正确地编写了派生类的构造函数、析构函数、赋值函数?注意事项: 47 www.thirdstudio.com.cn (1)派生类不可能继承基类的构造函数、析构函数、赋值函数。 (2)派生类的构造函数应在其初始化表里调用基类的构造函数。 (3)基类与派生类的析构函数应该为虚(即加 virtual 关键字)。 (4)在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新 赋值。 …… 类的高级特性 重要性 审查项 结论 重要 是否违背了继承和组合的规则? (1)若在逻辑上 B 是 A 的“一种”,并且 A 的所有功能和属性对 B 而 言都有意义,则允许 B 继承 A 的功能和属性。 (2)若在逻辑上 A 是 B 的“一部分”(a part of), 则不允许 B 从 A 派生,而是要用 A 和其它东西组合出 B。 …… 其它常见问题 重要性 审查项 结论 重要 数据类型问题: (1)变量的数据类型有错误吗? (2)存在不同数据类型的赋值吗? (3)存在不同数据类型的比较吗? 重要 变量值问题: (1)变量的初始化或缺省值有错误吗? (2)变量发生上溢或下溢吗? (3)变量的精度够吗? 重要 逻辑判断问题: (1)由于精度原因导致比较无效吗? (2)表达式中的优先级有误吗? (3)逻辑判断结果颠倒吗? 重要 循环问题: (1)循环终止条件不正确吗? (2)无法正常终止(死循环)吗? (3)错误地修改循环变量吗? (4)存在误差累积吗? 重要 错误处理问题: (1)忘记进行错误处理吗? (2)错误处理程序块一直没有机会被运行? (3) 错误处理程序块本身就有毛病吗?如报告的错误与实际错误不一 致,处理方式不正确等等。 48 www.thirdstudio.com.cn (4) 错误处理程序块是“马后炮”吗?如在被它被调用之前软件已经 出错。 重要 文件 I/O 问题: (1)对不存在的或者错误的文件进行操作吗? (2)文件以不正确的方式打开吗? (3)文件结束判断不正确吗? (4)没有正确地关闭文件吗? 49
还剩53页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 20 金币 [ 分享pdf获得金币 ] 2 人已下载

下载pdf

pdf贡献者

openpf

贡献于2011-09-26

下载需要 20 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf