|
豆瓣评分9.1。程序设计实践- Kernighan-裘宗燕译。这本书从排错、测试、性能、可移植性、设计、界面、风格和记法等方面,讨论了程序设计中实际的、又是非常深刻和具有广泛意义的思想、技术和方法。这是一本牢牢占据经典书架最起眼位置的小册子,也是最符合KISS之道的神作之一。
+ z+ C7 J3 L U
7 [6 h4 q8 _0 |抛开已有的业界的best practices,这本书还修正了我记忆中的很多错误认识,Brian Kernighan、Rob Pike的大名自然是本书最好的招牌;但作者的求实态度更值得每一个认真的programmer学习。6 d5 q" c5 c! V% ~5 K7 O
+ S7 {8 o2 R1 Z7 p2 K, q
整体上,全书采用一个实际问题作为切入点并且贯穿全书,涵盖了programming工作的各个方面,简明扼要却又发人深省。这个例子的选取本身就是非同谨慎的,规模不至于过大(100多行代码)但是又足以说明问题。8 h. g3 n0 F, d T/ a. l4 W* t3 j
Chinaopub.coM
- X" q3 B' u8 Q3 P+ ~, f, A/ y. d& ] a6 {第1章风格
) v# G& |- y3 S3 h6 i34 d% ?1 n' D; T% }8 i/ S2 q" X* c& J
下载$ q1 t4 s- n, h+ p; s$ c k# X
elementArray [theElementIndex]= theel ementIndex
5 H, J- u2 h# C和0 d7 q, J/ D/ a/ n- j( A6 e
for (i=0; i nelms; i++)
: y/ a3 Y6 W) V- E- F. m; A) [: Melem<i>=i
}: T) O) C, V8 Q& o: C' o人们常常鼓励程序员使用长的变量名,而不管用在什么地方。这种认识完全是错误的,清晰) n+ M0 m0 C4 l7 J8 m; u3 e& Y7 a
性经常是随着简洁而来的。
( |( i+ B. R! n- K' [/ H# i* A& \现实中存在许多命名约定或者本地习惯。常见的比如:指针采用以p结尾的变量名,例如
' t. |& L2 Y$ W. E2 X; ]: lnode;全局变量用大写开头的变量名,例如G1obal;常量用完全由大写字母拼写的变量9 V. h6 P$ O$ P9 o$ C4 `
名,如 CONSTANTS等。有些程序设计工场采用的规则更加彻底,他们要求把变量的类型和用
2 U. O! c& M7 p; A) z! L; u1 K途等都编排进变量名字中。例如用pch说明这是一个字符指针,用 strO和 strfrom表示它' s& Z- |# O4 Y
们分别是将要被读或者被写的字符串等。至于名字本身的拼写形式,是使用 pending或
' P7 e$ o/ `- p* [7 C/ H( ~upeNding还是 num pending,这些不过是个人的喜好问题,与始终如一地坚持一种切
8 ^; k9 U" H& R6 W1 ?. z9 u合实际的约定相比,这些特殊规矩并不那么重要
& [+ y3 Q9 i( o9 ]( gj名约定能使自己的代码更容易理解,对别人写的代码也是一样。这些约定也使人在写
4 r U, S) {, s/ Z$ c- a代码时更容易决定事物的命名。对于长的程序,选择那些好的、具有说明性的、系统化的名- L! A% l4 v7 R. }& S
字就更加重要。( ]) Y9 `% k( O" E- H: n
C++的名字空间和Java的包为管理各种名字的作用域提供了方法,能帮助我们保持名字
; P' c/ [4 z5 w6 n. s的意义清晰,又能避免过长的名字。* I+ i, k: B, ?* ^5 ]
保持一致性。相关的东西应给以相关的名字,以说明它们的关系和差异。
8 H" S/ g* ~7 z5 b, n) i3 _除了太长之外,下面这个Java类中各成员的名字一致性也很差
6 P( [& F1 q/ V+ `class Userqueue i+ g' c6 }% c/ K3 v
int noofItemsInQ, frontofTheQueue, queue Capaci ty
( C1 R& R" u( ~" Y7 }A public int noofUsersInQueueO i.F# n0 |) G5 E/ D- N: X* m' y- U
这里同一个词“队列( queue)"在名字里被分别写为Q、ρueue或 queue。由于只能在类型
0 \' b1 V1 \5 p" D1 T X$ L vUserqueue里访问,类成员的名字中完全不必提到队列,因为存在上下文。所以2 J# N8 v0 o# M2 e
queue queue Capacity. `9 G% v' K# k4 R
完全是多余的。下面的写法更好:
! x7 T# \# V# c. L! t* Dclass UserQueue i' L: ]9 x- f: q, }" K
int nitems, front, capacity;+ ?' r0 e2 h5 l3 j( R4 ~; E
× public int nuserso t…}
0 P# Z& \" Q* B8 {2 d1 I为这时可以如此写
; t+ m1 _5 R+ [# y6 jqueue capaci ty++i9 c8 q+ B' Z3 X4 f8 c8 @/ q
n queue users o;
0 h! d, S3 |, r这样做在清晰性方面没有仼何损失。在这里还有可做的事情。例如 Items和useτs实际是同一种
: L+ I0 T3 g" O9 B1 E东西,同样东西应该使用一个概念
' L) P, J6 c$ I( } v" _; y+ r函数采用动作性的名字。函数名应当用动作性的动词,后面可以跟着名词8 w0 h% @: r1 V/ }. H' v) C8 T
now date. getTimeo
" m2 ~4 c, d* ^" E* P& v `1 Sputchar (\n')! ]8 y$ F7 W' j4 d! E& A( O i
对返回布尔类型值(真或者假)的函数命名,应该清楚地反映其返回值情况。下面这样的语句: a7 p8 z5 @1 Y6 Z
if (checkoctal(c))" J* X6 a; z- z' w8 W
1 a4 q" S0 R2 A- A2 k6 F& p程序设计实践
: ^6 N1 V" B6 ?) pChinaopub.com" l( i% g4 p0 J* T
下载8 S. G1 m, z% ?4 ~/ G; d
是不好的,因为它没有指明什么时候返回真,什么时候返回假。而
) e; w" k1 |) _/ D q `6 k: e' Aif (isoctal(c))- K V9 N6 t; k( y7 q/ W7 N9 c
就把事情说清楚了∶如果参数是八进制数字则返回真,否则为假。7 a( W% j6 a& S( f& ^; {
要准确。名字不仅是个标记,它还携带着给读程序人的信息。误用的名字可能引起奇怪的程
- c) x: K+ L* r3 h: O! ^: o序错误。
% V% r6 V8 a l/ G1 D1 w8 d# b, U本书作者之一写过一个名为 sorta1的宏,并且发布使用多年,而实际上它的实现是错误的) E D' Z" a2 j" v% z
# define i soca1)〔(c)>="0&&(c)="0,&&〔c)=bitoff &0x7;: e1 ?: E. i+ b0 ], O+ P8 U
有些结构似乎总是要引诱人们去滥用它们。运算符?:大概属于这一类:$ q. q _) q( }( A) A$ I4 F3 G" D
child=c! LC&&! RC)?0: ( LC?RC: LC)1 g% V9 U2 Y- |
如果不仔细地追踪这个表达式的每条路径,就几乎没办法弄清它到底是在做什么。下面的形; t+ L* V/ Z5 |0 l8 h$ j& ?
式虽然长了一点,但却更容易理解,其中的每条路径都非常明显7 y% }+ _! a* k1 U5 Z
if (lc ==0 & RC == o)
" \3 G2 c+ a" l; a q4 cchild=0:; [; |2 t* F, l" k2 F- q
else if (lc ==02 D' H i1 i% Y4 z4 c1 e! w
child rc:
8 Q; C( ?% i, @6 H, `" Velse
2 N8 o$ T: |' D, \child= lo' B. m- @ R4 e% G7 g8 H7 x: ~
运算符?:适用于短的表达式,这时它可以把4行的if-else程序变成1行。例如这样
( _; \4 A0 A" u5 Y6 y& Qmax= Ca>b)? a: b:0 o# D0 Z! n' P, K
或者下面这样7 j5 m) |/ M. x" b
printf( the list has %d item%s\n",n, n==1? : 5") W# _5 [) Z9 b8 @4 U8 u9 E: l
e) f0 C, E" z8 |$ Y `; ]% h第1章风格" [5 w9 E: p1 A6 k3 I# v( g4 m
7( g2 \6 g, B5 l a! R/ a$ j: G3 q
下载0 W# C1 R+ q. s6 O
但是它不应该作为条件语句的一般性替换。6 [8 W/ j% z5 q1 W, e- s
清晰性并不等同于简短。短的代码常常更凊楚,例如上面移字位的例子。不过有时代码5 c) S% s! A4 _! ~) Z
长一点可能更好,如上面把条件表达式改成条件语句的例子。在这里最重要的评价标准是易
! _) y8 S' U2 ^4 N4 b8 g于理解。
; l. \) Z' E8 D" i0 b# T5 U当心副作用。像艹这一类运算符具有副作用,它们除了返回一个值外,还将隐含地改变变量- |$ W5 j( o! D- e3 D! s: O
的值。副作用有时用起来很方便,但有时也会成为问题,因为变量的取值操作和更新操作可
$ d; h- F5 z$ e0 Z; A3 u7 n& s能不是同时发生。C和C++对与副作用有关的执行顺序并没有明确定义,因此下面的多次赋$ j" H: o$ e7 U9 l1 b2 Q q; k) F7 m: O
值语句很可能将产生错误结果
# N" t) `+ I' n) s% j' Rstr[i++]= str[i++]
+ Q2 b6 q" g4 r% Z这样写的意图是给str中随后的两个位置赋空格值,但实际效果却要依赖于i的更新时刻,很可
+ |5 v8 `' }3 y7 p能把str里的一个位置跳过去,也可能导致只对实际更新一次。这里应该把它分成两个语句0 x& y/ j0 q0 c2 B A) ~% y/ ]- A0 Q
str[i++]=’’;. Y* [( D$ c0 p- G5 d* y
str[i++% E" F+ U0 s* Z1 O- O' U# r
下面的赋值语句虽然只包含一个增量操作,但也可能给出不同的结果
5 \1 ]9 A$ z1 w. z. T5 R. E/ k( K ]- karray[i++]= i! j, Q5 Z$ Q5 c4 D
如果初始时i的值是3,那么数组元素有可能被设置成3或者4。6 o; o$ R/ t+ z, _8 i
不仅增量和减量操作有副作用,IO也是一种附带地下活动的操作。下面的例子希望从标
" W4 t7 v! ~0 I" b; y0 K" ^准输入读入两个互相有关的数:
$ Y/ M$ f3 z1 X5 O/ j* }1 G/ zscanf("‰d‰",&yr,& profit[yr]);/ `; R! h* X. U) j- A
这样做很有问题,因为在这个表达式里的一个地方修改了yr,而在另一个地方又使用它。这
5 ?$ T( g* v) f- a$ v, i样,除非yr的新取值与原来的值相同,否则 profit[yr]就不可能是正确的。你可能认为事% U% }/ v7 b ?; |
情依赖于参数的求值顺序,实际情况并不是这样。这里的问题是: scanf的所有参数都在函
+ r, y" Q7 e; \7 S8 N数被真正调用前已经求好值了,所以& profit[yr]实际使用的总是yr原来的值。这种问题可2 N; m6 E" W1 L1 X7 r
能在仼何语言里发生。纠正的方法就是把语句分解为两个:
6 D3 M7 p9 C* s% `+ Xscanf( %d",&yr)" \: p2 i) w( M
scanf ( %d",&profit[yr]);/ s& `! `7 w) d; m+ C
下面的练习里列举了各种具有副作用的表达式+ {5 Q7 ~. l4 M8 r
练习1-4改进下面各个程序片段:
) v# X/ p; O5 w) t& E4 nifC!(c=
8 A1 i6 f, E6 Z" r! ^$ C3 Vc=”Y"))
9 m q C- x. |' w" Yreturn;3 v2 e9 j/ |5 s
length (length BUFSIZE)? length BUFSIZE;& E9 ?% N" z8 L! J+ S9 t
flag=印1ag?0:1:
3 U+ m6 d& }( j ~. U" a' Oquote=(*1ne=")?1:0
8 g' Z) x+ k* m M' ^if val & 1)
v6 h9 B8 r: A: fbit 1:
9 h& _: e" x8 felse! e& H+ ]4 ~. y) V
bit o
4 {, K6 P) B* ~3 m8 }' f# i练习1-5下面的例子里有什么错?
- k3 p0 X/ Y/ ?$ n$ Q% _0 A4 F9 N- r3 f) g' m8 a
8
0 f# D! K9 H/ |) `* K/ R程序设计实践
% H! e( c% B! C下载% `5 Z* @ b' i0 N
nt read (int *ip)i0 [8 G' G( S4 T
scanf(%d",ip)
0 X- S' j' @5 zreturn☆
2 O+ A4 l- Y9 ]; o# einsert(&graph [vert], read (&val), read (&ch))
, B. _- i* Y c% s, Y9 A, e练习1-6列出下面代码片段在各种求值顺序下可能产生的所有不同的输出:. J' E5 L2 z5 a2 G5 f
printf(%d %d\n", n++, n++);- ~; |% r& F9 y5 t
在尽可能多的编译系统中试验,看看实际中会发生什么情况。
( _2 i9 k$ b+ ~1.3一致性和习惯用法
* X8 i5 X$ L5 g/ @; `% g致性带来的将是更好的程序。如果程序中的格式很随意,例如对数组做循环,一会儿
( e' X& g( q5 V5 b1 N1 `6 ^1 A9 V采用下标变量从下到上的方式,一会儿又用从上到下的方式;对字符串一会儿用 strcpy做复- N. s' o& \& t, J0 r L! a5 L
制,一会儿又用for循环做复制;等等。这些变化就会使人很难看清实际上到底是怎么回事。
4 l" S' z0 q9 @* l: G而如果相同计算的每次出现总是采用同样方式,任何变化就预示着是经过了深思熟虑,要求1 q/ E4 K2 b$ @1 v% k. v
读程序的人注意。
2 [' m$ ^! {5 I4 y# O使用一致的缩排和加括号风格。缩排可以显示岀程序的结构,那么什么样的缩排风格最好
/ m+ L2 L1 U. c$ }* H7 @. }2 f呢?是把花括号放在if的同一行,还是放在下面一行?程序员们常常就程序的这些编排形式
$ O$ Z/ [+ n& S6 m争论不休。实际上,特定风格远没有一致地使用它们重要。不要在这里浪费时间。应该取) {% G5 s0 F: B* v# T( K% C% i
种风格,当然作者希望是他们所采用的风格,然后一致地使用9 H4 c. ~, F" T0 \( }
应该在那些并不必须用花括号的地方都加上它们吗?与一般的圆括号一样,花括号也可以
. k# p" h2 r0 n2 J用来消除歧义,但是在使代码更清晰方面的作用却不那么大。为了保持某种-致性,许多程序
9 W4 {9 F4 R' ]1 B8 ?; w" z. E员总在循环或if的体外面加花括号。当这里只有一个语句时,加花括号就不是必要的,所以8 V1 k( t( ]7 u( Y9 T
作者倾向于去掉它们。如果你赞成我们的方法,那么就要注意在必需的时候不要忽略了它们,
- x8 }2 G" X0 N; n例如,在程序里需要解决“悬空的else( dangling else)”问题时。下面是这方面的一个例子:6 j7 Y" C% Z' G
if (month = FEb)I
% R$ G# H9 `* }if (year%4==0! R p1 u$ O9 e/ L6 d0 w
if (day >29)
9 D8 u. D M& [4 c5 c7 n4 rlegal FALSE
" y( O% i6 O: G$ leIse$ e: O( G. U/ W% f; W. n) g+ G8 v
if (day >28)" f7 U1 \1 d h' g. V+ D# F
legal FALSE; ? v/ b3 I( O% U6 K* }4 U
这里的缩排方式把人搞糊涂了,实际上e1se隶属于行7 F( z4 v# u" ]' w
if (day>29# }: J' x( ?! R
代码本身也是错的。如果一个i紧接在另一个之后,那么请一定加上花括号:: N# L$ }1 W% O9 f% D
if (month = FEB)i
& u. {6 i2 T$ K/ p) \7 e" Aif (year%4 == 0t
% a- v1 N( _2 Nif (day >29- T8 H- L5 u- h
Tegal =FALSE
2 ~* z i+ \% R' L. fF else i6 v! n* B0 S5 F" G$ q6 B1 v2 d
f(day >28
" f( |/ k& L* tlegal= FALSE;
; s1 j2 u7 L0 G$ m! R1 q9 N7 M
* X" ]4 \* j' n1 Q2 Q$ l- G: ZChinaopub.com- l8 Z6 q1 ?9 w& Z
第l章风格& F J* Y7 K/ z8 `" Y8 R! \, }# @
载" [6 I0 X: X% z( {" k' `' ]) W
语法驱动的编辑工具可以帮助避免这类错误
# a6 P a' c+ E; Q, u虽然上面程序里的错误已经修正,但这个结果代码还是很难懂。如果我们用一个变量保& W+ H. v, G: [, v. S6 z
存二月的天数,计算过程就很容易看明白了:5 P/ O9 m* D! C, ~# W. R8 ?- J
f(month = FEB)i8 V. u) |7 r. X+ |" j L% a1 y
int nday" k" B& d( m* e$ C/ l8 `! Q
nday = 28;5 F3 X, S' F+ b& N
if (year%4== 0)# l) F1 A& S3 i, h$ r7 J. m' F
nday =29
: K$ U! z. k* [+ g# F( M( e6 t: P' zif (day >nday)
) v/ d5 w1 c; t* F& Tlegal= False! h3 E6 a j( W, s
这段代码实际上还是错的—2000年是闰年,而1900和2100都不是。要把现在这个结构改正
. z$ S' x& ~& I W确是非常容易的。
& @' @1 A: f& y$ X此外,如果你工作在一个不是自己写的程序上,请注意保留程序原有的风格。当你需要
& v$ \. N" j4 j! g4 E做修改时,不要使用你自己的风格,即使你特别喜欢它。程序的一致性比你本人的习惯更重
5 n) T, b* J# x! [4 {5 _0 \( u要,因为这将使随你之后的其他人生活得更容易些。
3 V. M. Y0 \! D$ V% p6 ?' ~+ K1 C为了一致性,使用习惯用法。和自然语言一样,程序设计语言也有许多惯用法,也就是那些( L% h. w1 x7 ^! h9 W; @% K8 R6 a9 }
经验丰富的程序员写常见代码片段的习惯方式。在学习一个语言的过程中,一个中心问题就
; \/ j g/ ?2 a. o( T- c" V是逐渐熟悉它的习惯用法。
" g0 r$ J: c0 A8 G. Y常见习惯用法之一是循环的形式。考虑在C、C++和Java中逐个处理n元数组中各个元素
1 j; F" U3 R5 d$ k d/ U2 K的代码,例如要对这些元素做初始化。有人可能写出下面的循环4 M) i9 `$ X# }
while (i =0;)$ G# s" w0 e- L3 d2 Q) U
array<i>= 1.0* w: S- }! b! b. B2 }
所有这些都正确,而习惯用法的形式却是
8 M6 g& j. I, k2 Wfor (i =0: i |
|