论坛元老
- 威望
- 0
- 贡献
- 53
- 热心值
- 2
- 金币
- 5530
- 注册时间
- 2020-8-31
|
豆瓣评分9.1。程序设计实践- Kernighan-裘宗燕译。这本书从排错、测试、性能、可移植性、设计、界面、风格和记法等方面,讨论了程序设计中实际的、又是非常深刻和具有广泛意义的思想、技术和方法。这是一本牢牢占据经典书架最起眼位置的小册子,也是最符合KISS之道的神作之一。
抛开已有的业界的best practices,这本书还修正了我记忆中的很多错误认识,Brian Kernighan、Rob Pike的大名自然是本书最好的招牌;但作者的求实态度更值得每一个认真的programmer学习。
整体上,全书采用一个实际问题作为切入点并且贯穿全书,涵盖了programming工作的各个方面,简明扼要却又发人深省。这个例子的选取本身就是非同谨慎的,规模不至于过大(100多行代码)但是又足以说明问题。
Chinaopub.coM
第1章风格
3
下载
elementArray [theElementIndex]= theel ementIndex
和
for (i=0; i nelms; i++)
elem=i
人们常常鼓励程序员使用长的变量名,而不管用在什么地方。这种认识完全是错误的,清晰
性经常是随着简洁而来的。
现实中存在许多命名约定或者本地习惯。常见的比如:指针采用以p结尾的变量名,例如
node;全局变量用大写开头的变量名,例如G1obal;常量用完全由大写字母拼写的变量
名,如 CONSTANTS等。有些程序设计工场采用的规则更加彻底,他们要求把变量的类型和用
途等都编排进变量名字中。例如用pch说明这是一个字符指针,用 strO和 strfrom表示它
们分别是将要被读或者被写的字符串等。至于名字本身的拼写形式,是使用 pending或
upeNding还是 num pending,这些不过是个人的喜好问题,与始终如一地坚持一种切
合实际的约定相比,这些特殊规矩并不那么重要
j名约定能使自己的代码更容易理解,对别人写的代码也是一样。这些约定也使人在写
代码时更容易决定事物的命名。对于长的程序,选择那些好的、具有说明性的、系统化的名
字就更加重要。
C++的名字空间和Java的包为管理各种名字的作用域提供了方法,能帮助我们保持名字
的意义清晰,又能避免过长的名字。
保持一致性。相关的东西应给以相关的名字,以说明它们的关系和差异。
除了太长之外,下面这个Java类中各成员的名字一致性也很差
class Userqueue i
int noofItemsInQ, frontofTheQueue, queue Capaci ty
A public int noofUsersInQueueO i.F
这里同一个词“队列( queue)"在名字里被分别写为Q、ρueue或 queue。由于只能在类型
Userqueue里访问,类成员的名字中完全不必提到队列,因为存在上下文。所以
queue queue Capacity
完全是多余的。下面的写法更好:
class UserQueue i
int nitems, front, capacity;
× public int nuserso t…}
为这时可以如此写
queue capaci ty++i
n queue users o;
这样做在清晰性方面没有仼何损失。在这里还有可做的事情。例如 Items和useτs实际是同一种
东西,同样东西应该使用一个概念
函数采用动作性的名字。函数名应当用动作性的动词,后面可以跟着名词
now date. getTimeo
putchar (\n')
对返回布尔类型值(真或者假)的函数命名,应该清楚地反映其返回值情况。下面这样的语句
if (checkoctal(c))
程序设计实践
Chinaopub.com
下载
是不好的,因为它没有指明什么时候返回真,什么时候返回假。而
if (isoctal(c))
就把事情说清楚了∶如果参数是八进制数字则返回真,否则为假。
要准确。名字不仅是个标记,它还携带着给读程序人的信息。误用的名字可能引起奇怪的程
序错误。
本书作者之一写过一个名为 sorta1的宏,并且发布使用多年,而实际上它的实现是错误的
# define i soca1)〔(c)>="0&&(c)<=·8)
正确的应该是
# define isoctal(c)〔(c)>="0,&&〔c)<="7)
这是另外—种情况:名字具有正确的含义,而对应的实现却是错的,一个合情合理的名字掩
盖了一个害人的实现。
下面是另一个例子,其中的名字和实现完全是矛盾的:
public boolean inTable(object obj)i
int j= this getIndex(obj):
return (i== tAble);
函数 getindex如果找到了有关对象,就返回0到nTab1e-1之间的一个值;否则返回
table值。而这里 intable返回的布尔值却正好与它名字所说的相反。在写这段代码时,
这种写法未必会引起什么问题。但如果后来修改这个程序,很可能是由别的程序员来做,这
个名字肯定会把人弄糊涂。
练习1-1评论下面代码中名字和值的选择
#define true o
#define FAlse
if ((ch getchar O)== EOF)
not eof FAlse
练习1-2改进下面的函数
int smaller(char *s, char *t)
if〔 strcmp(s,t)<1)
return 1.
e se
return 0;
练习1-3大声读出下面的代码
if (falloc(SMRHSHSCRTCH, S_IFEXT 0644, MAXRODDHSH))< 0)
1.2表达式和语句
名字的合理选择可以帮助读者理解程序,同样,我们也应该以尽可能-目了然的形式写好表
达式和语句。应该写最凊晰的代码,通过给运算符两边加空格的方式说明分组情况,更一般的是
通过格式化的方式来帮助阅读。这些都是很琐碎的事情,但却又是非常有价值的,就像保持书桌
整洁能使你容易找到东西一样。与你的书桌不同的是,你的程序代码很可能还会被别人使用。
第l章风格
5
载
用缩行显示程序的结构。采用一种一致的缩行风格,是使程序呈现出结构清晰的最省力的方
法。下面这个例子的格式太糟糕了
or(n+;n<100; field[n++]=\0);
0’; return(\n)
重新调整格式,可以改得好一点
for(n++;n<100; field[n++]=”\0)
\0
return(、n)
更好的是把赋值作为循环体,增量运算单独写。这样循环的格式更普通也更容易理解
for(n++;n<100;n++)
field[n]=”\0’;
0
return ,\n
使用表达式的自然形式。表达式应该写得你能大声念出来。含有否定运算的条件表达式比较
难理解
if ( (block_id actblks) !(block_id >=unblocks))
在两个测试中都用到否定,而它们都不是必要的。应该改变关系运算符的方向,使测试变成
肯定的:
if ((block_id > actblks)I I (block_id unblocks))
现在代码读起来就自然多了
用加括号的方式排除二义性。括号表示分组,即使有时并不必要,加了括号也可能把意图表
示得更清楚。在上面的例子里,内层括号就不是必需的,但加上它们没有坏处。熟练的程序
员会忽略它们,因为关系运算符(<<
!=>=)逻辑运算符(&和1)的优先级更高。
在混合使用互相无关的运算符时,多写几个括号是个好主意。C语言以及与之相关的语言
存在很险恶的优先级问题,在这里很容易犯错误。例如,由于逻辑运算符的约束力比赋值运
算符强,在大部分混合使用它们的表达式中,括号都是必需的。
while ((c getchar O)!= EOF)
字位运算符(和)的优先级低于关系运算符(比如==),不管出现在哪里
f (x&MASK = BITS)
实际上都意味着
if (x&(MASK==BITS))
这个表达式所表达的肯定不会是程序员的本意。在这里混合使用了字位运算和关系运算符号,
表达式里必须加上括号
if ((x&MASK== BITS)
如果一个表达式的分组情况不是一目了然的话,加上括号也可能有些帮助,虽然这种括
6
程序设计实践
下载
号可能不是必需的。下面的代码本来不必加括号
leap_year=y%4==0&&y%100!=0|ly%400==0;
但加上括号,代码将变得更容易理解了
1eap_year=((y%4=0)&(y‰100!=0))|(y%400==0);
这里还去掉了几个空格:使优先级高的运算符与运算对象连在一起,帮助读者更快地看清表
达式的结构。
分解复杂的表达式。C、C++和Java语言都有很丰富的表达式语法结构和很丰富的运算符。因
此,在这些语言里就很容易把一大堆东西塞进一个结构中。下面这样的表达式虽然很紧凑,
但是它塞进一个语句里的东西确实太多了
?☆X+=(★Xp=(2*k<(n-m)?c[k+1]:d[k--])
把它分解成几个部分,意思更容易把握
f(2*k< n-m)
☆Xp=C[k+1
else
*xp= d[k--];
要清晰。程序员有时把自己无穷尽的创造力用到了写最简短的代码上,或者用在寻求得到结
果的最巧妙方法上。有时这种技能是用错了地方,因为我们的目标应该是写出最清晰的代码,
而不是最巧妙的代码。
下面这个难懂的计算到底想做什么?
subkey subkey >>(bitoff -(Cbitoff > 3)<< 3))
最内层表达式把 bitoff右移3位,结果又被重新移回来。这样做实际上是把变量的最低3位设
置为0。从 bitoff的原值里面减掉这个结果,得到的将是 bitoff的最低3位。最后用这3位
的值确定 subkey的右移位数
上面的表达式与下面这个等价
subkey subkey > (bitoff 0x7);
要弄清前一个版本的意思简直像猜谜语,而后面这个则又短又清楚。经验丰富的程序员会把
它写得更短,换一个赋值运算符
subkey >>=bitoff &0x7;
有些结构似乎总是要引诱人们去滥用它们。运算符?:大概属于这一类:
child=c! LC&&! RC)?0: ( LC?RC: LC)
如果不仔细地追踪这个表达式的每条路径,就几乎没办法弄清它到底是在做什么。下面的形
式虽然长了一点,但却更容易理解,其中的每条路径都非常明显
if (lc ==0 & RC == o)
child=0:
else if (lc ==0
child rc:
else
child= lo
运算符?:适用于短的表达式,这时它可以把4行的if-else程序变成1行。例如这样
max= Ca>b)? a: b:
或者下面这样
printf( the list has %d item%s\n",n, n==1? : 5")
第1章风格
7
下载
但是它不应该作为条件语句的一般性替换。
清晰性并不等同于简短。短的代码常常更凊楚,例如上面移字位的例子。不过有时代码
长一点可能更好,如上面把条件表达式改成条件语句的例子。在这里最重要的评价标准是易
于理解。
当心副作用。像艹这一类运算符具有副作用,它们除了返回一个值外,还将隐含地改变变量
的值。副作用有时用起来很方便,但有时也会成为问题,因为变量的取值操作和更新操作可
能不是同时发生。C和C++对与副作用有关的执行顺序并没有明确定义,因此下面的多次赋
值语句很可能将产生错误结果
str[i++]= str[i++]
这样写的意图是给str中随后的两个位置赋空格值,但实际效果却要依赖于i的更新时刻,很可
能把str里的一个位置跳过去,也可能导致只对实际更新一次。这里应该把它分成两个语句
str[i++]=’’;
str[i++
下面的赋值语句虽然只包含一个增量操作,但也可能给出不同的结果
array[i++]= i
如果初始时i的值是3,那么数组元素有可能被设置成3或者4。
不仅增量和减量操作有副作用,IO也是一种附带地下活动的操作。下面的例子希望从标
准输入读入两个互相有关的数:
scanf("‰d‰",&yr,& profit[yr]);
这样做很有问题,因为在这个表达式里的一个地方修改了yr,而在另一个地方又使用它。这
样,除非yr的新取值与原来的值相同,否则 profit[yr]就不可能是正确的。你可能认为事
情依赖于参数的求值顺序,实际情况并不是这样。这里的问题是: scanf的所有参数都在函
数被真正调用前已经求好值了,所以& profit[yr]实际使用的总是yr原来的值。这种问题可
能在仼何语言里发生。纠正的方法就是把语句分解为两个:
scanf( %d",&yr)
scanf ( %d",&profit[yr]);
下面的练习里列举了各种具有副作用的表达式
练习1-4改进下面各个程序片段:
ifC!(c=
c=”Y"))
return;
length (length BUFSIZE)? length BUFSIZE;
flag=印1ag?0:1:
quote=(*1ne=")?1:0
if val & 1)
bit 1:
else
bit o
练习1-5下面的例子里有什么错?
8
程序设计实践
下载
nt read (int *ip)i
scanf(%d",ip)
return☆
insert(&graph [vert], read (&val), read (&ch))
练习1-6列出下面代码片段在各种求值顺序下可能产生的所有不同的输出:
printf(%d %d\n", n++, n++);
在尽可能多的编译系统中试验,看看实际中会发生什么情况。
1.3一致性和习惯用法
致性带来的将是更好的程序。如果程序中的格式很随意,例如对数组做循环,一会儿
采用下标变量从下到上的方式,一会儿又用从上到下的方式;对字符串一会儿用 strcpy做复
制,一会儿又用for循环做复制;等等。这些变化就会使人很难看清实际上到底是怎么回事。
而如果相同计算的每次出现总是采用同样方式,任何变化就预示着是经过了深思熟虑,要求
读程序的人注意。
使用一致的缩排和加括号风格。缩排可以显示岀程序的结构,那么什么样的缩排风格最好
呢?是把花括号放在if的同一行,还是放在下面一行?程序员们常常就程序的这些编排形式
争论不休。实际上,特定风格远没有一致地使用它们重要。不要在这里浪费时间。应该取
种风格,当然作者希望是他们所采用的风格,然后一致地使用
应该在那些并不必须用花括号的地方都加上它们吗?与一般的圆括号一样,花括号也可以
用来消除歧义,但是在使代码更清晰方面的作用却不那么大。为了保持某种-致性,许多程序
员总在循环或if的体外面加花括号。当这里只有一个语句时,加花括号就不是必要的,所以
作者倾向于去掉它们。如果你赞成我们的方法,那么就要注意在必需的时候不要忽略了它们,
例如,在程序里需要解决“悬空的else( dangling else)”问题时。下面是这方面的一个例子:
if (month = FEb)I
if (year%4==0
if (day >29)
legal FALSE
eIse
if (day >28)
legal FALSE
这里的缩排方式把人搞糊涂了,实际上e1se隶属于行
if (day>29
代码本身也是错的。如果一个i紧接在另一个之后,那么请一定加上花括号:
if (month = FEB)i
if (year%4 == 0t
if (day >29
Tegal =FALSE
F else i
f(day >28
legal= FALSE;
Chinaopub.com
第l章风格
载
语法驱动的编辑工具可以帮助避免这类错误
虽然上面程序里的错误已经修正,但这个结果代码还是很难懂。如果我们用一个变量保
存二月的天数,计算过程就很容易看明白了:
f(month = FEB)i
int nday
nday = 28;
if (year%4== 0)
nday =29
if (day >nday)
legal= False
这段代码实际上还是错的—2000年是闰年,而1900和2100都不是。要把现在这个结构改正
确是非常容易的。
此外,如果你工作在一个不是自己写的程序上,请注意保留程序原有的风格。当你需要
做修改时,不要使用你自己的风格,即使你特别喜欢它。程序的一致性比你本人的习惯更重
要,因为这将使随你之后的其他人生活得更容易些。
为了一致性,使用习惯用法。和自然语言一样,程序设计语言也有许多惯用法,也就是那些
经验丰富的程序员写常见代码片段的习惯方式。在学习一个语言的过程中,一个中心问题就
是逐渐熟悉它的习惯用法。
常见习惯用法之一是循环的形式。考虑在C、C++和Java中逐个处理n元数组中各个元素
的代码,例如要对这些元素做初始化。有人可能写出下面的循环
while (i <= n-1)
array[i++]= 1.0;
或者是这样的
or
0;i
array [i++] =1.0
也可能是
r(i=n;--i>=0;)
array= 1.0
所有这些都正确,而习惯用法的形式却是
for (i =0: i<n; i++)
array = 1.0;
这并不是一种随意的选择:这段代码要求访问n元数组里的每个元素,下标从0到n-1。在这
里所有循环控制都被放在一个for里,以递增顺序运行,并使用++的习惯形式做循环变量的
更新。这样做还保证循环结束时下标变量的值是一个已知值,它刚刚超出数组里最后元素的
位置。熟悉C语言的人不用琢磨就能理解它,不加思考就能正确地写岀它来。
C++或Java里常见的另一种形式是把循环变量的声明也包括在内
for (int i=0:i<n; i++)
array
=1.0;
下面是在C语言里扫描一个链表的标准循环
for (p= list: p!= NULL; p= p->next)
10
程序设计实践
Chinapub.com
下载
同样,所有的控制都放在一个for里面。
对于无穷循环,我们喜欢用
for (ii
但
while (1)
也很流行。请不要使用其他形式。
缩排也应该采用习惯形式。下面这种垂直排列会妨碍人的阅读,它更像三个语句而不像
个循环:
for
?
ap arr
ap< arr + 128;
ap++=0
?
写成标准的循环形式,读起来就容易多了
or (ap =arr; ap< arr+128; ap++)
*ap =0;
这种故意拉长的格式还会使代码摊到更多的页或显示屏去,进一步妨碍人的阅读。
常见的另一个惯用法是把一个赋值放进循环条件里
while ((c getchar O)!= EOF)
putchar(c)
do- While循环远比for和 While循环用得少,因为它将至少执行循环体一次,在代码
的最后而不是开始执行条件测试。这种执行方式在许多情况下是不正确的,例如下面这段重
写的使用 getchar的循环
?
c= getchar o:
putchar(c)
while (c ! EOF)
在这里测试被放在对 putchar的调用之后,将使这个代码段无端地多写出一个字符。只有在
某个循环体总是必须至少执行一次的情况下,使用do- While循环才是正确的。后面会看到
这种例子。
一致地使用习惯用法还有另一个优点,那就是使非标准的循环很容易被注意到,这种情
况常常预示着有什么问题
int 1, *aRray, nmemb;
aRray
malloc(nmemb sizeof (int))
?
for (i=0; i <= nmemb; i++)
aRray
=i
在这里分配了 nmemb个项的空间,从 aRray0到 aRray[ nmemb-1l。但由于采用的是<
|
|