/* * 函数介绍: * 输入参数: * 输出参数: * 返回值 : */ void Function(float x, float y, float z) { … } if (…) { … while (…) { … } // end of while … } // end of if
版权和版本的声明位于H和CPP的开头(参见示例 1-1),主要内容有: ( 1)版权信息。 ( 2)文件名称,标识符,摘要。 ( 3)当前版本号,作者/修改者,完成日期。 ( 4)版本历史信息。 头文件里面的内容/////////////////////// ( 1)头文件开头处的版权和版本声明(参见示例 1-1)。 ( 2)预处理块。 ( 3)函数和类结构声明等。 建议将成员函数的定 义与声明分开,不论该函数体有多么小。 /* * Copyright (c) 2001,上海贝尔有限公司网络应用事业部 * All rights reserved. * * 文件名称: filename.h * 文件标识: 见配置管理计划书 * 摘 要: 简要描述本文件的内容 * * 当前版本: 1.1 * 作 者: 输入作者(或修改者)名字 * 完成日期: 2001年7月20日 * * 取代版本: 1.0 * 原作者 : 输入原作者(或修改者)名字 * 完成日期: 2001年5月10日 */ #ifndef GRAPHICS_H // 防止 graphics.h 被重复引用 #define GRAPHICS_H #include <math.h> // 引用标准库的头文件 … #include “myheader.h” // 引用非标准库的头文件 … void Function1(…); // 全局函数声明 … class Box // 类结构声明 { … }; #endif //尽量不要在头文件中出现象 extern int value ////////////////////////CPP 文件 // 版权和版本声明见示例 1-1,此处省略。 #include “graphics.h” // 引用头文件 … // 全局函数的实现体 void Function1(…) { … } // 类成员函数的实现体 void Box::Draw(…) { … }
头文件作用 /*( 1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要 向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功 能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。 ( 2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中 的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的 */ 例如可将头文件保存于 include 目录,将定义文件保存于 source 目录(可以是多级 目录)。 如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声 明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。 //在每个类声明之后、每个函数定义结束之后都要加空行 //一行代码只做一件事情 //if、 for、 while、 do 等语句自占一行,不管执行语句有多少都要加{} int width; // 宽度 尽可能在定义变量的同时初始化该变量 int width = 10; // 定义并初绐化 width int height; // 高度 int depth; // 深度 if (width < height) { dosomething(); } for (initialization; condition; update) { dosomething(); } // 空行 other(); void Func1(int x, int y, int z); // 良好的风格 void Func1 (int x,int y,int z); // 不良的风格 if (year >= 2000) // 良好的风格 if(year>=2000) // 不良的风格 if ((a>=b) && (c<=d)) // 良好的风格 if(a>=b&&c<=d) // 不良的风格 for (i=0; i<10; i++) // 良好的风格 for(i=0;i<10;i++) // 不良的风格 for (i = 0; I < 10; i ++) // 过多的空格 x = a < b ? a : b; // 良好的风格 x=a<b?a:b; // 不好的风格 int *x = &y; // 良好的风格 int * x = & y; // 不良的风格 array[5] = 0; // 不要写成 array [ 5 ] = 0; a.Function(); // 不要写成 a . Function(); b->Function(); // 不要写成 b -> Function(); 【规则 2-3-1】 关键字之后要留空格。象 const、 virtual、 inline、 case 等关键字之后 至少要留一个空格,否则无法辨析关键字。象 if、 for、 while 等关键字之后应留一个 空格再跟左括号‘(’,以突出关键字。 z 【规则 2-3-2】 函数名之后不要留空格,紧跟左括号‘(’,以与关键字区别。 z 【规则 2-3-3】‘ (’向后紧跟,‘)’、‘, ’、‘ ;’向前紧跟,紧跟处不留空格。 z 【规则 2-3-4】‘,’之后要留空格,如 Function(x, y, z)。如果‘ ;’不是一行的结束 符号,其后要留空格,如 for (initialization; condition; update)。 z 【规则 2-3-5】 赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符, 如“ =”、“ +=” “ >=”、“ <=”、“ +”、“ *”、“ %”、“ &&”、“ ||”、“ <<” ,“ ^”等二元 操作符的前后应当加空格。 z 【规则 2-3-6】 一元操作符如“ !”、“ ~”、“ ++”、“ --”、“ &”(地址运算符)等前后不 加空格。 2001 Page 15 of 95 高质量 C++/C 编程指南, v 1.0 z 【规则 2-3-7】 象“[]”、“.”、“->”这类操作符前后不加空格。 【建议 2-3-1】 对于表达式比较长的 for 语句和 if 语句,为了紧凑起见可以适当地去 掉一些空格,如 for (i=0; i<10; i++)和 if ((a<=b) && (c<=d))
for (initialization; condition; update) { … // program code } if (condition) { … // program code } else { … // program code } While (condition) { … // program code } while (condition){ … // program code } 如果出现嵌套的{},则使用缩进对齐,如: { … { … } … } if ((very_longer_variable1 >= very_longer_variable12) && (very_longer_variable3 <= very_longer_variable14) && (very_longer_variable5 <= very_longer_variable16)) { dosomething(); } virtual CMatrix CMultiplyMatrix (CMatrix leftMatrix, CMatrix rightMatrix); for (very_longer_initialization; very_longer_condition; very_longer_update) { dosomething(); }
应当将修饰符 * 和 & 紧靠变量名
/* C 语言的注释符为“ /*…*/”。 C++语言中,程序块的注释常采用“ /*…*/”,行注释 般采用“ //…”。注释通常用于: )版本、版权声明; 2)函数接口说明; 3)重要的代码行或段落提示。 虽然注释有助于理解代码,但注意不可过多地使用注释。参见示例 2-6。 【规则 2-7-1】 注释是对代码的“提示”,而不是文档。程序中的注释不可喧宾夺主, 注释太多了会让人眼花缭乱。注释的花样要少。 【规则 2-7-2】 如果代码本来就是清楚的,则不必加注释。否则多此一举,令人厌烦。 例如 i++; // i 加 1,多余的注释 【规则 2-7-3】 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码 的一致性。不再有用的注释要删除。 【规则 2-7-4】 注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而 有害。 【规则 2-7-5】 尽量避免在注释中使用缩写,特别是不常用缩写。 【规则 2-7-6】 注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不 可放在下方。 【规则 2-7-8】 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注 释,便于阅读。 */
class A { public: void Func1(void); void Func2(void); … private: int i, j; float x, y; … }
标识符最好采用英文单词或其组合
程序中不要出现仅靠大小写区分的相似的标识符。
例如:
int x, X; // 变量 x 与 X 容易混淆
void foo(int x); // 函数 foo 与 FOO 容易混淆
void FOO(float x);
程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的
作用域不同而不会发生语法错误,但会使人误解。
变量的名字应当使用“名词”或者“形容词+名词”。 例如: float value; float oldValue; float newValue; z 【规则 3-1-7】 全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。 类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。 例如: DrawBox(); // 全局函数 box->Draw(); // 类的成员函数 z 【规则 3-1-8】 用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。 例如: int minValue; int maxValue; int SetValue(…); int GetValue(…); 【建议 3-1-1】 尽量避免名字中出现数字编号,
jiandan 单的 Windows 应用程序命名规则
作者对“匈牙利”命名规则做了合理的简化,下述的命名规则简单易用,比较适合
于 Windows 应用软件的开发。
z 【规则 3-2-1】 类名和函数名用大写字母开头的单词组合而成。
2001 Page 21 of 95
高质量 C++/C 编程指南, v 1.0
例如:
class Node; // 类名
class LeafNode; // 类名
void Draw(void); // 函数名
void SetValue(int value); // 函数名
z 【规则 3-2-2】 变量和参数用小写字母开头的单词组合而成。
例如:
BOOL flag;
int drawMode;
z 【规则 3-2-3】 常量全用大写的字母,用下划线分割单词。
例如:
const int MAX = 100;
const int MAX_LENGTH = 100;
z 【规则 3-2-4】 静态变量加前缀 s_(表示 static)。
例如:
void Init(…)
{
static int s_initValue; // 静态变量
…
}
z 【规则 3-2-5】 如果不得已需要全局变量,则使全局变量加前缀 g_(表示 global)。
例如:
int g_howManyPeople; // 全局变量
int g_howMuchMoney; // 全局变量
z 【规则 3-2-6】 类的数据成员加前缀 m_(表示 member),这样可以避免数据成员与
成员函数的参数同名。
例如:
void Object::SetValue(int width, int height)
{
m_width = width;
m_height = height;
}
z 【规则 3-2-7】 为了防止某一软件库中的一些标识符和其它软件库中的冲突,可以为
各种标识符加上能反映软件性质的前缀。例如三维图形标准 OpenGL 的所有库函数
2001 Page 22 of 95
高质量 C++/C 编程指南, v 1.0
均以 gl 开头,所有常量(或宏定义)均以 GL 开头。
布尔变量与零值比较 z 【规则 4-3-1】 不可将布尔变量直接与 TRUE、 FALSE 或者 1、 0 进行比较。 根据布尔类型的语义,零值为“假”(记为 FALSE),任何非零值都是“真”(记为 TRUE)。 TRUE 的值究竟是什么并没有统一的标准。例如 Visual C++ 将 TRUE 定义为 1, 而 Visual Basic 则将 TRUE 定义为-1。 假设布尔变量名字为 flag,它与零值比较的标准 if 语句如下: if (flag) // 表示 flag 为真 if (!flag) // 表示 flag 为假 整型变量与零值比较 z 【规则 4-3-2】 应当将整型变量用“==”或“!=”直接与 0 比较。 假设整型变量的名字为 value,它与零值比较的标准 if 语句如下: if (value == 0) if (value != 0) 假设浮点变量的名字为 x,应当将 if (x == 0.0) // 隐含错误的比较 转化为 if ((x>=-EPSINON) && (x<=EPSINON)) 其中 EPSINON 是允许的误差(即精度)。
【规则 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)
有时候我们可能会看到 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; 2001 Page 26 of 95 高质量 C++/C 编程指南, v 1.0 return y; 改写为 if (condition) { return x; } else { return y; } 或者改写成更加简练的 return (condition ? x : y);
在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的 循环放在最外层,以减少 CPU 跨切循环层的次数。例如示例 4-4(b)的效率比示例 4-4(a)的高。 for (row=0; row<100; row++) { for ( col=0; col<5; col++ ) { 示例 4-4(a) 低效率:长循环在最外层 sum = sum + a[row][col]; } } for (col=0; col<5; col++ ) { for (row=0; row<100; row++) { sum = sum + a[row][col]; } } 示例 4-4(b) 高效率:长循环在最内层
【建议 4-4-2】 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到 循环体的外面。示例 4-4(c)的程序比示例 4-4(d)多执行了 N-1 次逻辑判断。并且由 于前者老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进 行优化处理,降低了效率。如果 N 非常大,最好采用示例 4-4(d)的写法 if (condition) { for (i=0; i<N; i++) DoSomething(); } else { for (i=0; i<N; i++) DoOtherthing(); }
建议 for 语句的循环控制变量的取值采用“半开半闭区间”写法 for (int x=0; x<N; x++) { …优先 } for (int x=0; x<=N-1; x++
switch 语句的基本格式是: switch (variable) { case value1 : … break; case value2 : … break; … default : … break; }
goto 语句 自从提倡结构化设计以来,goto 就成了有争议的语句。首先,由于 goto 语句可以 灵活跳转,如果不加限制,它的确会破坏结构化设计风格。其次,goto 语句经常带来错 误或隐患。它可能跳过了某些对象的构造、变量的初始化、重要的计算等语句,例如: goto state; String s1, s2; // 被 goto 跳过 int sum = 0; // 被 goto 跳过 … state: …
很多人建议废除 C++/C 的 goto 语句,以绝后患。但实事求是地说,错误是程序员自 己造成的,不是 goto 的过错。goto 语句至少有一处可显神通,它能从多重循环体中咻 地一下子跳到外面,用不着写很多次的 break 语句; 例如 { … { … { … goto error; } } } error: … 就象楼房着火了,来不及从楼梯一级一级往下走,可从窗口跳出火坑。所以我们主 张少用、慎用 goto 语句,而不是禁用。
常量 // C++ 语言可以用 const 来定义常量,也可以用 #define 来定义常量。但是前者比后 者有更多的优点: (1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安 全检查。而对后者只进行字符替换, //需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义 文件的头部 //由于#define 定义的宏常量是全局的 const 数据成员只在某个对象生存期内是常 量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其 const 数 据成员的值可以不同。 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 怎样才能建立在整个类中都恒定的常量呢?别指望 const 数据成员了,应该用类中 的枚举常量来实现。例如 class A {… enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量 int array1[SIZE1]; int array2[SIZE2]; }; 枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是: 它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如 PI=3.14159)。
参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。 如果函数没有参数,则用 void 填充。 例如: void SetValue(int width, int height);// 良好的风格 void SetValue(int, int); // 不良的风格 float GetValue(void); // 良好的风格 float GetValue(); // 不良的风格
void StringCopy(char *str1, char *str2);
那么我们很难搞清楚究竟是把 str1 拷贝到 str2 中,还是刚好倒过来。
可以把参数名字起得更有意义,如叫 strSource 和 strDestination。这样从名字上就可
以看出应该把 strSource 拷贝到 strDestination。
还有一个问题,这两个参数那一个该在前那一个该在后?参数的顺序要遵循程序员
的习惯。一般地,应将目的参数放在前面,源参数放在后面
如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该 指针在函数体内被意外修改。 例如: void StringCopy(char *strDestination,const char *strSource);
如果输入参数以值传递的方式传递对象,则宜改用“ const &”方式来 传递,这样可以省去临时对象的构造和析构过程,从而提高效率。 避免函数有太多的参数,参数个数尽量控制在 5 个以内 尽量不要使用类型和数目不确定的参数。 C 标准库函数 printf 是采用不确定参数的典型代表,其原型为: int printf(const chat *format[, argument]…);
不要省略返回值的类型。 按照 getchar 名字的意思,将变量 c 声明为 char 类型是很自然的事情。但不幸的是 ////getchar 的确不是 char 类型,而是 int 类型,其原型如下: int getchar(void); 不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而 错误标志用 return 语句返回。 我们在实际工作中,经常会碰到上述令人为难的问题。为了避免出现误解,我们应 该将正常值和错误标志分开。即:正常值用输出参数获得,而错误标志用 return 语句返 回。 函数 getchar 可以改写成 BOOL GetChar(char *c); 虽然 gechar 比 GetChar 灵活,例如 putchar(getchar()); 但是如果 getchar 用错了, 它的灵活性又有什么用呢?
有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达, 可以附加返回值。 例如字符串拷贝函数 strcpy 的原型: char *strcpy(char *strDest,const char *strSrc); strcpy 函数将 strSrc 拷贝至输出参数 strDest 中,同时函数的返回值又是 strDest。这 样做并非多此一举,可以获得如下灵活性: char str[20]; int length = strlen( strcpy(str, “Hello World”) );
如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传 递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出 错。 例如: class String {… // 赋值函数 String & operate=(const String &other); // 相加函数,如果没有 friend 修饰则只许有一个右侧参数 friend String operate+( const String &s1, const String &s2); private: char *m_data; } 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 是仅含‘ ’的字符串 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 什么也得不到,流下了隐患。
在函数体的“入口处”,对参数的有效性进行检查。 很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”( assert) 来防止此类错误。详见 6.5 节“使用断言”。 z 【规则 6-3-2】 在函数体的“出口处”,对 return 语句的正确性和效率进行检查。 如果函数有返回值,那么函数的“出口处”是 return 语句。我们不要轻视 return 语 2001 Page 36 of 95 高质量 C++/C 编程指南, v 1.0 句。如果 return 语句写得不好,函数要么出错,要么效率低下。 注意事项如下: ( 1) return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数 体结束时被自动销毁。例如 char * Func(void) { char str[] = “hello world”; // str 的内存位于栈上 … 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-1】 函数的功能要单一,不要设计多用途的函数。 【建议 6-4-2】 函数体的规模要小,尽量控制在 50 行代码之内。 【建议 6-4-3】 尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。 带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某 种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在 C/C++语言中,函数的 static 局部变量是函数的“记忆”存储器。建议尽量少用 static 局部变量,除非必需。 【建议 6-4-4】 不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内 的变量的有效性,例如全局变量、文件句柄等。 2001 Page 37 of 95 高质量 C++/C 编程指南, v 1.0 【建议 6-4-5】 用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误 情况。 6.5 使用断言 程序一般分为 Debug 版本和 Release 版本, Debug 版本用于内部调试, Release 版本 发行给用户使用。 断言 assert 是仅在 Debug 版本起作用的宏,它用于检查“不应该”发生的情况。示 例 6-5 是一个内存复制函数。在运行过程中,如果 assert 的参数为假,那么程序就会中 止(一般地还会出现提示对话,说明在什么地方引发了 assert)。 void *memcpy(void *pvTo, const void *pvFrom, size_t size) { assert((pvTo != NULL) && (pvFrom != NULL)); // 使用断言 byte *pbTo = (byte *) pvTo; // 防止改变 pvTo 的地址 byte *pbFrom = (byte *) pvFrom; // 防止改变 pvFrom 的地址 while(size -- > 0 ) *pbTo ++ = *pbFrom ++ ; return pvTo; } 示例 6-5 复制不重叠的内存块 assert 不是一个仓促拼凑起来的宏。为了不在程序的 Debug 版本和 Release 版本引起 差别, assert 不应该产生任何副作用。所以 assert 不是函数,而是宏。程序员可以把 assert 看成一个在任何系统状态下都可以安全使用的无害测试手段。 如果程序在 assert 处终止 了,并不是说含有该 assert 的函数有错误,而是调用者出了差错, assert 可以帮助我们 找到发生错误的原因。 很少有比跟踪到程序的断言,却不知道该断言的作用更让人沮丧的事了。你化了很 多时间,不是为了排除错误,而只是为了弄清楚这个错误到底是什么。有的时候,程序 员偶尔还会设计出有错误的断言。所以如果搞不清楚断言检查的是什么,就很难判断错 误是出现在程序中,还是出现在断言中。幸运的是这个问题很好解决,只要加上清晰的 注释即可。这本是显而易见的事情,可是很少有程序员这样做。这好比一个人在森林里, 看到树上钉着一块“危险”的大牌子。但危险到底是什么?树要倒?有废井?有野兽? 除非告诉人们“危险”是什么,否则这个警告牌难以起到积极有效的作用。难以理解的 断言常常被程序员忽略,甚至被删除。 [Maguire, p8-p30] z 【规则 6-5-1】 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况 之间的区别,后者是必然存在的并且是一定要作出处理的。 z 【规则 6-5-2】在函数的入口处,使用断言检查参数的有效性(合法性)。 z 【建议 6-5-1】在编写函数时, 要进行反复的考查, 并且自问: “我打算做哪些假定?” 一旦确定了的假定,就要使用断言对假定进行检查。 z 【建议 6-5-2】 一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可 2001 Page 38 of 95 高质量 C++/C 编程指南, v 1.0 能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要 使用断言进行报警。 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; 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) { 2001 Page 39 of 95 高质量 C++/C 编程指南, v 1.0 (* 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 对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象 “值传递”。实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用” 这东西? 答案是“用适当的工具做恰如其分的工作”。 指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。 就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢这样用? 如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”, 以免发生意外。比如说,某人需要一份证明,本来在文件上盖上公章的印子就行了,如 果把取公章的钥匙交给他,那么他就获得了不该有的权利。