C 编程最佳实践Word文档格式.docx
《C 编程最佳实践Word文档格式.docx》由会员分享,可在线阅读,更多相关《C 编程最佳实践Word文档格式.docx(15页珍藏版)》请在冰豆网上搜索。
![C 编程最佳实践Word文档格式.docx](https://file1.bdocx.com/fileroot1/2023-1/8/97296c0e-7892-4f2f-ba1c-eb6c31572d9f/97296c0e-7892-4f2f-ba1c-eb6c31572d9f1.gif)
number<
limit&
limit<
=SIZE
&
node_active(this_input)){...
最好改成:
next==NULL
limit&
=SIZE
node_active(this_input))
{
...
同样,应将描述得很详细的for循环分割成不同的行:
for(curr=*varp,trail=varp;
curr!
=NULL;
trail=&
(curr->
next),curr=curr->
next)
对于其它复杂表达式(如使用三元运算符?
:
的表达式),最好也将其分割成数行。
z=(x==y)
?
n+f(x)
:
f(y)-n;
∙注释
注释应描述正在发生什么事、如何完成它、参数表示什么、使用了哪些全局变量以及任何限制或错误。
但要避免不必要的注释。
如果代码比较清晰,并且使用了良好的变量名,那么它应该能够较好地说明自身。
因为编译器不检查注释,所以不保证它们是正确的。
与代码不一致的注释会起到相反的作用。
过多的注释会使代码混乱。
下面是一种多余的注释风格:
i=i+1;
/*Addonetoi*/
很明显变量i递增了1。
还有更糟的注释方法:
/************************************
**
*Addonetoi*
************************************/
∙命名约定
具有前导和尾随下划线的名称是为系统用途而保留的,不应当用于任何用户创建的名称。
约定规定:
1.#define常量应全部大写。
2.enum常量应以大写字母开头或全部大写。
3.函数、类型定义(typedef)和变量名以及结构(struct)、联合(union)和枚举(enum)标记名称应小写。
为清晰起见,避免使用仅在大小写上有区别的名称,如foo和Foo。
同样,避免使用foobar和foo_bar这样的名称。
避免使用看上去相似的名称。
在许多终端和打印机上,“l”、“1”和“I”看上去非常相似。
使用名为“l”的变量非常不明智,因为它看上去非常象常量“1”。
∙变量名
选择变量名时,长度不重要,但清晰的表达很重要。
长名称可用于全局变量,因为它不常用,而将在每行循环上要使用的数组下标命名为i就完全够了。
如果使用“index”或“elementnumber”的话,不仅输入得更多,而且会使计算的细节不明确。
如果使用长变量名,有时候会使代码更难理解。
比较:
for(i=0to100)
array[i]=0
和
for(elementnumber=0to100)
array[elementnumber]=0;
∙函数名
函数名应反映函数执行什么操作以及返回什么内容。
函数在表达式中使用,通常用于if子句,因此它们的意图应一目了然。
if(checksize(x))
没有帮助作用,因为它没有告诉我们checksize是在出错时返回true还是在不出错时返回true;
而
if(validsize(x))
则使函数的意图很明确。
∙声明
所有的外部数据声明前都应加上extern关键字。
“指针”限定符“*”应紧邻变量名而不是类型。
例如,应使用
char*s,*t,*u;
而不是
char*s,t,u;
后一条语句没有错,但可能不是我们期望的,因为没有将“t”和“u”声明为指针。
∙头文件
头文件应按功能组织在一起,即,对单独子系统的声明应在单独的头文件中。
此外,当代码从一个平台移植到另一个平台时有可能发生更改的声明应位于单独的头文件中。
避免使用与库头文件名相同的专用头文件名。
语句#include"
math.h"
如果在当前目录中找不到所期望文件的话,会包括标准库math头文件。
如果这是您期望的结果,可以注释掉这行include语句。
最后说明一点,对头文件使用绝对路径名不是一个好主意。
C编译器的“include-path”选项(在许多系统上为-I—大写的i)是处理众多专用头文件库的首选方法;
它允许在不改变源文件的情况下重新组织目录结构。
∙scanf
在重要的应用程序中永远不要使用scanf。
它的错误检测不够完善。
请看下面的示例:
#include<
stdio.h>
intmain(void)
floatf;
printf("
Enteranintegerandafloat:
"
);
scanf("
%d%f"
&
i,&
f);
Iread%dand%f\n"
i,f);
return0;
}
测试运行
18252.38
Iread182and52.380001
另一个测试运行
67132478964.4
Iread-1876686696and4.400000
∙++和--
当对语句中的变量使用递增或递减运算符时,该变量不应在语句中出现一次以上,因为求值的顺序取决于编译器。
编写代码时不要对顺序作假设,也不要编写在某一机器上能够如期运作但没有明确定义的行为的代码:
inti=0,a[5];
a[i]=i++;
/*assigntoa[0]?
ora[1]?
*/
∙不要被表面现象迷惑
请看以下示例:
while(c=='
\t'
||c='
'
||c=='
\n'
)
c=getc(f);
乍一看,while子句中的语句似乎是有效的C代码。
但是,使用赋值运算符而不是比较运算符却产生了语义上不正确的代码。
=的优先级在所有运算符中是最低的,因此将以下列方式解释该语句(为清晰起见添加了括号):
while((c=='
||c)=('
))
赋值运算符左边的子句是:
(c=='
||c)
它不会产生左值。
如果c包含制表符,则结果是“true”,并且不会执行进一步的求值,而“true”不能位于赋值表达式的左边。
∙意图要明确。
当您编写的代码可以解释成另一种意图时,使用括号或用其它方法以确保您的意图清楚。
如果您以后必须处理该程序的话,这有助于您理解您当初的意图。
如果其他人要维护该代码,这可以让维护任务变得更简单。
用能预见可能出现错误的方式编码,有时是可行的。
例如,可以将常量放在比较等式的左边。
即,不编写:
而是编写:
while('
==c||'
==c)
用以下方法却会得到编译器诊断:
=c||'
这种风格让编译器发现问题;
上面的语句是无效的,因为它试图对“\t”赋值。
∙意想不到的麻烦。
各种C实现通常在某些方面各有不同。
坚持使用语言中可能对所有实现都是公共的部分会有帮助。
通过这样做,您更容易将程序移植到新的机器或编译器,并且不大会遇到编译器特殊性所带来的问题。
例如,考虑字符串:
/*/*/2*/**/1
这里利用了“最大适合(maximalmunch)”规则。
如果可以嵌套注释,则可将该字符串解释为:
/*/*/2*/**/1
两个/*符号与两个*/符号匹配,因此该字符串的值为1。
如果注释不嵌套,那么在有些系统上,注释中的/*就被忽略。
在另一些系统上会针对/*发出警告。
无论哪种情况,该表达式可解释为:
/*/*/2*/**/1
2*1求值得2。
∙清空输出缓冲区
当应用程序异常终止时,其输出的尾部常常会丢失。
应用程序可能没有机会完全清空它的输出缓冲区。
输出的某一部分可能仍在内存中,并且永远不会被写出。
在有些系统上,这一输出可能有几页长。
以这种方式丢失输出会使人误解,因为它给人的印象是程序在它实际失败很久之前就失败了。
解决这一问题的方法是强制将输出从缓冲区清除,特别是在调试期间。
确切的方法随系统的不同而有所不同,不过也有常用的方法,如下所示:
setbuf(stdout,(char*)0);
必须在将任何内容写到标准输出之前执行该语句。
理想情况下,这将是主程序中的第一条语句。
∙getchar()—宏还是函数
以下程序将其输入复制到其输出:
registerinta;
while((a=getchar())!
=EOF)
putchar(a);
从该程序除去#include语句将使该程序无法编译,因为EOF将是未定义的。
我们可以用以下方法重新编写该程序:
#defineEOF-1
这在许多系统上都可行,但在有些系统上运行要慢很多。
因为函数调用通常要花较长时间,所以常常把getchar实现为宏。
这个宏定义在stdio.h中,所以当除去#include<
时,编译器就不知道getchar是什么。
在有些系统上,假设getchar是返回一个int的函数。
实际上,许多C实现在其库中都有getchar函数,部分原因是为了防止这样的失误。
于是,在#include<
遗漏的情况下,编译器使用getchar的函数版本。
函数调用的开销使程序变慢。
putchar有同样的问题。
∙空指针
空指针不指向任何对象。
因此,为了赋值和比较以外的目的而使用空指针都是非法的。
不要重新定义NULL符号。
NULL符号应始终是常量值零。
任何给定类型的空指针总是等于常量零,而与值为零的变量或与某一非零常量的比较,其行为由实现定义。
反引用null指针可能会导致奇怪的事情发生。
∙a+++++b表示什么?
解析它的唯一有意义的方法是:
a+++++b
然而,“最大适合”规则要求将它分解为:
a+++++b
这在语法上是无效的:
它等于:
((a++)++)+b
但a++的结果不是左值,因此作为++的操作数是不可接受的。
于是,解析词法不明确性的规则使得以语法上有意义的方式解析该示例变得不可能。
当然,谨慎的办法实际上是在不能完全确定它们的意义的情况下,避免这样的构造。
当然,添加空格有助于编译器理解语句的意图,但(从代码维护的角度看)将这一构造分割成多行更可取:
++b;
(a++)+b;
∙小心处理函数
函数是C中最常用的结构概念。
它们应用于实现“自顶向下的”问题解决方法—即,将问题分解成越来越小的子问题,直到每个子问题都能够用代码表示。
这对程序的模块化和文档记录有帮助。
此外,由许多小函数组成的程序更易于调试。
如果有一些函数参数还不是期望的类型,则将它们强制转换为期望的类型,即使您确信没有必要也应该这样做,因为(如果不转换的话)它们可能在您最意料不到的时候给您带来麻烦。
换句话说,编译器通常将函数参数的类型提升和转换成期望的数据类型以符合函数参数的声明。
但是,在代码中以手工方式这样做可以清楚地说明程序员的意图,并且在将代码移植到其它平台时能确保有正确的结果。
如果头文件未能声明库函数的返回类型,那就自己声明它们。
用#ifdef/#endif语句将您的声明括起来,以备代码被移植到另一个平台。
函数原型应当用来使代码更健壮,使它运行得更快。
∙悬空else
除非知道自己在做什么,否则应避免“悬空else”问题:
if(a==1)
if(b==2)
***\n"
else
###\n"
规则是else附加至最近的if。
当有疑虑时,或有不明确的可能时,添加花括号以说明代码的块结构。
∙数组界限
检查所有数组的数组界限,包括字符串,因为在您现在输入“fubar”的地方,有人可能会输入“floccinaucinihilipilification”。
健壮的软件产品不应使用gets()。
C下标以零作为开始的这一事实使所有的计数问题变得更简单。
然而,掌握如何处理它们需要花些努力。
∙空语句
for或while循环的空语句体应当单独位于一行并加上注释,这样就表明这个空语句体是有意放置的,而不是遗漏了代码。
while(*dest++=*src++)
;
/*VOID*/
∙测试真(true)还是假(false)
不要以缺省方式测试非零值,即:
if(f()!
=FAIL)
优于
if(f())
尽管FAIL的值可能是0(在C中视为假(false))。
(当然,应当在这一风格与“函数名”一节中演示的构造之间作出权衡。
)当以后有人认为失败的返回值应该是-1而不是0时,显式的测试对您会有帮助。
常见的问题是使用strcmp函数测试字符串是否相等,决不应该以缺省方式处理它的结果。
更可取的方法是定义宏STREQ:
#defineSTREQ(str1,str2)(strcmp((str1),(str2))==0)
用这种方法,语句
If(STREQ(inputstring,somestring))...
就具有隐含的行为,该行为不大会在您不知情的情况下改变(人们往往不会重新编写或重新定义象strcmp()这样的标准库函数)。
不要用1检查相等性的布尔值(TRUE和YES等);
而要用0测试不等性(FALSE和NO等)。
绝大多数函数被确保在条件为假(false)时返回0,但仅在条件为真(true)时才返回非零。
因此,最好将
if(func()==TRUE){...
写成
if(func()!
=FALSE)
∙嵌入语句
使用嵌入赋值语句要看时间和地点。
在有些构造中,如果不使用更多且不易阅读的代码就没有更好的方法来实现结果:
while((c=getchar())!
=EOF){
processthecharacter
使用嵌入赋值语句来提高运行时性能是可能的。
但是,您应当在提高速度和降低可维护性之间加以权衡,在人为指定的位置使用嵌入赋值语句会导致可维护性降低。
x=y+z;
d=x+r;
不应被替换为:
d=(x=y+z)+r;
即使后者可能节省一个周期也不行。
最终,这两者之间在运行时间上的差异将随着优化器的增强而减少,易维护性的差异却将增加。
∙goto语句
应保守地使用goto。
从数层switch、for和while嵌套中跳出来时,使用该语句很有效,不过,如果有这样的需要,则表明应将内部构造分解成单独的函数。
for(...){
while(...){
if(wrong)
gotoerror;
error:
printamessage
当必须使用goto时,随附的标号应单独位于一行,并且同后续代码的左边相距一个制表符或位于一行的开头。
对goto语句和目标都应加上注释,说明其作用和目的。
∙switch中的“落空”(fall-through)
当一块代码有数个标号时,将这些标号放在单独的行。
这种风格与垂直空格的使用一致,并且使重新安排case选项(如果那是必需的话)成了一项简单的任务。
应对Cswitch语句的“落空”特征加以注释,以便于以后的维护。
如果这一特性曾给您带来“麻烦”,那么您就能够理解这样做的重要性!
switch(expr){
caseABC:
caseDEF:
statement;
break;
caseUVW:
/*FALLTHROUGH*/
caseXYZ:
尽管从技术上说,最后一个break不是必需的,但是,如果以后要在最后一个case之后添加了另一个case,那么一致地使用break可以防止“落空”错误。
如果使用defaultcase语句的话,它应当永远是最后一个,并且(如果它是最后的语句)不需要最后的break语句。
∙常量
符号常量使代码更易于阅读。
应尽量避免使用数字常量;
使用C预处理器的#define函数给常量赋予一个有意义的名称。
在一个位置(最好在头文件中)定义值还会使得管理大型程序变得更容易,因为只需更改定义就可以统一地更改常量值。
可以考虑使用枚举数据类型作为对声明只取一组离散值的变量的改进方法。
使用枚举还可以让编译器对您枚举类型的任何误用发出警告。
任何直接编码的数字常量必须至少有一个说明值的出处的注释。
常量的定义与它的使用应该一致;
例如,将540.0用于浮点数,而不要通过隐式浮点类型强制转换使用540。
也就是说,在有些情况下,常量0和1可以以本身的形式直接出现,而不要以定义的形式出现。
例如,如果某个for循环遍历一个数组,那么:
for(i=0;
i<
arraysub;
i++)
非常合理,而代码:
gate_t*front_gate=opens(gate[i],7);
if(front_gate==0)
error("
can'
topen%s\n"
gate[i]);
就不合理。
在第二个示例中,front_gate是指针;
当值是指针时,它应与NULL比较而不与0比较。
即使象1或0这样的简单值,通常最好也使用象TRUE和FALSE这样的定义来表示(有时YES和NO读起来更清楚)。
不要在需要离散值的地方使用浮点变量。
这是由于浮点数不精确的表示决定的(请参阅以上scanf中的第二个测试)。
使用<
=或>
=测试浮点数;
精确比较(==或!
=)也许不能检测出“可接受的”等同性。
应将简单的字符常量定义为字符文字而不是数字。
不提倡使用非文本字符,因为它们是不可移植的。
如果必须使用非文本字符,尤其是在字符串中使用它们,则应使用三位八进制数(不是一个字符)的转义字符(例如“\007”)来编写它们。
即便如此,这样的用法应视为与机器相关,并且应按这一情况来处理。
∙条件编译
条件编译可用于机器相关性、调试以及在编译时设置某些选项。
可以用无法预料的方式轻易地组合各种控制。
如果将#ifdef用于机器相关性,应确保当没有指定机器时会出错,而不是使用缺省的机器。
#error伪指令可以较方便地用于这一用途。
如果使用#ifdef进行优化,缺省值应是未优化的代码而不是不可编译或不正确的程序。
要确保对未优化的代码进行了测试。
其它
∙象Make这样用于编译和链接的实用程序极大简化了将应用程序从一个环境移到另一个环境的任务。
在开发期间,make仅对那些自上次使用make以来发生了更改的模块进行重新编译。
经常使用lint。
lint是C程序检查器,它检查C源文件以检测并报告函数定义和调用之间类型的不匹配和不一致,以及可能存在的程序错误等。
此外,研究一下编译器文档,了解那些使编译器变得“吹毛求疵”的开关。
编译器的工作是力求精确,因此通过使用适当的命令行选项让它报告可能存在的错误。
∙使应用程序中全局符号的数量最少。
这样做的好处之一是与系统定义的函数冲突的可能性降低。
∙许多程序在遗漏输入时会失败。
对所有的程序都应进行空输入测试。
这也可能帮助您理解程序的工作原理。
∙不要对您的用户或您所用的语言实现有任何过多的假设。
那些“不可能发生”的事情有时的确会发生。
健壮的程序可以防范这样的情形。
如果需要找到某个边界条件,您的用户将以某种方式找到它!
永远不要对给定类型的大小作任何假设,尤其是指针。
当在表达式中使用char类型时,大多数实现将它们当作无符号类型,但有些实现把它们作为有符号的类型。
当在算术表达式使用它们时,建议始终对它们进行类型强制转换。
不要依靠对自动变量和malloc返回的内存进行的初始化。
∙使您程序的目的和结构清晰。
∙要记住,可能会在以后要求您或别的人修改您的代码或在别的机器上运行它。
细心编写您的代码,