笔记整理--C语言--高质量C编程指南—林锐——转载

高质量C编程指南—林锐

头文件的作用略作解释:

  1. 通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。

  2. 头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

如果一个软件的头文件数目比较多(如超过十个) ,通常应将头文件和定义文件分别保存于不同的目录,以便于维护。

例如可将头文件保存于 include 目录,将定义文件保存于 source 目录(可以是多级目录)。

如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明” 。
为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。

空行

空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。空行不会浪费内存,虽然打印含有空行的程序是会多消耗一些纸张,但是值得。

所以不要舍不得用空行。

在每个类声明之后、每个函数定义结束之后都要加空行。参见示例2-1(a)

// 空行
void Function1(…)
{
 …
}
// 空行
void Function2(…)
{
 …
}
// 空行
void Function3(…)
{
 …
}

示例 2-1(a) 函数之间的空行

在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。参见示例 2-1(b )

// 空行 
while (condition) 
{ 
 statement1; 
 // 空行 
 if (condition)  
 { 
   statement2; 
 } 
 else
 { 
   statement3; 
 } 
// 空行 
 statement4; 
}

示例 2-1(b) 函数内部的空行

代码行

  1. 一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。

  2. if、for、while、do 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}。这样可以防止书写失误。

风格良好的代码行

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();

风格不良的代码行

int width, height, depth; // 宽度高度深度
 
X = a + b;   y = c + d;  z = e + f; 
 
if (width < height) dosomething(); 
for (initialization; condition; update) 
     dosomething(); 
other();
  1. 尽可能在定义变量的同时初始化该变量(就近原则)
    如果变量的引用处和其定义处相隔比较远,变量的初始化很容易被忘记。如果引用了未被初始化的变量,可能会导致程序错误。本建议可以减少隐患。

例如

int width = 10;   // 定义并初绐化 width  

int height = 10;  // 定义并初绐化 height  

int depth = 10;   // 定义并初绐化 depth  

代码行内的空格

  • 关键字之后要留空格。象 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))

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();

对齐

  • 程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用它们的语句左对齐。

  • { }之内的代码块在‘{’右边数格处左对齐。

长行拆分

  • 代码行最大长度宜控制在 70 至 80 个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印。

长表达式要在低优先级操作符处拆分成新行, 操作符放在新行之首 (以便突出操作符) 。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。

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(); 
}

修饰符的位置

修饰符 * 和 & 应该靠近数据类型还是该靠近变量名,是个有争议的活题。若将修饰符 * 靠近数据类型,例如:int* x; 从语义上讲此写法比较直观,即 x是 int 类型的指针。 上述写法的弊端是容易引起误解,例如:int* x, y; 此处 y 容易被误解为指针变量。虽然将 x 和 y 分行定义可以避免误解,但并不是人人都愿意这样做。

因此,应当将修饰符 * 和 & 紧靠变量名 。

例如:

char  *name;

int   *x, y;  // 此处 y 不会被误解为指针

注释

C 语言的注释符为“//” 。C++语言中,程序块的注释常采用“//” ,行注释一般采用“//…” 。注释通常用于:

  • 版本、版权声明;
  • 函数接口说明;
  • 重要的代码行或段落提示。

虽然注释有助于理解代码,但注意不可过多地使用注释。参见示例

/* 
*  函数介绍: 
*  输入参数: 
*  输出参数: 
*  返回值   : 
*/
void Function(float x, float y, float z) 
{ 
  … 
} 
if (…) 
{ 
  … 
  while (…) 
    { 
    … 
  } // end of while 
  … 
} // end of if

类的版式

类的版式主要有两种方式:

  • 将 private 类型的数据写在前面, 而将 public 类型的函数写在后面, 如示例 8-3 (a) 。采用这种版式的程序员主张类的设计“以数据为中心” ,重点关注类的内部结构。
class A 
{ 
    private: 
    int    i, j; 
    float  x, y; 
    … 
    public: 
    void Func1(void); 
    void Func2(void); 
    … 
}
  • 将 public 类型的函数写在前面,而将 private 类型的数据写在后面,如示例 8.3(b)采用这种版式的程序员主张类的设计“以行为为中心” ,重点关注的是类应该提供什么样的接口(或服务) 。
