C语言高质量编程

C语言高质量编程

1.2 头文件的结构

  1. 头文件开头处的版权和版本声明(参见示例1-1)。
  2. 预处理块。
  3. 函数和类结构声明等。

假设头文件名称为 graphics.h,头文件的结构参见下面。

  • 【规则1-2-1】为了防止头文件被重复引用,应当用ifndef/define/endif 结构产生预处理块。
  • 【规则 1-2-2】用#include <filename.h> 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。
  • 【规则1-2-3】用#include “filename.h” 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。
  • 【建议1-2-1】头文件中只存放“声明”而不存放“定义”在 C++ 语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,但却造成了风格不一致,弊大于利。建议将成员函数的定义与声明分开,不论该函数体有多么小。
  • 【建议1-2-2】不提倡使用全局变量,尽量不要在头文件中出现象extern int value 这类声明。
1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef GRAPHICS_H // 防止graphics.h 被重复引用
#define GRAPHICS_H
#include <math.h> // 引用标准库的头文件

#include “myheader.h” // 引用非标准库的头文件

void Function1(⋯); // 全局函数声明

class Box // 类结构声明
{

};
#endif

1.3 定义文件的结构

定义文件有三部分内容:

  1. 定义文件开头处的版权和版本声明(参见示例1-1)。
  2. 对一些头文件的引用。
  3. 程序的实现体(包括数据和代码)。
    假设定义文件的名称为 graphics.cpp,定义文件的结构参见下面。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include “graphics.h” // 引用头文件

// 全局函数的实现体
void Function1(⋯)
{

}
// 类成员函数的实现体
void Box::Draw(⋯)
{

}

2.1 空行

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

  • 【规则 2-1-1】在每个类声明之后、每个函数定义结束之后都要加空行。参见示例2-1(a)
  • 【规则2-1-2】在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔

2.2 代码行

  • 【规则2-2-1】一行代码只做一件事情如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。
  • 【规则2-2-2】if、for、while、do 等语句自占一行,执行语句不得紧跟其后。**不论执行语句有多少都要加{}**。这样可以防止书写失误。

2.3 代码行内的空格

  • 【规则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】xu“[]”、“.”、“->”这类操作符前后不加空格。
  • 【建议2-3-1】对于表达式比较长的for 语句和if 语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d))

