目 录
一、 文件结构
1.1 版权和版本的声明
1.2 头文件的结构
1.3 定义文件的结构
二、文件的版式
2.1 空行的用法
2.2 代码行
2.3 代码行内的空格
2.4 对齐
2.5 长行拆分
2.6 修饰符的位置
2.7 注释
三、命名规则
3.1 命名基本规则
四、表达式和基本语句
4.1 运算符的优先级
4.2 复合表达式
4.3 if 语句
4.4 循环语句的效率
五、函数设计
5.1 参数的规则
5.2 返回值的规则
5.3 函数内部实现的规则
5.4 其它建议
六、其它经验和建议
6.1 提高程序的效率
一、文件结构
每个C程序通常分为两个文件。一个文件用于保存程序的声明(declaration),称为头文件。另一个文件用于保存程序的实现(implementation),称为定义(definition)文件。
C程序的头文件以“.h”为后缀,C程序的定义文件以“.c”为后缀
1.1 版权和版本的声明
版权和版本的声明位于头文件和定义文件的开头,主要内容有:
(1)版权信息。
(2)文件名称,文件内容摘要。
(3)当前版本号,最后修改日期。
格式如下:
/*-------------------------------------------------------------------------- 文件名称 空行 文件摘要 Version版本号 Last Modify: 最后更改日期 Copyright (c) 2006 Shenzhen International Solution Software Co., Ltd. All rights reserved. --------------------------------------------------------------------------*/ |
示例如下:
/*-------------------------------------------------------------------------- Siss8118.h
Header file for Siss5118 Version 1.0 Last Modify: 2005.12.30 Copyright (c) 2006 Shenzhen International Solution Software Co., Ltd. All rights reserved. --------------------------------------------------------------------------*/ |
示例1-1 版权和版本的声明
1.2 头文件的结构
头文件由三部分内容组成:
(1)头文件开头处的版权和版本声明
(2)预处理块
(3)函数和结构体声明等
假设头文件名称为 Syscfg.h,头文件的结构参见示例
1.2.1 为了防止头文件被重复引用,应当用#ifndef/#define/#endif 结构产生预处理块
1.2.2 用#include <filename.h> 格式来引用标准库的头文件(编译器将从库目录开始搜索)
1.2.3 用#include “filename.h” 格式来引用非标准库的头文件(编译器将从工作目录开始搜索)
1.2.4 头文件中只存放函数和变量的“声明”而不存放函数和变量的“定义”
1.2.5 结构体定义应使用typedef定义结构体别名,避免直接使用struct直接定义结构体。
1.2.6 尽量避免使用全局变量,确实需要使用全局变量供其它模块引用时,必须在头文件中
使用类似extern int value声明。
1.2.7 头文件中存放需要引用的特殊宏(使用#define定义的宏替换)
1.2.8 在函数声明前存放函数需要引用的常量宏(使用#define定义的常量)
示例如下:
// 版权和版本声明见示例1-1,此处省略。 #ifndef _SYSCFG_H // 防止syscfg.h 被重复引用 #define _SYSCFG_H #include <absacc.h> // 头文件中需要引用标准库的头文件 … #include “main.h” // 头文件中需要引用非标准库的头文件 … #define MEM_TYPE code // 特殊宏定义
typedef struct //结构体定义 { … }tSysCfg;
extern int iSysStatus; //全局变量声明
#define SYS_RUNNING_STATUS 1 // 全局函数引用宏定义 #define SYS_CONFIG_STATUS 2 void Function1(…); // 全局函数声明 … #endif // _SYSCFG_H
|
示例1-2 头文件的结构
1.3 定义文件的结构
定义文件有三部分内容:
(1) 定义文件开头处的版权和版本声明(参见示例1-1)
(2) 对一些头文件的引用
(3) 程序的全局变量,模块静态变量定义
(4) 程序的模块静态函数声明
(5) 程序的实现体(包括数据和代码)
假设定义文件的名称为 Syscfg.c,定义文件的结构参见示例1-3。
// 版权和版本声明见示例1-1,此处省略。 #include <absacc.h> // 模块中需要引用标准库的头文件 … #include “main.h” // 模块中需要引用非标准库的头文件 … #include “Syscfg.h” // 引用本模块头文件
#define SYS_STATUS_MAX 1 //模块常量宏定义
#define ZeroMemory(Destination,Length) memset((Destination),0,(Length)) //模块特殊宏定义
int iSysStatus; //全局变量定义
static int iSys; //模块静态变量定义
static void ReadCfg(…); // 模块静态函数声明
void Function1(…) // 全局函数实现 { … }
static void ReadCfg(…) // 模块静态函数实现 { … } |
示例1-3 C文件的结构
二、文件的版式
2.1 空行的用法
空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。空行不会浪费内存,所以不要舍不得用空行。
2.1.1 在每个类声明之后、每个函数定义结束之后都要加空行。
2.1.2 在一个函数体内,函数变量定义后面应加空行分隔。
2.1.3 在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
示例如下:
// 空行 void Function1(…) { int i,j; unsigned char cBuff[10]; // 空行 … } // 空行 void Function2(…) { … } // 空行 void Function3(…) { … }
|
// 空行 while (condition) { statement1; // 空行 if (condition) { statement2; } else { statement3; } // 空行 statement4; }
|
示例2-1空行的用法
2.2 代码行
2.1.1 一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。
2.1.2 if、for、while、do 等语句自占一行,执行语句不得紧跟其后。
x = a + b; y = c + d; z = e + f; if (width < height) { dosomething(); } for (initialization; condition; update) { dosomething(); } // 空行 other(); |
x = a + b; y = c + d; z = e + f;
if (width < height) dosomething();
for (initialization; condition; update) dosomething(); other();
|
示例2-2代码行的风格:左边的为良好的风格,右边为不良的风格
2.3 代码行内的空格
2.3.1 函数名之后不要留空格,紧跟左括号‘(’。
2.3.2‘(’向后紧跟,‘)’、‘,’、‘;’向前紧跟,紧跟处不留空格。
2.3.3‘,’之后要留空格,如Function(x, y, z)。如果‘;’不是一行的结束符号,其后要留空格,如for (initialization; condition; update)。
2.3.4 赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。
2.3.5 一元操作符如“!”、“~”、“++”、“--”、“&”(地址运算符)等前后不加空格。
2.3.6 象“[]”、“.”、“->”这类操作符前后不加空格。
2.3.7 对于表达式比较长的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 ++) // 过多的空格 |
array[5] = 0; // 不要写成 array [ 5 ] = 0;
|
示例2-3 代码行内的空格
2.4 对齐
2.4.1 程序的分界符‘{’和‘}’应独占一行并位于同一列,同时与引用它们的语句左对齐。
2.4.2 { }之内的代码块在‘{’右边数格处左对齐。
void Function(int x) { … // program code } | void Function(int x){ … // program code }
|
if (condition) { … // program code } else { … // program code } | if (condition){ … // program code } else { … // program code }
|
如果出现嵌套的{},则使用缩进对齐,如: { … { … } … } | for (initialization; condition; update){ … // program code while (condition){ … // program code } }
|
示例2-4对齐:左边的为良好的风格,右边为不良的风格
2.5 长行拆分
2.5.1 代码行最大长度宜控制在70 至80 个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印。
2.5.2 长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
if ((very_longer_variable1 >= very_longer_variable12) && (very_longer_variable3 <= very_longer_variable14) && (very_longer_variable5 <= very_longer_variable16)) { dosomething(); } |
for (very_longer_initialization; very_longer_condition; very_longer_update) { dosomething(); } |
unsigned char WriteEEPROM (unsigned char Page, unsigned char Address, unsigned char Count);
|
示例2-5 长行的拆分
2.6 修饰符的位置
修饰符 * 和 &应该靠近数据类型还是该靠近变量名,是个有争议的活题。若将修饰符 * 靠近数据类型,例如:int* x; 从语义上讲此写法比较直观,即x 是int 类型的指针。
上述写法的弊端是容易引起误解,例如:int* x, y; 此处y 容易被误解为指针变量。虽然将x 和y 分行定义可以避免误解,但并不是人人都愿意这样做。
2.6.1 应当将修饰符 * 和 &紧靠变量名
例如:
char *name;
int *x, y; // 此处y 不会被误解为指针
2.7 注释
C 语言的注释符为“/*…*/”。C++语言中,程序块的注释常采用“/*…*/”,行注释
一般采用“//…”。注释通常用于:
(1)版本、版权声明;
(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.7 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
/* * 函数介绍: * 输入参数: * 输出参数: * 返回值 : */ void Function(float x, float y, float z) { … } | if (…) { … while (…) { … } // end of while … } // end of if
|
示例2-6 注释
三、命名规则
比较著名的命名规则当推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.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 标识符采用“大小写”混排的方式
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 全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。
类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。
例如:
DrawBox(); // 全局函数
box->Draw(); // 类的成员函数
用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
例如:
int minValue;
int maxValue;
int SetValue(…);
int GetValue(…);
3.1.8 尽量避免名字中出现数字编号,如Value1,Value2 等,除非逻辑上的确需要编号。
3.1.9 函数名用大写字母开头的单词组合而成。
例如:
void Draw(void); // 函数名
void SetValue(int value); // 函数名
3.1.10 常量全用大写的字母,用下划线分割单词。
例如:
#define MAX_VALUE 100;
#define MAX_LENGTH 100;
3.1.11 静态变量加前缀s_(表示static)。
例如:
static int s_initValue; // 静态变量
3.1.12 如果不得已需要全局变量,则使全局变量加前缀g_(表示global)。
例如:
int g_howManyPeople; // 全局变量
int g_howMuchMoney; // 全局变量
四、表达式和基本语句
4.1 运算符的优先级
C 语言的运算符有数十个,运算符的优先级与结合律须查询相关编译器手册,注意一元运算符 + - * 的优先级高于对应的二元运算符。
4.1.1 如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。由于将运算符的优先级与结合律熟记是比较困难的,为了防止产生歧义并提高可读性,应当用括号确定表达式的操作顺序。例如:
word = (high << 8) | low
if ((a | b) && (a & c))
4.2 复合表达式
如 a = b = c = 0 这样的表达式称为复合表达式。允许复合表达式存在的理由是:
(1)书写简洁;
(2)可以提高编译效率。但要防止滥用复合表达式。
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.3 if 语句
布尔变量与零值比较
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 应当将整型变量用“==”或“!=”直接与0 比较。假设整型变量的名字为value,它与零值比较的标准if 语句如下:
if (value == 0)
if (value != 0)
不可模仿布尔变量的风格而写成
if (value) // 会让人误解 value 是布尔变量
if (!value)
浮点变量与零值比较
4.3.3 不可将浮点变量用“==”或“!=”与任何数字比较。千万要留意,无论是float 还是double 类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。
假设浮点变量的名字为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)
4.4 循环语句的效率
C循环语句中,for 语句使用频率最高,while语句其次,do语句很少用。提高循环体效率的基本办法是降低循环体的复杂性。
4.4.1 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU 跨切循环层的次数。例如示例4-4(b)的效率比示例4-4(a)的高。
for (row=0; row<100; row++) { for ( col=0; col<5; col++ ) { sum = sum + a[row][col]; } } | for (col=0; col<5; col++ ) { for (row=0; row<100; row++) { sum = sum + a[row][col]; } } |
示例4-4(a) 低效率:长循环在最外层 示例4-4(b) 高效率:长循环在最内层
4.4.2 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。示例4-4(c)的程序比示例4-4(d)多执行了N-1 次逻辑判断。并且由于前者老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。如果N 非常大,最好采用示例4-4(d)的写法,可以提高效率。如果N非常小,两者效率差别并不明显,采用示例4-4(c)的写法比较好,因为程序更加简洁。
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-4(c) 效率低但程序简洁 示例4-4(d) 效率高但程序不简洁
五、函数设计
函数接口的两个要素是参数和返回值。C 语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。
5.1 参数的规则
5.1.1 参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void 填充。
例如:
void SetValue(int width, int height); // 良好的风格
void SetValue(int, int); // 不良的风格
float GetValue(void); // 良好的风格
float GetValue(); // 不良的风格
5.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”); // 参数顺序颠倒
5.1.3 如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。例如:
void StringCopy(char *strDestination,const char *strSource);
5.1.4 避免函数有太多的参数,参数个数尽量控制在5 个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。
5.2 返回值的规则
5.2.1 不要省略返回值的类型。
C 语言中,凡不加类型说明的函数,一律自动按整型处理。这样做不会有什么好处,却容易被误解为void 类型。
5.2.2 函数名字与返回值类型在语义上不可冲突。
5.2.3 不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return 语句返回。
5.3 函数内部实现的规则
不同功能的函数其内部实现各不相同,看起来似乎无法就“内部实现”达成一致的观点。但根据经验,我们可以在函数体的“入口处”和“出口处”从严把关,从而提高函数的质量。
5.3.1 在函数体的“入口处”,对参数的进行必要的有效性进行检查。
很多程序错误是由非法参数引起的,我们进行有效性检查来防止此类错误。
(1) 使用参数检查来捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的
(2) 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用参数检查对假定进行检查
5.3.2 在函数体的“出口处”,对return 语句的正确性和效率进行检查。
(1)return 语句不可返回指向“局部变量”的“指针”,因为该内存在函数体结束时被自动销毁。例如
char * Func(void)
{
char str[] = “hello world”; // str 的内存位于栈上
…
return str; // 将导致错误
}
(2)要搞清楚返回的究竟是“值”还是“指针”
(3)如果函数返回值是一个变量,要考虑return 语句的效率
return int(x + y); // 创建一个临时变量并返回它
这是临时变量的语法,表示“创建一个临时变量并返回它”。不要以为它与“先创建
一个局部变量temp 并返回它的结果”是等价的,如
int temp = x + y;
return temp;
实质不然,上述代码将发生三件事。首先,temp被创建;然后把temp 拷贝到保存返回值的存储单元中;最后,temp在函数结束时被销毁。然而“创建一个临时变量并返回它”的过程是不同的,编译器直接把临时变量创建在存储单元中,省去了拷贝和销毁的化费,提高了效率。
5.4 其它建议
5.4.1 函数的功能要单一,不要设计多用途的函数。
5.4.2 函数体的规模要小,尽量控制在50 行代码之内。
5.4.3 尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在C语言中,函数的static 局部变量是函数的“记忆”存储器。建议尽量少用static 局部变量,除非必需。
5.4.4不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内
的变量的有效性,例如全局变量、文件句柄等。
5.4.5 用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。
六、其它经验和建议
6.1 提高程序的效率
程序的时间效率是指运行速度,空间效率是指程序占用内存或者外存的状况。全局效率是指站在整个系统的角度上考虑的效率,局部效率是指站在模块或函数角度上考虑的效率。
6.1.1 不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。
6.1.2 以提高程序的全局效率为主,提高局部效率为辅。
6.1.3 在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关紧要之处优化。
6.1.4 先优化数据结构和算法,再优化执行代码。
6.1.5 有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷。例如多花费一些内存来提高性能。
6.1.6 不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码。