class A 
{ 
    public: 
    void Func1(void); 
    void Func2(void); 
    … 
    private: 
    int    i, j; 
    float  x, y; 
    … 
}

布尔变量与零值比较

  1. 不可将布尔变量直接与 TRUE、FALSE 或者 1、0 进行比较。

  2. 根据布尔类型的语义,零值为“假” (记为 FALSE) ,任何非零值都是“真” (记为TRUE) 。TRUE 的值究竟是什么并没有统一的标准。例如 Visual C++ 将 TRUE 定义为1,而 Visual Basic 则将 TRUE 定义为-1。

假设布尔变量名字为 flag,它与零值比较的标准 if 语句如下:

       if (flag)  // 表示 flag 为真

       if (!flag) // 表示 flag 为假

它的用法都属于不良风格,例如:

       if (flag == TRUE)  

       if (flag == 1 )    

       if (flag == FALSE)  

       if (flag == 0)    

整型变量与零值比较

应当将整型变量用“==”或“!=”直接与 0 比较。

假设整型变量的名字为 value,它与零值比较的标准 if 语句如下:

   if (value == 0)  

   if (value != 0)

不可模仿布尔变量的风格而写成

   if (value)   // 会让人误解 value 是布尔变量

   if (!value)

浮点变量与零值比较

不可将浮点变量用“==”或“!=”与任何数字比较。

千万要留意,无论是 float 还是 double 类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。

假设浮点变量的名字为 x,应当将

   if (x == 0.0)  // 隐含错误的比较

转化为

   if ((x>=-EPSINON) && (x<=EPSINON))

其中 EPSINON 是允许的误差(即精度) 。

指针变量与零值比较

应当将指针变量用“==”或“!=”与 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)    

const 与 #define 的比较

C++ 语言可以用 const 来定义常量,也可以用 #define 来定义常量。但是前者比后者有更多的优点:
(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应) 。

(2) 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。

在 C++ 程序中只使用 const 常量而不使用宏常量,即 const 常量完全取代宏常量。

常量定义规则

需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。

如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。

例如:

const float RADIUS = 100;

const float DIAMETER = RADIUS * 2;

类中的常量

由于#define 定义的宏常量是全局的,不能达到目的,于是想当然地觉得应该用 const 修饰数据成员来实现。const 数据成员的确是存在的,但其含义却不是我们所期望的。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

怎样才能建立在整个类中都恒定的常量呢?别指望 const 数据成员了,应该用类中的枚举常量来实现。例如

class A
{…

   enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量

   int array1[SIZE1];  

   int array2[SIZE2];

};

枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如 PI=3.14159) 。

使用断言

使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。

在函数的入口处,使用断言检查参数的有效性(合法性) 。

在编写函数时, 要进行反复的考查, 并且自问: “我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。

一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

引用与指针的比较

引用是 C++中的概念,初学者容易把引用和指针混淆一起。一下程序中,n 是 m 的一个引用(reference) ,m 是被引用物(referent) 。

    int m;

    int &n = m;

n 相当于 m 的别名(绰号) ,对 n 的任何操作就是对 m 的操作。例如有人名叫王小毛,他的绰号是“三毛” 。说“三毛”怎么怎么的,其实就是对王小毛说三道四。所以 n 既不是 m 的拷贝,也不是指向 m 的指针,其实 n 就是 m 它自己。

引用的一些规则如下:

  • 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化) 。
  • 不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以是 NULL) 。
  • 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象) 。

以下示例程序中,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) 
{ 
  (* 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

对比上述三个示例程序,会发现“引用传递”的性质象“指针传递” ,而书写方式象“值传递” 。实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西?

答案是“用适当的工具做恰如其分的工作” 。

指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢这样用?

如果的确只需要借用一下某个对象的“别名” ,那么就用“引用” ,而不要用“指针” ,以免发生意外。比如说,某人需要一份证明,本来在文件上盖上公章的印子就行了,如果把取公章的钥匙交给他,那么他就获得了不该有的权利。

posted @ 2023-08-17 15:13  suntl  阅读(8)  评论(0编辑  收藏  举报