2.4 对齐

  • 【规则2-4-1】程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用它们的语句左对齐。
  • 【规则2-4-2】{ }之内的代码块在‘{’右边数格处左对齐。

2.5 长行拆分

  • 【规则2-5-1】代码行最大长度宜控制在70 至80 个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印。
  • 【规则2-5-2】长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。

2.6 修饰符的位置

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

  • 【规则2-6-1】应当将修饰符 * 和& 紧靠变量名

2.7 注释

C 语言的注释符为“//”。C++语言中,程序块的注释常采用“//”,行注释一般采用“//…”。注释通常用于:
(1)版本、版权声明;
(2)函数接口说明;
(3)重要的代码行或段落提示。
虽然注释有助于理解代码,但注意不可过多地使用注释。
【规则2-7-1】注释是对代码的“提示”,而不是文档。程序中的注释不可喧宾夺主,注释太多了会让人眼花缭乱。注释的花样要少。
【规则2-7-2】如果代码本来就是清楚的,则不必加注释。否则多此一举,令人厌烦。例如i++; // i 加 1,多余的注释
【规则2-7-3】边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
【规则2-7-4】注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有害。
【规则2-7-5】尽量避免在注释中使用缩写,特别是不常用缩写。
【规则2-7-6】注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。
【规则2-7-8】当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* 函数介绍:
* 输入参数:
* 输出参数:
* 返回值:
*/
void Function(float x, float y, float z)
{

}

if (…)
{

while (…)
{

} // end of while

} // end of if

3.2 简单的单片机应用程序命名规则

  1. 函数名用大写字母开头的单词组合而成。
    例如:
    void Draw(void); // 函数名
    void Setvalue(int value); // 函数名
  2. 变量和参数用小写字母开头的单词组合而成,float 变量名前加 f,int 变量名前加 i,bit变量前加 b 。(char 不加)
    例如:
    bit b_tirm;
    int i_time;
    float f_time;
    char time;
  3. 常量、宏定义(DF_xxx)全用大写的字母,用下划线分割单词。
    例如:
    code char MAX = 100;
    #define DF_MAX 100

4.3 if 语句

4.3.1 布尔变量与零值比较

【规则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 为假

其它的用法都属于不良风格,例如:
if (flag == TRUE)
if (flag == 1 )
if (flag == FALSE)

if (flag == 0)

4.3.2 整型变量与零值比较

【规则4-3-2】应当将整型变量用“==”或“!=”直接与0 比较。
假设整型变量的名字为value,它与零值比较的标准if 语句如下:
if (value == 0)
if (value != 0)
不可模仿布尔变量的风格而写成
if (value) // 会让人误解 value 是布尔变量

if (!value)

4.3.3 浮点变量与零值比较

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

假设浮点变量的名字为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不能被赋值。

4.4 循环语句的效率

【建议4-4-1】在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU 跨切循环层的次数。例如示例4-4(b)的效率比示例4-4(a)的高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 示例 4-4(a) 低效率:长循环在最外层 
for (row=0; row<100; row++)
{
for ( col=0; col<5; col++ )
{
sum = sum + a[row][col];
}
}
// 示例4-4(b) 高效率:长循环在最内层
for (col=0; col<5; col++ )
{
for (row=0; row<100; row++)
{
sum = sum + a[row][col];
}
}

【建议4-4-2】如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。示例4-4(c)的程序比示例4-4(d)多执行了N-1 次逻辑判断。并且由于前者老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。如果N 非常大,最好采用示例4-4(d)的写法,可以提高效率。如果N 非常小,两者效率差别并不明显,采用示例4-4(c)的写法比较好,因为程序更加简洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 效率低但程序简洁
for (i=0; i<N; i++)
{
if (condition)
DoSomething();
else
DoOtherthing();
}
// 效率高但程序不简洁
if (condition)
{
for (i=0; i<N; i++)
DoSomething();
}
else
{
for (i=0; i<N; i++)
DoOtherthing();
}

4.5 for 语句的循环控制变量

【规则4-5-1】不可在for 循环体内修改循环变量,防止for 循环失去控制。
【建议4-5-1】建议for 语句的循环控制变量的取值采用“半开半闭区间”写法。
示例 4-5(a)中的x 值属于半开半闭区间“0 =< x < N”,起点到终点的间隔为N,循环次数为N。
示例 4-5(b)中的x 值属于闭区间“0 =< x <= N-1”,起点到终点的间隔为N-1,循环次数为N。

相比之下,示例4-5(a)的写法更加直观,尽管两者的功能是相同的。

1
2
3
4
5
6
7
8
9
10
// 循环变量属于半开半闭区间 
for (int x=0; x<N; x++)
{

}
// 循环变量属于闭区间
for (int x=0; x<=N-1; x++)
{

}

4.6 switch 语句

【规则4-6-1】每个case 语句的结尾不要忘了加break,否则将导致多个分支重叠(除非有意使多个分支重叠)。
【规则4-6-2】不要忘记最后那个default 分支。即使程序真的不需要default 处理,也应该保留语句 default : break; 这样做并非多此一举,而是为了防止别人误以为你忘了default 处理。

4.7 goto 语句

自从提倡结构化设计以来,goto 就成了有争议的语句。首先,由于goto 语句可以灵活跳转,如果不加限制,它的确会破坏结构化设计风格。其次,goto 语句经常带来错误或隐患。它可能跳过了某些对象的构造、变量的初始化、重要的计算等语句,例如:

1
2
3
4
5
6
goto state;
String s1, s2; // 被goto 跳过
int sum = 0; // 被goto 跳过

state:

所以我们主张少用、慎用goto 语句,而不是禁用。

5.1 为什么需要常量

如果不使用常量,直接在程序中填写数字或字符串,将会有什么麻烦?
(1) 程序的可读性(可理解性)变差。程序员自己会忘记那些数字或字符串是什么意思,用户则更加不知它们从何处来、表示什么。
(2) 在程序的很多地方输入同样的数字或字符串,难保不发生书写错误。

(3) 如果要修改数字或字符串,则会在很多地方改动,既麻烦又容易出错。

【规则5-1-1】尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。

5.2 const 与 #define 的比较

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

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

【规则5-2-1】在C++ 程序中只使用const 常量而不使用宏常量,即const 常量完全取代宏常量

5.3 常量定义规则


C语言高质量编程
https://www.oikiou.top/2019/b0d7d162/
作者
Oikiou
发布于
2019年10月28日
许可协议