资讯·论坛·笑话大全·QQ表情·设为首页
发新话题
打印

[c语言] C语言的陷阱和缺陷

C语言的陷阱和缺陷

C语言的陷阱和缺陷0 简介
" r7 d8 N7 B' t- R2 D& P+ Mwww.520diannao.com
* Z& `" g( u6 O$ T* \0 m9 Q; x1 Q& U我爱电脑技术社区--打造最好的电脑技术自学交流平台    C语言及其典型实现被设计为能被专家们容易地使用。这门语言简洁并附有表达力。但有一些限制可以保护那些浮躁的人。一个浮躁的人可以从这些条款中获得一些帮助。
. K% g% Q0 ]8 N
& B% l* x9 O" w# D- g, @打造最好的电脑自学交流论坛    在本文中,我们将会看一看这些未可知的益处。这是由于它的未可知,我们无法为其进行完全的分类。不过,我们仍然通过研究为了一个C程序的运行所需要做的事来做到这些。我们假设读者对C语言至少有个粗浅的了解。
9 K* H% w" y: \& k( I& Q& Qwww.520diannao.com
6 C; i, |! N# w: S4 E: P打造最好的电脑自学交流论坛    第一部分研究了当程序被划分为记号时会发生的问题。第二部分继续研究了当程序的记号被编译器组合为声明、表达式和语句时会出现的问题。第三部分研究了由多个部分组成、分别编译并绑定到一起的C程序。第四部分处理了概念上的误解:当一个程序具体执行时会发生的事情。第五部分研究了我们的程序和它们所使用的常用库之间的关系。在第六部分中,我们注意到了我们所写的程序也不并不是我们所运行的程序;预处理器将首先运行。最后,第七部分讨论了可移植性问题:一个能在一个实现中运行的程序无法在另一个实现中运行的原因。
9 B7 K; T0 e" N4 o
* |# i* J0 N+ k( A打造最好的电脑自学交流论坛1 词法缺陷
- h8 w9 p7 i0 D) Q0 O, _1 d打造最好的电脑自学交流论坛
6 Y& Z6 S$ J) z* ]* h打造最好的电脑自学交流论坛    编译器的第一个部分常被称为词法分析器(lexical analyzer)。词法分析器检查组成程序的字符序列,并将它们划分为记号(token)一个记号是一个有一个或多个字符的序列,它在语言被编译时具有一个(相关地)统一的意义。在C中, 例如,记号->的意义和组成它的每个独立的字符具有明显的区别,而且其意义独立于->出现的上下文环境。另外一个例子,考虑下面的语句:我爱电脑技术论坛) C3 Y; p4 a! j5 E

9 Q6 J! [- B# P1 |6 o' k0 kif(x > big) big = x;我爱电脑技术论坛3 ~" U8 Q% J  q* ^' q
6 m" q# D& p) X: G) U
        该语句中的每一个分离的字符都被划分为一个记号,除了关键字if和标识符big的两个实例。事实上,C程序被两次划分为记号。首先是预处理器读取程序。它必须对程序进行记号划分以发现标识宏的标识符。它必须通过对每个宏进行求值来替换宏调用。最后,经过宏替换的程序又被汇集成字符流送给编译器。编译器再第二次将这个流划分为记号。www.520diannao.com. b( E2 }9 h+ p; `9 Y7 K
我爱电脑技术社区--打造最好的电脑技术自学交流平台  \2 t/ M6 B% F1 y+ }! [
    在这一节中,我们将探索对记号的意义的普遍的误解以及记号和组成它们的字符之间的关系。稍后我们将谈到预处理器。
5 j/ p) h6 ^. ^. _电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
) D, O7 t+ P' X( p; t' ]: X9 P1.1 = 不是 ==
5 L: T+ _, _/ e9 z* e打造最好的电脑自学交流论坛
7 o1 f1 P  P+ O6 Q5 L# b我爱电脑技术论坛        从Algol派生出来的语言,如Pascal和Ada,用:=表示赋值而用=表示比较。而C语言则是用=表示赋值而用==表示比较。这是因为赋值的频率要高于比较,因此为其分配更短的符号。此外,C还将赋值视为一个运算符,因此可以很容易地写出多重赋值(如a = b = c),并且可以将赋值嵌入到一个大的表达式中。这种便捷导致了一个潜在的问题:可能将需要比较的地方写成赋值。因此,下面的语句好像看起来是要检查x是否等于y:www.520diannao.com' m) r" u' B4 |- ^& y
打造最好的电脑自学交流论坛2 _5 i. j5 q1 V) E/ M
if(x = y)
# B# h0 u, S; n. N( q9 T& pfoo();
3 T2 o( l3 q( Y8 ]我爱电脑技术社区--打造最好的电脑技术自学交流平台
* n5 K/ l/ M* k( Y+ q# Bwww.520diannao.com        而实际上是将x设置为y的值并检查结果是否非零。在考虑下面的一个希望跳过空格、制表符和换行符的循环:
0 P) P$ F/ J0 O  T- wwww.520diannao.com
, M; v& m6 e5 G" M" \我爱电脑技术社区--打造最好的电脑技术自学交流平台while(c == ' ' || c = '\t' || c == '\n')www.520diannao.com: q) s; x2 b; q
c = getc(f);
0 L7 u# A; B0 [; y( H' x2 q2 t* b打造最好的电脑自学交流论坛# R$ k% i7 M+ Q- `
        在与'\t'进行比较的地方程序员错误地使用=代替了==。这个“比较”实际上是将'\t'赋给c,然后判断c的(新的)值是否为零。因为'\t'不为零,这个“比较”将一直为真,因此这个循环会吃尽整个文件。这之后会发生什么取决于特定的实现是否允许一个程序读取超过文件尾部的部分。如果允许,这个循环会一直运行。一些C编译器会对形如e1 = e2的条件给出一个警告以提醒用户。当你确实需要先对一个变量进行赋值之后再检查变量是否非零时,为了在这种编译器中避免警告信息,应考虑显式给出比较符。换句话说,将:www.520diannao.com8 t" \' T  v+ }: q  O1 E. Z3 y

" ]* W1 W. F# n* {# U电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站if(x = y)
# c& K/ U, K+ T' e$ L, rwww.520diannao.comfoo();
$ ^7 I8 }; n  b$ ^) z8 U# V电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
) Q! O$ H2 s2 \( h  p我爱电脑技术论坛改写为:
' y* @7 {- p) L. u5 k5 Q4 S2 A3 F打造最好的电脑自学交流论坛
0 ^' r) S/ L2 Z+ Y: ?$ y0 pwww.520diannao.comif((x = y) != 0)
- d; Q7 \& N' e打造最好的电脑自学交流论坛foo();
) u+ @) T- z$ s我爱电脑技术论坛电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站! a& X+ i# ?& Q0 S& z! Y- G0 R# z
这样可以清晰地表示你的意图。电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站4 D, i3 k& f2 X9 Y/ i3 c2 ~

9 H7 z% _( H3 Q9 `. k" s; L1.2 & 和 | 不是 && 和 ||打造最好的电脑自学交流论坛6 _  j7 e% s6 n
我爱电脑技术论坛! q2 t" n, {3 F1 d
        容易将==错写为=是因为很多其他语言使用=表示比较运算。 其他容易写错的运算符还有&和&&,或|和||,这主要是因为C语言中的&和|运算符于其他语言中具有类似功能的运算符大为不同。我们将在第4节中贴近地观察这些运算符。www.520diannao.com9 ^" S8 Y# e; ]. C+ q# O" p
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站7 Q) {) _: }: O, b; j) o2 N! G
1.3 多字符记号我爱电脑技术论坛. m* ]7 Y2 l8 c% p+ H

& \; E$ j( w+ E* i& {% \  [我爱电脑技术论坛    一些C记号,如/、*和=只有一个字符。而其他一些C记号,如/*和==,以及标识符,具有多个字符。当C编译器遇到紧连在一起的/和*时,它必须能够决定是将这两个字符识别为两个分离的记号还是一个单独的记号。C语言参考手册说明了如何决定:“如果输入流到一个给定的字符串为止已经被识别为记号,则应该包含下一个字符以组成能够构成记号的最长的字符串”。因此,如果/是一个记号的第一个字符,并且/后面紧随了一个*,则这两个字符构成了注释的开始,不管其他上下文环境。下面的语句看起来像是将y的值设置为x的值除以p所指向的值:
+ o# G. M8 [1 l打造最好的电脑自学交流论坛我爱电脑技术论坛/ A$ ~- ]; }; y
y = x/*p /* p 指向除数 */;我爱电脑技术社区--打造最好的电脑技术自学交流平台% I. G, D, B4 R5 O1 p% v4 J; S

5 d9 s" |- a; e% X" T8 s6 J' m0 zwww.520diannao.com        实际上,/*开始了一个注释,因此编译器简单地吞噬程序文本,直到*/的出现。换句话说,这条语句仅仅把y的值设置为x的值,而根本没有看到p。将这条语句重写为:我爱电脑技术社区--打造最好的电脑技术自学交流平台: ]$ q4 R& x* S4 S
打造最好的电脑自学交流论坛$ C+ o' a" r# e% v8 \7 d
y = x / *p /* p 指向除数 */;
% C2 O' j6 S1 R. ^6 X我爱电脑技术社区--打造最好的电脑技术自学交流平台: l" I* t/ |" c9 v' [* q
或者干脆是
! S3 i5 H; Y" r" f, U0 Xwww.520diannao.com2 X8 |1 F# Z$ ]
y = x / (*p) /* p指向除数 */;电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站5 f, [# r6 u  ~3 e/ J% h9 c7 [, J
我爱电脑技术论坛$ t2 W2 D0 \: N
它就可以做注释所暗示的除法了。
3 E" y2 q4 C# s打造最好的电脑自学交流论坛
# n. ~3 `/ T* j# |* |; [2 V  T我爱电脑技术社区--打造最好的电脑技术自学交流平台    这种模棱两可的写法在其他环境中就会引起麻烦。例如,老版本的C使用=+表示现在版本中的+=。这样的编译器会将
& {: ?* u8 T7 Z我爱电脑技术社区--打造最好的电脑技术自学交流平台我爱电脑技术论坛% B% R+ Y0 v) x0 V
a=-1;
' ?$ o" \4 Z- B) ], r$ b4 v电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站打造最好的电脑自学交流论坛' A/ s& s- n6 X
视为我爱电脑技术社区--打造最好的电脑技术自学交流平台( A- |+ S9 G) a

/ X2 g8 O6 z; E" bwww.520diannao.coma =- 1;
2 |5 O: q0 h9 U% z- m我爱电脑技术论坛
# k! j. x; }$ t: g/ X5 M! A我爱电脑技术论坛
. Z; J- o1 A5 N; f& L3 B7 J我爱电脑技术社区--打造最好的电脑技术自学交流平台打造最好的电脑自学交流论坛: a" b) {2 u4 R* Q- I
a = a - 1;我爱电脑技术社区--打造最好的电脑技术自学交流平台/ E* J( f/ M- H2 s2 f/ ~
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站) m* g( ?3 ?7 i$ b: B3 H
这会让打算写我爱电脑技术论坛- M. ^* _7 R. @# \
打造最好的电脑自学交流论坛4 Q' B) q; M# J( v, O% V# s. S
a = -1;打造最好的电脑自学交流论坛6 T9 e9 ]8 F1 h% q1 ]+ c" F1 t7 l
我爱电脑技术论坛9 N) I* B( D$ c1 w
的程序员感到吃惊。另一方面,这种老版本的C编译器会将打造最好的电脑自学交流论坛$ w3 ?& l) Z8 W
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站/ [& T- M% K) e$ g! V0 A  f' L
a=/*b;www.520diannao.com( s7 P4 c% b8 d8 B+ }9 a0 |* l6 k
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站* J, I. l5 T7 T, T
断句为
1 c( d! X5 b! f1 F& l% o8 I" B我爱电脑技术社区--打造最好的电脑技术自学交流平台www.520diannao.com$ N: T& v- R, T& t! e) p4 Q$ y/ n9 ~
a =/ *b;www.520diannao.com* e  Z6 f1 i' C; ~$ b

$ A6 ?7 s5 ^0 ]; a  r! r" B我爱电脑技术论坛尽管/*看起来像一个注释。
4 F, x6 O/ U, n* g$ \+ N7 ~我爱电脑技术论坛
" U8 E; P# h3 L$ J; {' W9 f打造最好的电脑自学交流论坛1.4 例外我爱电脑技术社区--打造最好的电脑技术自学交流平台9 b1 Q* }9 e0 w: T. K

% D! {; h* X3 b  v5 B组合赋值运算符如+=实际上是两个记号。因此,
* Z: F* k% ?9 R1 m# g; G5 a- v我爱电脑技术社区--打造最好的电脑技术自学交流平台www.520diannao.com8 ^. T4 |2 n8 M  b9 ~# j/ M  p) v) X
a + /* strange */ = 1我爱电脑技术社区--打造最好的电脑技术自学交流平台$ ^. L6 z9 f% i9 t
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站: @# [, h. S$ C7 t2 r# \  p; l

8 Q9 A: F& p& X% T9 n- |* F, f+ Zwww.520diannao.com$ ]' o- X1 w. D, g& C
a += 1) x4 _3 [7 P( j" m- z2 u0 L. W

. I5 @6 u; u0 o. }; A是一个意思。看起来像一个单独的记号而实际上是多个记号的只有这一个特例。特别地,电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站& m9 g8 r% d. v7 M, a4 k) E' \2 `

" J! F; [" ^2 H7 h* U我爱电脑技术社区--打造最好的电脑技术自学交流平台
0 L; z$ {& V$ Z" _p - > a
: K+ v& y  o# f. l6 D. ?电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
1 v0 `. n! s3 z1 o+ w- N, P& d电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站是不合法的。它和www.520diannao.com: g/ C+ y- X5 H% l

3 g) q3 R$ s& ]# J* V6 M# s0 M电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站p -> a打造最好的电脑自学交流论坛$ {/ s. X  b5 y5 l4 J
打造最好的电脑自学交流论坛6 M* c& |3 ]  z8 A
不是同义词。
; A$ C* X* K7 O8 Xwww.520diannao.com
8 [  X/ p1 N' w  w* [3 Q- p7 j: h打造最好的电脑自学交流论坛另一方面,有些老式编译器还是将=+视为一个单独的记号并且和+=是同义词。电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站" J1 e+ f' q+ t, `, K* c4 \9 }
我爱电脑技术社区--打造最好的电脑技术自学交流平台- K3 N- a7 U& k" N
1.5 字符串和字符
( s& y# d; X# G. C  s3 M4 O电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站我爱电脑技术论坛9 y: K( `# s. |
    单引号和双引号在C中的意义完全不同,在一些混乱的上下文中它们会导致奇怪的结果而不是错误消息。包围在单引号中的一个字符只是书写整数的另一种方法。这个整数是给定的字符在实现的对照序列中的一个对应的值。因此,在一个ASCII实现中,'a'和0141或97表示完全相同的东西。而一个包围在双引号中的字符串,只是书写一个有双引号之间的字符和一个附加的二进制值为零的字符所初始化的一个无名数组的指针的一种简短方法。下面的两个程序片断是等价的:
, X% t" P0 ~! l电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
' B9 M3 D1 {: Y$ {; O/ Y- B我爱电脑技术社区--打造最好的电脑技术自学交流平台printf("Hello world\n");
7 r$ ^+ S& ]; s/ ]2 d# e电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站www.520diannao.com  b2 X0 T( V4 m, F
char hello[] = { 'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\n', ; f+ F% C6 f( Z" T% U  ^
0 };
, t: ?' M; O$ D$ r' n7 ?1 N  t4 x" ?打造最好的电脑自学交流论坛printf(hello);电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站: s/ J* N8 u5 w' ~9 a3 y
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站) R7 V. k' |- l: Q- b2 m2 J
        使用一个指针来代替一个整数通常会得到一个警告消息(反之亦然),使用双引号来代替单引号也会得到一个警告消息(反之亦然)。但对于不检查参数类型的编译器却除外。因此,用电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站0 r, s0 J4 ~" E& [- q+ {9 {

0 a, a2 c& `3 @/ c  Vprintf('\n');我爱电脑技术论坛) w- G* w( j/ K

" L$ _3 J( A; k) h/ D来代替打造最好的电脑自学交流论坛: W, F' @) h- O: q+ k# @( m9 V+ C
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站2 n8 u/ C5 R$ D
printf("\n");我爱电脑技术论坛% y3 Q" B5 w; j# @
我爱电脑技术社区--打造最好的电脑技术自学交流平台) K+ y) @( ^% B! U; Z5 W2 T, H
通常会在运行时得到奇怪的结果。我爱电脑技术论坛' M/ Y2 s. W- A  W# y+ R3 b$ K
我爱电脑技术社区--打造最好的电脑技术自学交流平台& l$ L; W1 g: A) a
    由于一个整数通常足够大,以至于能够放下多个字符,一些C编译器允许在一个字符常量中存放多个字符。这意味着用'yes'代替"yes"将不会被发现。后者意味着“分别包含y、e、s和一个空字符的四个连续存贮器区域中的第一个的地址”,而前者意味着“在一些实现定义的样式中表示由字符y、e、s联合构成的一个整数”。这两者之间的任何一致性都纯属巧合。
. e& D* c& k/ T0 T& i7 x2 H, vwww.520diannao.com' h( L% m8 f8 X# L) t
2 句法缺陷打造最好的电脑自学交流论坛: k, H. q5 `7 b$ C# y/ |4 g

" \& Y* P5 ?8 P( g  h* S我爱电脑技术论坛    要理解C语言程序,仅了解构成它的记号是不够的。还要理解这些记号是如何构成声明、表达式、语句和程序的。尽管这些构成通常都是定义良好的,但这些定义有时候是有悖于直觉的或混乱的。我爱电脑技术社区--打造最好的电脑技术自学交流平台# n9 W  A( y" ~1 ?

1 \6 M7 G' j( b* p& Uwww.520diannao.com在这一节中,我们将着眼于一些不明显句法构造。
: J( k- E. A. x, _我爱电脑技术社区--打造最好的电脑技术自学交流平台
6 P! q/ y" M. u% h, s" o3 h2.1 理解声明我爱电脑技术论坛$ l9 T6 s0 l. X5 k

2 @% D( s6 r/ H( n    在一个小型的微处理器上单机运行的C程序,当这台机器的开关打开的时候,硬件会调用地址为0处的子程序。为了模仿电源打开的情形,我们要设计一条C语句来显式地调用这个子程序。经过一些思考,我们写出了下面的语句:www.520diannao.com* I, y# Z) q$ P. U; R9 T+ j& F
我爱电脑技术社区--打造最好的电脑技术自学交流平台( l: _1 B9 H3 V; W7 h
(*(void(*)())0)();我爱电脑技术论坛5 @0 @3 x3 U; G9 N( _

% E5 N% ~, W, g7 Y* T& j1 k# l我爱电脑技术论坛        这样的表达式会令C程序员心惊胆战。但是,并不需要这样,因为他们可以在一个简单的规则的帮助下很容易地构造它:以你使用的方式声明它。每个C变量声明都具有两个部分:一个类型和一组具有特定格式的期望用来对该类型求值的表达式。最简单的表达式就是一个变量:
8 g! y1 l) {0 K; X4 o& y, r( n) `我爱电脑技术社区--打造最好的电脑技术自学交流平台电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站+ {$ ^- H6 u/ Z$ w) r' x5 `4 |
float f, g;www.520diannao.com3 Z$ s$ K, Q) n. I) ?7 [+ k& d+ h

8 r* v/ W9 U8 `! k& u+ v打造最好的电脑自学交流论坛        说明表达式f和g——在求值的时候——具有类型float。由于待求值的是表达式,因此可以
3 v) S! ^) F2 P+ c我爱电脑技术论坛自由地使用圆括号:我爱电脑技术社区--打造最好的电脑技术自学交流平台( W* Y* U5 I, P% a6 O( p

  E, g% K; C& p: Z% A电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站float ((f));我爱电脑技术社区--打造最好的电脑技术自学交流平台: @, S0 F4 E9 P% X% e9 r

1 k; Z* q3 e1 h# t我爱电脑技术论坛        这表示((f))求值为float并且因此,通过推断,f也是一个float。同样的逻辑用在函数和指针类型。例如:
7 A, n* ?5 G' `电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站打造最好的电脑自学交流论坛" c' f8 U- ^% f2 Q
float ff();打造最好的电脑自学交流论坛! v9 b2 z% x, V. H' D6 a. m; ~. A7 W" n
我爱电脑技术论坛: {/ ?) A0 q/ g. s3 [$ A; X
表示表达式ff()是一个float,因此ff是一个返回一个float的函数。类似地,
' u/ C& V3 y: {0 v3 |% {  v电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
3 _3 X8 @$ ^$ q4 {  _( ^2 l7 R打造最好的电脑自学交流论坛float *pf;
2 s  |0 ~6 X# y5 f; J, V我爱电脑技术社区--打造最好的电脑技术自学交流平台
" T0 k$ A% d# x  C+ }! J  q8 J" s$ }电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站        表示*pf是一个float并且因此pf是一个指向一个float的指针。这些形式的组合声明对表达式是一样的。因此,我爱电脑技术论坛& {! r* _' x$ j

/ x4 ~+ K, f5 j( Y$ ~电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站float *g(), (*h)();. g% V# o7 m8 L
3 w' s7 l1 T3 z. ~  n0 \
        表示*g()和(*h)()都是float表达式。由于()比*绑定得更紧密,*g()和*(g())表示同样的东西:g是一个返回指float指针的函数,而h是一个指向返回float的函数的指针。当我们知道如何声明一个给定类型的变量以后,就能够很容易地写出一个类型的模型(cast):只要删除变量名和分号并将所有的东西包围在一对圆括号中即可。因此,由于我爱电脑技术社区--打造最好的电脑技术自学交流平台: F: v8 x) Q( \+ N
www.520diannao.com8 m  e# M* k% A( o! S
float *g();
: D6 b6 Y' v& n: @) ywww.520diannao.com我爱电脑技术社区--打造最好的电脑技术自学交流平台; l! l/ b7 y# ?4 u+ u* U
        声明g是一个返回float指针的函数,所以(float *())就是它的模型。有了这些知识的武装,我们现在可以准备解决(*(void(*)())0)()了。 我们可以将它分为两个部分进行分析。首先,假设我们有一个变量fp,它包含了一个函数指针,并且我们希望调用fp所指向的函数。可以这样写:打造最好的电脑自学交流论坛& Z0 z' @) T9 J" s7 g

) F1 O" Y7 R3 ]www.520diannao.com(*fp)();打造最好的电脑自学交流论坛0 u  M. D$ x, S. g, y# B& P

1 z7 p) G; c* H" y我爱电脑技术社区--打造最好的电脑技术自学交流平台        如果fp是一个指向函数的指针,则*fp就是函数本身,因此(*fp)()是调用它的一种方法。(*fp)中的括号是必须的,否则这个表达式将会被分析为*(fp())。我们现在要找一个适当的表达式来替换fp。这个问题就是我们的第二步分析。如果C可以读入并理解类型,我们可以写:我爱电脑技术论坛0 g- a+ ^; a' j# D5 R

( q; q8 ^6 X  @) y7 f) m电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站(*0)();
. f( d1 y( j- |我爱电脑技术社区--打造最好的电脑技术自学交流平台
$ @1 t0 w$ v: X" N4 Y- M打造最好的电脑自学交流论坛         但这样并不行,因为*运算符要求必须有一个指针作为他的操作数。另外,这个操作数必须是一个指向函数的指针,以保证*的结果可以被调用。因此,我们需要将0转换为一个可以描述“指向一个返回void的函数的指针”的类型。如果fp是一个指向返回void的函数的指针,则(*fp)()是一个void值,并且它的声明将会是这样的:www.520diannao.com8 h. n' E- c5 q8 ?2 S# c- E4 h6 g

" j7 O9 G" h& a! x" G. dvoid (*fp)();
. N' v3 P& Q3 s$ F电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站www.520diannao.com$ Q- g; p: I( Q
因此,我们需要写:
% L' m; K- N9 O1 k电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
* U- E- c5 J' ]  V6 f打造最好的电脑自学交流论坛void (*fp)();$ M5 Q3 S- C2 V/ a. I! l  K
(*fp)();
" N5 _: C" |% P8 [/ S我爱电脑技术论坛
5 `0 [( w' G; ^( R  W. {8 ~打造最好的电脑自学交流论坛        来声明一个哑变量。一旦我们知道了如何声明该变量,我们也就知道了如何将一个常数转换为该类型:只要从变量的声明中去掉名字即可。因此,我们像下面这样将0转换为一个“指向返回void的函数的指针”:
( v  F9 i! H) K- ?7 a1 Hwww.520diannao.com我爱电脑技术社区--打造最好的电脑技术自学交流平台, [- f  R- F( T6 i5 R9 u
(void(*)())0www.520diannao.com% n( y& U, x' |) X
www.520diannao.com2 U- P0 o9 q# u8 h# Y( m' y( |
接下来,我们用(void(*)())0来替换fp:
7 ^% y0 P3 {5 p0 s- j. E; h
# x1 K: C. z" ^/ j/ {4 D6 s, A/ M电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站(*(void(*)())0)();
4 M/ |9 Z( s4 i0 i+ p我爱电脑技术论坛3 r- W; U' ?" H
        结尾处的分号用于将这个表达式转换为一个语句。在这里,我们就解决了这个问题时没有使用typedef声明。通过使用它,我们可以更清晰地解决这个问题:
7 l$ A! m" i5 S. }" @8 _打造最好的电脑自学交流论坛
/ @; d# v) b4 m6 s6 R0 c5 Mtypedef void (*funcptr)();
; A" Z. \& q7 O, y' K我爱电脑技术社区--打造最好的电脑技术自学交流平台(*(funcptr)0)();www.520diannao.com/ Q- [. Z! V: h
9 G# X3 p: [+ T. y2 P
2.2 运算符并不总是具有你所想象的优先级
; K" b6 X/ y& k) r5 |0 c  vwww.520diannao.comwww.520diannao.com3 D% T$ K$ G5 E+ X6 t2 C1 H) k
    假设有一个声明了的常量FLAG是一个整数,其二进制表示中的某一位被置位(换句话说,它是2的某次幂),并且你希望测试一个整型变量flags该位是否被置位。通常的写法是:我爱电脑技术论坛7 M# V7 [  U$ r2 A& a/ \

: ]4 I' \3 o  O我爱电脑技术论坛if(flags & FLAG) ...
/ C4 M3 o5 K; Y$ C  i电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站' P/ F* d. [- _: m; b% F( d
        其意义对于很多C程序员都是很明确的:if语句测试括号中的表达式求值的结果是否为0。出于清晰的目的我们可以将它写得更明确:
: R% G/ _3 Z) {我爱电脑技术社区--打造最好的电脑技术自学交流平台电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站7 y9 ]5 p9 U% o# f- _* l
if(flags & FLAG != 0) ...
# C, P2 O: P9 ]电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站打造最好的电脑自学交流论坛' M) ~8 e7 U, c. |6 j" b
这个语句现在更容易理解了。但它仍然是错的,因为!=比&绑定得更紧密,因此它被分析为:
6 x) K( t  S% u$ x- a# l1 o% ~我爱电脑技术社区--打造最好的电脑技术自学交流平台
4 B+ p) R8 P, Y* ]! [' `. v5 R+ @if(flags & (FLAG != 0)) ...打造最好的电脑自学交流论坛- H6 B- E, d* J* m& F( T9 z

1 B3 F7 u2 A+ x: J; l* p2 R  F7 lwww.520diannao.com        这(偶尔)是可以的,如FLAG是1或0(!)的时候,但对于其他2的幂是不行的脚注[2]。假设你有两个整型变量,h和l,它们的值在0和15(含0和15)之间,并且你希望将r设置为8位值,其低位为l,高位为h。一种自然的写法是:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站6 Z3 q+ \1 _* G: p" q& [! P

" {$ Q; K& v8 h电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站r = h << 4 + 1;我爱电脑技术社区--打造最好的电脑技术自学交流平台! d* [2 V( E; o- `5 J  }

" l" r: m! u- K3 \! x6 {打造最好的电脑自学交流论坛不幸的是,这是错误的。加法比移位绑定得更紧密,因此这个例子等价于:, Q" O. ]$ ?# K2 g- Y

* g4 o+ S. d; e0 M2 \" ^" H我爱电脑技术论坛r = h << (4 + l);电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站1 y) W9 X! t+ }8 |; v

) R  C, e% f: K我爱电脑技术论坛正确的方法有两种:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站$ Q3 _/ D. A- u; w/ Y* F/ `
www.520diannao.com3 o5 i. J5 {' @( b# B+ R
r = (h << 4) + l;
6 l! W- h* A: c: H. \+ f4 m& Q! {我爱电脑技术社区--打造最好的电脑技术自学交流平台
1 j+ G2 j/ |* B3 Y" ]9 b( Cr = h << 4 | l;
8 `* [  K2 K# M1 Q$ |, B+ g+ b- I" X我爱电脑技术论坛' e9 r& _5 R, e* [  o5 _# C' m
        避免这种问题的一个方法是将所有的东西都用括号括起来,但表达式中的括号过度就会难以理解,因此最好还是是记住C中的优先级。不幸的是,这有15个,太困难了。然而,通过将它们分组可以变得容易。绑定得最紧密的运算符并不是真正的运算符:下标、函数调用和结构选择。这些都与左边相关联。接下来是一元运算符。它们具有真正的运算符中的最高优先级。由于函数调用比一元运算符绑定得更紧密,你必须写(*p)()来调用p指向的函数;*p()表示p是一个返回一个指针的函数。转换是一元运算符,并且和其他一元运算符具有相同的优先级。一元运算符是右结合的,因此*p++表示*(p++),而不是(*p)++。在接下来是真正的二元运算符。其中数学运算符具有最高的优先级,然后是移位运算符、关系运算符、逻辑运算符、赋值运算符,最后是条件运算符。需要记住的两个重要的东西是:所有的逻辑运算符具有比所有关系运算符都低的优先级。移位运算符比关系运算符绑定得更紧密,但又不如数学运算符。在这些运算符类别中,有一些奇怪的地方。乘法、除法和求余具有相同的优先级,加法和减法具有相同的优先级,以及移位运算符具有相同的优先级。还有就是六个关系运算符并不具有相同的优先级:==和!=的优先级比其他关系运算符要低。这就允许我们判断a和b是否具有与c和d相同的顺序,例如:
, ]" }5 w* u8 e5 f9 {8 r我爱电脑技术论坛- Q$ ^6 ?7 g) v. ~
a < b == c < d
2 c" ^2 S' E9 t5 q4 b3 G电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
8 D8 n% |2 K5 ]8 _" H$ Owww.520diannao.com        在逻辑运算符中,没有任何两个具有相同的优先级。按位运算符比所有顺序运算符绑定得都紧密,每种与运算符都比相应的或运算符绑定得更紧密,并且按位异或(^)运算符介于按位与和按位或之间。三元运算符的优先级比我们提到过的所有运算符的优先级都低。这可以保证选择表达式中包含的关系运算符的逻辑组合特性,如:www.520diannao.com# H: i: ^) b9 N* ]
我爱电脑技术论坛+ h3 _+ \7 U' c! ^& p
z = a < b && b < c ? d : e; S: Y  Q+ o3 |% y

2 r$ X  `  [/ j2 q; {, B1 a打造最好的电脑自学交流论坛        这个例子还说明了赋值运算符具有比条件运算符更低的优先级是有意义的。另外,所有的复合赋值运算符具有相同的优先级并且是自右至左结合的,因此7 @: I- m: X5 e, }+ A- }

9 M$ C0 m- T6 S  P5 y+ T电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站a = b = c电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站# [/ L4 i8 ^" b; W5 R

2 W# ~% F. Y5 }. R我爱电脑技术社区--打造最好的电脑技术自学交流平台我爱电脑技术社区--打造最好的电脑技术自学交流平台6 T1 G1 G# N. E2 S$ x

" e) k: ?4 N4 y7 }% I打造最好的电脑自学交流论坛b = c; a = b;www.520diannao.com( {% b. g+ `3 a% N2 ?; l
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站, y% v9 W) z# R; n* m: y% h
        是等价的。具有最低优先级的是逗号运算符。这很容易理解,因为逗号通常在需要表达式而不是语句的时候用来替代分号。赋值是另一种运算符,通常具有混合的优先级。例如,考虑下面这个用于复制文件的循环:
( o0 K( V2 a4 F( i; E电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站9 }; l# e% P: G
while(c = getc(in) != EOF)电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站4 u; u' D7 V6 r7 k1 v5 R6 H! f+ @6 W3 Y
putc(c, out);
# p  N! K$ M9 d/ l6 G我爱电脑技术社区--打造最好的电脑技术自学交流平台我爱电脑技术社区--打造最好的电脑技术自学交流平台2 N9 Y, L& c) _; Q* K* h& D
        这个while循环中的表达式看起来像是c被赋以getc(in)的值,接下来判断是否等于EOF以结束循环。不幸的是,赋值的优先级比任何比较操作都低,因此c的值将会是getc(in)和EOF比较的结果,并且会被抛弃。因此,“复制”得到的文件将是一个由值为1的字节流组成的文件。
& E# i; B- t( {( r0 e) c7 t我爱电脑技术论坛我爱电脑技术论坛. V6 U0 e' f* u  A) x' ~3 \
上面这个例子正确的写法并不难:
4 y% m) e* ~5 k* B打造最好的电脑自学交流论坛
; G" m, h  J' M* o* \& f( a. E打造最好的电脑自学交流论坛while((c = getc(in)) != EOF)
3 [' V; W( n% z/ t: }8 _打造最好的电脑自学交流论坛putc(c, out);打造最好的电脑自学交流论坛2 {+ M: k4 E3 K
我爱电脑技术社区--打造最好的电脑技术自学交流平台6 x2 @0 t% G& \6 Z# h. ?6 Z
        然而,这种错误在很多复杂的表达式中却很难被发现。例如,随UNIX系统一同发布的lint程序通常带有下面的错误行:打造最好的电脑自学交流论坛* r# K% \+ K' e  X  y
我爱电脑技术论坛7 N5 S8 Y0 }1 @6 J
if (((t = BTYPE(pt1->aty) == STRTY) || t == UNIONTY) {
, Q, _, U* B9 Q我爱电脑技术论坛
# {9 z* K2 {5 Y$ s" ~* c( b电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站        这条语句希望给t赋一个值,然后看t是否与STRTY或UNIONTY相等。而实际的效果却大不相同脚注[3]。C中的逻辑运算符的优先级具有历史原因。B——C的前辈——具有和C中的&和|运算符对应的逻辑运算符。尽管它们的定义是按位的 ,但编译器在条件判断上下文中将它们视为和&&和||一样。当在C中将它们分开后,优先级的改变是很危险的脚注[4]。
1 C; U; {. K; O  p电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
9 E' I/ V) Q' {/ lwww.520diannao.com2.3 看看这些分号!我爱电脑技术论坛" ?. e; F7 S* U& S) n  ~

4 w2 D, h$ w, j8 Pwww.520diannao.com    C中的一个多余的分号通常会带来一点点不同:或者是一个空语句,无任何效果;或者编译器可能提出一个诊断消息,可以方便除去掉它。一个重要的区别是在必须跟有一个语句的if和while语句中。考虑下面的例子:
  A' f2 P  N1 \9 Gwww.520diannao.com
: O" q7 J/ W! Xwww.520diannao.comif(x > big);
# A9 n6 X1 M) Q( R# O9 \2 R) y我爱电脑技术论坛big = x;
  U1 m5 ]- |7 T* g0 X, f9 y+ b% N& c打造最好的电脑自学交流论坛
1 n6 X& O- O9 D4 b& f, S这不会发生编译错误,但这段程序的意义与:我爱电脑技术论坛1 ?, M1 I$ Y( p! X
打造最好的电脑自学交流论坛0 |& ^9 a! ~' u: V) t
if(x > big)
7 V6 u5 X+ S% u8 s1 ?电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站big = x;电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站- N/ v! E) L: V8 y
我爱电脑技术社区--打造最好的电脑技术自学交流平台9 V' S' J  K- g
就大不相同了。第一个程序段等价于:
  t# w! @( x2 C0 ~' F电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站www.520diannao.com# Y' R. X, l( f: _
if(x > big) { }www.520diannao.com7 p! I2 I& f" `7 z: E
big = x;
( |2 ^) S( r2 H) [1 @6 d我爱电脑技术论坛1 G/ Y+ [0 `2 l
也就是等价于:
" l* l5 O. ]& y& A- f& f- G, x打造最好的电脑自学交流论坛
+ K6 U! v/ g$ o! w' V电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站big = x;我爱电脑技术论坛1 o: F" o2 S; H3 f
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站, P: u- ?* i' M# y$ t# A
       (除非x、i或big是带有副作用的宏)。另一个因分号引起巨大不同的地方是函数定义前面的结构声明的末尾[译注:这句话不太好听,看例子就明白了]。考虑下面的程序片段:
6 m! y1 G* d" w# f" @我爱电脑技术社区--打造最好的电脑技术自学交流平台
, k; n6 r: k0 W5 D# x8 O6 M4 Z打造最好的电脑自学交流论坛struct foo {www.520diannao.com( Q& |5 V$ _& ~
int x;
1 Y3 H+ i& u1 U& K0 \" v我爱电脑技术论坛}
; D: [+ X# L+ |5 i  _www.520diannao.com
! y' r9 O' j$ r4 B8 h我爱电脑技术论坛f() {
: `2 f& V4 S6 u$ a) Bwww.520diannao.com...我爱电脑技术论坛; F' o2 w% v# |; }
}
4 @0 R* @; Y* v1 {: Q$ J$ R# T* y我爱电脑技术论坛电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站+ ~' |( r* ]" @" w8 t8 J0 P/ e' N5 H
        在紧挨着f的第一个}后面丢失了一个分号。它的效果是声明了一个函数f,返回值类型是struct foo,这个结构成了函数声明的一部分。如果这里出现了分号,则f将被定义为具有默认的整型返回值脚注[5]。
5 Z9 n* }! a4 q/ X& ]电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
8 N! |7 l# |  m( W' w# r; s电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站2.4 switch语句  j- D$ H* v8 F( I
打造最好的电脑自学交流论坛# k$ X1 J) c/ a3 y: O
通常C中的switch语句中的case段可以进入下一个。例如,考虑下面的C和Pascal程序片断:
6 |* f: f- s4 m7 _  z' C
  H! ?6 l" ^0 S0 a$ }& O% M; jswitch(color) {
/ ?$ W: G9 V% C# V/ Q8 i; t. Gcase 1: printf ("red");我爱电脑技术论坛! Z; P! i0 A# n+ o# [# s
break;
9 W; P% o9 Z; i: n3 I电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站case 2: printf ("yellow");我爱电脑技术论坛7 ]7 D5 s/ i& s  ], k3 S) k' N
break;
! U- p" w7 r" y* F3 ^2 Kcase 3: printf ("blue");
5 d% Q; A3 K) R2 |break;
$ L) Z2 ~3 t5 l- f( q/ T我爱电脑技术论坛}, c2 N$ e3 q8 f$ S/ G$ R/ a# F
www.520diannao.com9 [) g8 P" v) C' z/ G8 q) P* G
case color of我爱电脑技术社区--打造最好的电脑技术自学交流平台% V. C7 K( c# `$ p6 u. _
1: write ('red');我爱电脑技术社区--打造最好的电脑技术自学交流平台$ }) v. r+ X# [1 e0 ?2 K
2: write ('yellow');
1 g! t7 x! s- D3 l. l我爱电脑技术社区--打造最好的电脑技术自学交流平台3: write ('blue');
6 r( j" g1 ^* c1 C' xend/ Z3 ~, a& q- A( T+ H) y
www.520diannao.com, Z: B3 d( D( k0 R8 `
         这两个程序片断都作相同的事情:根据变量color的值是1、2还是3打印red、yellow或blue(没有新行符)。这两个程序片断非常相似,只有一点不同:Pascal程序中没有C中相应的break语句。C中的case标签是真正的标签:控制流程可以无限制地进入到一个case标签中。看看另一种形式,假设C程序段看起来更像Pascal:& H5 N% l* t) ]) y' q. c

" f$ i( v8 n& J% R我爱电脑技术论坛switch(color) {我爱电脑技术论坛4 k: {' f% V. R$ J! ?- M! E
case 1: printf ("red");" o  [! {9 g6 G, F& s/ R% d
case 2: printf ("yellow");
7 e4 ~# U$ n5 ~  _( a2 X5 e我爱电脑技术社区--打造最好的电脑技术自学交流平台case 3: printf ("blue");打造最好的电脑自学交流论坛  l' {& P9 ^' }( T6 ]; Z
}我爱电脑技术论坛6 t+ H2 N: ?/ N: i! v' F! n; g

; o. n, a  p0 t6 b9 mwww.520diannao.com        并且假设color的值是2。则该程序将打印yellowblue,因为控制自然地转入到下一个printf()的调用。这既是C语言switch语句的优点又是它的弱点。说它是弱点,是因为很容易忘记一个break语句,从而导致程序出现隐晦的异常行为。说它是优点,是因为通过故意去掉break语句,可以很容易实现其他方法难以实现的控制结构。尤其是在一个大型的switch语句中,我们经常发现对一个case的处理可以简化其他一些特殊的处理。例如,设想有一个程序是一台假想的机器的翻译器。这样的一个程序可能包含一个switch语句来处理各种操作码。在这样一台机器上,通常减法在对其第二个运算数进行变号后就变成和加法一样了。因此,最好可以写出这样的语句:
9 o( O# N: A; a1 S3 g( Y; B. A- y电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站我爱电脑技术社区--打造最好的电脑技术自学交流平台3 d: d8 A0 q/ D" }6 p& Y8 _- I
case SUBTRACT:0 D% C- w; k. I" w
opnd2 = -opnd2;
* d! W0 J- q( r/* no break; */我爱电脑技术社区--打造最好的电脑技术自学交流平台2 I8 Z3 {/ m* O" T0 G
case ADD:我爱电脑技术论坛" ^& e+ K" e. P8 c6 k, m9 @  s
...我爱电脑技术论坛5 i3 T; ^# d' n! S0 h

4 @( y" O( `- D! |/ o0 `6 z" w7 P, Y电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站        另外一个例子,考虑编译器通过跳过空白字符来查找一个记号。这里,我们将空格、制表符和新行符视为是相同的,除了新行符还要引起行计数器的增长外:
; i8 o! v! O" e0 Q4 d. m  R1 kwww.520diannao.com打造最好的电脑自学交流论坛$ w' C4 {/ I# U, @  S
case '\n':
+ y6 C; `, {2 }. T9 slinecount++;
  i+ u2 p2 r: w; o. n9 q9 l我爱电脑技术社区--打造最好的电脑技术自学交流平台/* no break */打造最好的电脑自学交流论坛3 D& m2 c# h0 G8 G
case '\t':电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站. g3 s6 J: W% N  P3 Z
case ' ':
5 v" h- l" Q! t1 F+ V: l电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站...! A3 O. q' b( |
www.520diannao.com1 _# K+ _: o- M. ?2 D
2.5 函数调用
) M. }* U3 |5 N& ?/ Q4 Hwww.520diannao.com
+ w; S* T8 E3 M: s打造最好的电脑自学交流论坛    和其他程序设计语言不同,C要求一个函数调用必须有一个参数列表,但可以没有参数。因此,如果f是一个函数,
* A8 h5 A1 G# ~5 e, J我爱电脑技术社区--打造最好的电脑技术自学交流平台0 J6 J" ?  [. N* Y1 U# q
f();
) u! P* Z/ u6 |; l9 e
$ j3 c& [$ }$ z6 Q4 U% U' o! D就是对该函数进行调用的语句,而打造最好的电脑自学交流论坛- m9 d5 R$ |/ B: ~2 ?8 Z

/ T5 f, o5 T1 c3 Z8 ?+ A: ]f;
& w5 z" p$ `& J- xwww.520diannao.com
% ?7 v6 a) z; ^- P/ L+ r我爱电脑技术论坛什么也不做。它会作为函数地址被求值,但不会调用它脚注[6]。
0 Z1 M1 ~6 s8 k1 w. r' H电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站我爱电脑技术论坛+ z0 R! L9 Z; B# H/ O0 S- c
2.6 悬挂else问题
. _, y: W% d* ^9 W/ M1 J$ qwww.520diannao.com打造最好的电脑自学交流论坛) L, g. F; Y0 g
    在讨论任何语法缺陷时我们都不会忘记提到这个问题。尽管这一问题不是C语言所独有的,但它仍然伤害着那些有着多年经验的C程序员。考虑下面的程序片断:我爱电脑技术社区--打造最好的电脑技术自学交流平台) k& D1 M  G: i

6 c, S9 O$ `3 k/ \2 l% m5 q1 s打造最好的电脑自学交流论坛if(x == 0)电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站# E2 |4 A9 c$ V% g7 {, \+ O  {; c
if(y == 0) error();
' B" Q! S6 E9 W* ~- t6 @# C% D电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站else {
' Y) C2 u; _/ z( V, O& B电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站z = x + y;www.520diannao.com) ]/ ]: l# p/ n  E
f(&z);8 O: S0 J, D( B6 P
}
* y/ v% F6 S( W2 Q: w: twww.520diannao.com
2 ~  {% u. O4 V1 h& Y  {$ V3 ]打造最好的电脑自学交流论坛        写这段程序的程序员的目的明显是将情况分为两种:x = 0和x != 0。在第一种情况中,程序段什么都不做,除非y = 0时调用error()。第二种情况中,程序设置z = x + y并以z的地址作为参数调用f()。打造最好的电脑自学交流论坛1 `' ~7 J" s0 G' T6 u* D
然而, 这段程序的实际效果却大为不同。其原因是一个else总是与其最近的if相关联。如果我们希望这段程序能够按照实际的情况运行,应该这样写:
( Q. y* ]7 t# k电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站/ ]- U) M8 L5 [6 l! G& E% v
if(x == 0) {
' T( u, w1 @7 K/ N4 n" _( g9 D我爱电脑技术论坛if(y == 0)电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站- f0 r: a' F- K8 Q: ?3 b
error();我爱电脑技术论坛; |  A. z1 B" _3 |2 g; |! N/ ~
else {我爱电脑技术论坛3 p) m3 S9 D( `; z
z = x + y;打造最好的电脑自学交流论坛6 ?$ q$ c! m5 D/ K" x
f(&z);
1 C5 L7 [4 k6 x, g% Swww.520diannao.com}我爱电脑技术社区--打造最好的电脑技术自学交流平台+ g8 e8 O5 g( }. ]7 B
}
# ^7 ?+ h  q* ^6 {5 x打造最好的电脑自学交流论坛我爱电脑技术论坛0 c: J2 b' ^6 e6 f5 |0 x
换句话说,当x != 0发生时什么也不做。如果要达到第一个例子的效果,应该写:: f) w) ]* X$ K% p; n+ r

) a( q8 `9 v3 _www.520diannao.comif(x == 0) {
2 |# F7 M4 ?* D, H  a我爱电脑技术论坛if(y ==0)
3 G( B0 }8 l1 `( ?3 Vwww.520diannao.comerror();
: L8 Y) ^0 p6 U: C: }1 _www.520diannao.com}
# b$ ~+ E1 O' b( l电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站else {www.520diannao.com8 A$ R2 T% \4 s* V1 }; `
z = z + y;
  E3 `. q7 m& ~0 h( \1 Y% l我爱电脑技术论坛f(&z);打造最好的电脑自学交流论坛" A6 r' ]2 _! M7 V, R
}www.520diannao.com1 W2 K( I" {# O. ^9 q6 L

( \( ~) C+ J  j' s8 H) r6 A$ R电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站3 链接
6 G: U* \  d( a; u打造最好的电脑自学交流论坛我爱电脑技术社区--打造最好的电脑技术自学交流平台$ f; g; A4 |: A
    一个C程序可能有很多部分组成,它们被分别编译,并由一个通常称为链接器、链接编辑器或加载器的程序绑定到一起。由于编译器一次通常只能看到一个文件,因此它无法检测到需要程序的多个源文件的内容才能发现的错误。在这一节中,我们将看到一些这种类型的错误。有一些C实现,但不是所有的,带有一个称为lint的程序来捕获这些错误。如果具有一个这样的程序,那么无论怎样地强调它的重要性都不过分。
  V) H) q/ c! |; T' E- q, c% o
7 V2 h" a# j  ~. U3.1 你必须自己检查外部类型% \& @+ m  t1 o
我爱电脑技术论坛6 d6 ^0 S4 M- R# W  {
假设你有一个C程序,被划分为两个文件。其中一个包含如下声明:
+ B) w) Y' i6 x. O  U我爱电脑技术社区--打造最好的电脑技术自学交流平台
* ]: @, K# \/ {4 d我爱电脑技术社区--打造最好的电脑技术自学交流平台int n;www.520diannao.com6 e. u  Z# L2 U; L* M
我爱电脑技术论坛6 A3 J$ F1 T* S4 m3 M# z- h. R& ~
而令一个包含如下声明:
. j# t) ^- w* \8 }- M我爱电脑技术社区--打造最好的电脑技术自学交流平台我爱电脑技术论坛/ O' s6 V' N2 ^- G5 a
long n;我爱电脑技术论坛% M8 W0 N+ I( a/ O
打造最好的电脑自学交流论坛/ k: [1 b3 S9 _! v% ]' a! ^
        这不是一个有效的C程序,因为一些外部名称在两个文件中被声明为不同的类型。然而,很多实现检测不到这个错误,因为编译器在编译其中一个文件时并不知道另一个文件的内容。因此,检查类型的工作只能由链接器(或一些工具程序如lint)来完成;如果操作系统的链接器不能识别数据类型,C编译器也没法过多地强制它。那么,这个程序运行时实际会发生什么?这有很多可能性:实现足够聪明,能够检测到类型冲突。则我们会得到一个诊断消息,说明n在两个文件中具有不同的类型。你所使用的实现将int和long视为相同的类型。典型的情况是机器可以自然地进行32位运算。在这种情况下你的程序或许能够工作,好象你两次都将变量声明为long(或int)。但这种程序的工作纯属偶然。n的两个实例需要不同的存储,它们以某种方式共享存储区,即对其中一个的赋值对另一个也有效。这可能发生,例如,编译器可以将int安排在long的低位。不论这是基于系统的还是基于机器的,这种程序的运行同样是偶然。n的两个实例以另一种方式共享存储区,即对其中一个赋值的效果是对另一个赋以不同的值。在这种情况下,程序可能失败。这种情况发生的里一个例子出奇地频繁。程序的某一个文件包含下面的声明:
+ [3 f/ u, g; Z0 t  ?7 d我爱电脑技术社区--打造最好的电脑技术自学交流平台电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站# W2 h; p8 P" d. u5 m/ }  K
char filename[] = "etc/passwd";www.520diannao.com1 W& D) m) ]( _% _; X

2 j5 W( @; ~! y' i电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站而另一个文件包含这样的声明:我爱电脑技术社区--打造最好的电脑技术自学交流平台' j4 A3 ]0 Z5 d/ @0 O8 q0 l

# x7 V; e. j$ _  S8 x, A9 Z$ D电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站char *filename;
" b9 p$ ^$ q; r电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
0 _, v/ B% S: A0 X; Z% ^我爱电脑技术论坛        尽管在某些环境中数组和指针的行为非常相似,但它们是不同的。在第一个声明中,filename是一个字符数组的名字。尽管使用数组的名字可以产生数组第一个元素的指针,但这个指针只有在需要的时候才产生并且不会持续。在第二个声明中,filename是一个指针的名字。这个指针可以指向程序员让它指向的任何地方。如果程序员没有给它赋一个值,它将具有一个默认的0值(null)[译注:实际上,在C中一个为初始化的指针通常具有一个随机的值,这是很危险的!]。这两个声明以不同的方式使用存储区,他们不可能共存。避免这种类型冲突的一个方法是使用像lint这样的工具(如果可以的话)。为了在一个程序的不同编译单元之间检查类型冲突,一些程序需要一次看到其所有部分。典型的编译器无法完成,但lint可以。避免该问题的另一种方法是将外部声明放到包含文件中。这时,一个外部对象的类型仅出现一次脚注[7]。
/ @8 K+ ~+ K1 D- ]# \. y打造最好的电脑自学交流论坛
3 y' l" x7 L% |7 I- C4 语义缺陷打造最好的电脑自学交流论坛" }+ s6 v7 T' s7 l
打造最好的电脑自学交流论坛& T: r. `  k+ w( P
    一个句子可以是精确拼写的并且没有语法错误,但仍然没有意义。在这一节中,我们将会看到一些程序的写法会使得它们看起来是一个意思,但实际上是另一种完全不同的意思。我们还要讨论一些表面上看起来合理但实际上会产生未定义结果的环境。我们这里讨论的东西并不保证能够在所有的C实现中工作。我们暂且忘记这些能够在一些实现中工作但可能不能在另一些实现中工作的东西,直到第7节讨论可以执行问题为止。我爱电脑技术社区--打造最好的电脑技术自学交流平台: F) y7 x7 M( P& T9 Z3 G! W# ?
打造最好的电脑自学交流论坛) Y3 N1 a3 q* \2 R& k6 K) P
4.1 表达式求值顺序
+ \! t) W1 L; ^  f. w" P# p/ g电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站我爱电脑技术论坛$ v5 J* t% |7 q( b$ E' n
    一些C运算符以一种已知的、特定的顺序对其操作数进行求值。但另一些不能。例如,考虑下面的表达式:
9 k- M! Z$ n# w$ G; C
6 v' d3 m* z5 H' K0 A' g打造最好的电脑自学交流论坛a < b && c < dwww.520diannao.com) ]5 u/ M+ @1 J  E+ i0 A6 @

/ k6 L/ s; [! fwww.520diannao.com        C语言定义规定a < b首先被求值。如果a确实小于b,c < d必须紧接着被求值以计算整个表达式的值。但如果a大于或等于b,则c < d根本不会被求值。要对a < b求值,编译器对a和b的求值就会有一个先后。但在一些机器上,它们也许是并行进行的。C中只有四个运算符&&、||、?:和,指定了求值顺序。&&和||最先对左边的操作数进行求值,而右边的操作数只有在需要的时候才进行求值。而?:运算符中的三个操作数:a、b和c,最先对a进行求值,之后仅对b或c中的一个进行求值,这取决于a的值。,运算符首先对左边的操作数进行求值,然后抛弃它的值,对右边的操作数进行求值脚注[8]。C中所有其它的运算符对操作数的求值顺序都是未定义的。事实上,赋值运算符不对求值顺序做出任何保证。出于这个原因,下面这种将数组x中的前n个元素复制到数组y中的方法是不可行的:
( v/ t' y0 B- x5 d( u7 @我爱电脑技术论坛打造最好的电脑自学交流论坛( z5 v8 Z' b- w3 r0 i. S
i = 0;我爱电脑技术论坛+ I7 h5 X( w) A$ ?4 W0 ?
while(i < n)
7 ~- B* _9 p* E3 c( J2 ], C! Zy = x[i++];
9 C1 i+ Z' n, H$ `! W* Y电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
0 t4 l$ m( @3 K! [) a我爱电脑技术社区--打造最好的电脑技术自学交流平台        其中的问题是y的地址并不保证在i增长之前被求值。在某些实现中,这是可能的;但在另一些实现中却不可能。另一种情况出于同样的原因会失败:
& ^/ ?4 d* P$ R1 Y) L- X
1 P) U# u% h4 z2 ?( Z% Z" Nwww.520diannao.comi = 0;+ r! Z% b/ Q3 [$ d
while(i < n)打造最好的电脑自学交流论坛: e. ]1 D  M9 Q8 w& ]5 _
y[i++] = x;电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站# X0 C6 z: ^. p1 k: t- I
我爱电脑技术社区--打造最好的电脑技术自学交流平台- X8 X; k/ n0 d- S
而下面的代码是可以工作的:
' U8 ^2 }* K7 I/ M! Z我爱电脑技术社区--打造最好的电脑技术自学交流平台( P7 {$ j, }9 b0 E
i = 0;打造最好的电脑自学交流论坛+ F3 J; N: C1 T1 F$ K& j2 {0 c& N* G
while(i < n) {
3 X% v) ^$ U6 m' g( W5 By = x;
2 x- K) L- y. v9 Q- B% M  \我爱电脑技术论坛i++;打造最好的电脑自学交流论坛1 w, D/ J& R2 o+ C
}我爱电脑技术论坛' Q: K6 r7 A! B6 i/ x- D( Z

/ b. S/ I: X& N. f: I* ?电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站当然,这可以简写为:
; t9 h$ [, a2 e. L5 y' d我爱电脑技术论坛( K; ?, w! u: ?& i6 C
for(i = 0; i < n; i++)www.520diannao.com0 x: Y3 E0 Q1 B, \
y = x;我爱电脑技术社区--打造最好的电脑技术自学交流平台- a% v2 M( |0 x" V+ b
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站" B# l8 }" f* d# e' J
4.2 &&、||和!运算符打造最好的电脑自学交流论坛- i- c, z3 Y# P' _

  \( I7 {! R+ F% l+ o% r$ N打造最好的电脑自学交流论坛    C中有两种逻辑运算符,在某些情况下是可以交换的:按位运算符&、|和~,以及逻辑运算符&&、||和!。一个程序员如果用某一类运算符替换相应的另一类运算符会得到某些奇怪的效果:程序可能会正确地工作,但这纯属偶然。&&、||和!运算符将它们的参数视为仅有“真”或“假”,通常约定0代表“假”而其它的任意值都代表“真”。这些运算符返回1表示“真”而返回0表示“假”,而且&&和||运算符当可以通过左边的操作数确定其返回值时,就不会对右边的操作数进行求值。因此!10是零,因为10非零;10 && 12是1,因为10和12都非零;10 || 12也是1,因为10非零。另外,最后一个表达式中的12不会被求值,10 || f()中的f()也不会被求值。考虑下面这段用于在一个表中查找一个特定元素的程序:
3 f5 e) C  P/ ]: ]5 {www.520diannao.com
8 @0 r$ z7 q2 u; W$ _打造最好的电脑自学交流论坛i = 0;我爱电脑技术社区--打造最好的电脑技术自学交流平台) P& C" x5 O1 Y
while(i < tabsize && tab != x)
4 s& O$ L$ N( n3 z电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站i++;www.520diannao.com9 w$ y+ S! ?1 D* G
www.520diannao.com% |  w$ J# B7 I* F5 r
        这段循环背后的意思是如果i等于tabsize时循环结束,元素未被找到。否则,i包含了元素的索引。电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站0 C( M( F6 R/ |
假设这个例子中的&&不小心被替换为了&,这个循环可能仍然能够工作,但只有两种幸运的情况可以使它停下来。首先,这两个操作都是当条件为假时返回0,当条件为真时返回1。只要x和y都是1或0,x & y和x && y都具有相同的值。然而,如果当使用了出了1之外的非零值表示“真”时互换了这两个运算符,这个循环将不会工作。其次,由于数组元素不会改变,因此越过数组最后一个元素进一个位置时是无害的,循环会幸运地停下来。失误的程序会越过数组的结尾,因为&不像&&,总是会对所有的操作数进行求值。因此循环的最后一次获取tab时i的值已经等于tabsize了。如果tabsize是tab中元素的数量, 则会取到tab中不存在的一个值。我爱电脑技术社区--打造最好的电脑技术自学交流平台7 O6 I1 C3 ^4 C0 N
www.520diannao.com" y- G6 S: |* o0 z1 q
4.3 下标从零开始我爱电脑技术论坛- s! V# U$ G, J4 c3 m# \
www.520diannao.com6 R6 A- M& q8 X2 b2 C, J
    在很多语言中,具有n个元素的数组其元素的号码和它的下标是从1到n严格对应的。但在C中不是这样。一个具有n个元素的C数组中没有下标为n的元素,其中的元素的下标是从0到n - 1。因此从其它语言转到C语言的程序员应该特别小心地使用数组:打造最好的电脑自学交流论坛# R! h& V3 [4 s; f; z6 g) o) B

! b2 f4 A$ t4 K* w! e/ d我爱电脑技术论坛int i, a[10];打造最好的电脑自学交流论坛* E- h. v4 D4 v, B
for(i = 1; i <= 10; i++)电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站* i9 Q; s: n0 c$ [4 j6 m# h
a = 0;www.520diannao.com8 u& k4 Z! f9 |4 I' S5 h  ?4 j& K

6 k8 R& K* a$ h6 Mwww.520diannao.com        这个例子的目的是要将a中的每个元素都设置为0,但没有期望的效果。因为for语句中的比较i < 10被替换成了i <= 10,a中的一个编号为10的并不存在的元素被设置为了0,这样内存中a后面的一个字被破坏了。如果编译该程序的编译器按照降序地址为用户变量分配内存,则a后面就是i。将i设置为零会导致该循环陷入一个无限循环。
$ `! t3 H+ c/ f( B% ~打造最好的电脑自学交流论坛电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站, n2 p" L: Q& b9 }( V( n
4.4 C并不总是转换实参' L) p$ G: Y" k5 Y# S" Y4 M
: ~4 _& I. Y) O* l0 w+ T- _' _
下面的程序段由于两个原因会失败:www.520diannao.com3 e: S0 d- R) B& q) G8 ^* [

/ n- e' ?- a; T8 {1 M0 N( G% ]) xwww.520diannao.comdouble s;打造最好的电脑自学交流论坛6 K, E' L9 L2 V8 Q/ B
s = sqrt(2);我爱电脑技术社区--打造最好的电脑技术自学交流平台# a) B% O: _$ r
printf("%g\n", s);打造最好的电脑自学交流论坛7 ^/ [) A! ]" U- p; K1 D
我爱电脑技术论坛) m/ ?1 Z7 V6 O) I; p! s* G
        第一个原因是sqrt()需要一个double值作为它的参数,但没有得到。第二个原因是它返回一个double值但没有这样声明。改正的方法只有一个:
% l+ d5 s$ I$ J( b0 ?. A; P# t电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
1 y  N* X' n7 r9 M5 s电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站double s, sqrt();我爱电脑技术论坛% @; w- H- H% h
s = sqrt(2.0);我爱电脑技术社区--打造最好的电脑技术自学交流平台# |" S0 l+ C4 D- y- ^+ n3 d
printf("%g\n", s);
0 J* r! y0 \( n: Q) y1 V0 a, j1 h  K8 v2 P1 z
         C中有两个简单的规则控制着函数参数的转换:(1)比int短的整型被转换为int;(2)比double短的浮点类型被转换为double。所有的其它值不被转换。确保函数参数类型的正确是程序员的责任。因此,一个程序员如果想使用如sqrt()这样接受一个double类型参数的函数,就必须仅传递给它float或double类型的参数。常数2是一个int,因此其类型是错误的。当一个函数的值被用在表达式中时,其值会被自动地转换为适当的类型。然而,为了完成这个自动转换,编译器必须知道该函数实际返回的类型。没有更进一步声明的函数被假设返回int,因此声明这样的函数并不是必须的。然而,sqrt()返回double,因此在成功使用它之前必须要声明。实际上,C实现通常允许一个文件包含include语句来包含如sqrt()这些库函数的声明,但是对那些自己写函数的程序员来说,书写声明也是必要的——或者说,对那些书写非凡的C程序的人来说是有必要的。这里有一个更加壮观的例子:
* ^$ Y, N+ \4 d9 M0 t我爱电脑技术社区--打造最好的电脑技术自学交流平台电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站& n. c, R. B/ O6 F; f7 b) x
main() {电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站' y& y  z% o3 U3 m/ G9 M. X2 b
int i;我爱电脑技术社区--打造最好的电脑技术自学交流平台+ ]) z& O9 @$ H0 x2 s7 r5 X5 O9 Y
char c;
' j' }9 Y# m8 R我爱电脑技术社区--打造最好的电脑技术自学交流平台for(i = 0; i < 5; i++) {
; v+ B. `: Z& v+ k* U6 ^/ X. ~我爱电脑技术论坛scanf("%d", &c);我爱电脑技术社区--打造最好的电脑技术自学交流平台- {9 F! h$ i0 {% r
printf("%d", i);我爱电脑技术论坛7 Y9 T, x- X# m
}! C+ A0 i% `+ s! M
printf("\n");
5 P9 p- |' J# l) U$ wwww.520diannao.com}
& n. P3 \) }) B$ ~2 @6 G' Hwww.520diannao.com打造最好的电脑自学交流论坛0 d2 |& g$ A& _; A
        表面上看,这个程序从标准输入中读取五个整数并向标准输出写入0 1 2 3 4。实际上,它并不总是这么做。譬如在一些编译器中,它的输出为0 0 0 0 0 1 2 3 4。为什么?因为c的声明是char而不是int。当你令scanf()去读取一个整数时,它需要一个指向一个整数的指针。但这里它得到的是一个字符的指针。但scanf()并不知道它没有得到它所需要的:它将输入看作是一个指向整数的指针并将一个整数存贮到那里。由于整数占用比字符更多的内存,这样做会影响到c附近的内存。c附近确切是什么是编译器的事;在这种情况下这有可能是i的低位。因此,每当向c中读入一个值,i就被置零。当程序最后到达文件结尾时,scanf()不再尝试向c中放入新值,i才可以正常地增长,直到循环结束。

TOP

4.5 指针不是数组
0 |( L* Z* _$ A$ l+ |$ _我爱电脑技术社区--打造最好的电脑技术自学交流平台我爱电脑技术论坛8 ~: T/ g' J; m: m
   C程序通常将一个字符串转换为一个以空字符结尾的字符数组。假设我们有两个这样的字符串s和t,并且我们想要将它们连接为一个单独的字符串r。我们通常使用库函数strcpy()和strcat()来完成。下面这种明显的方法并不会工作:打造最好的电脑自学交流论坛0 K! {8 h5 N$ o4 G9 h
我爱电脑技术论坛/ ^4 j* Y+ Z0 A( k6 {& _+ V
char *r;
! j/ z$ B/ q! P. awww.520diannao.comstrcpy(r, s);电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站2 N# C2 ^) ]8 |
strcat(r, t);我爱电脑技术社区--打造最好的电脑技术自学交流平台' B* l2 m9 r3 k) i7 V$ Y$ L3 ]
打造最好的电脑自学交流论坛/ y& S' x3 F( H& q# w+ |
        这是因为r没有被 初始化为指向任何地方。尽管r可能潜在地表示某一块内存,但这并不存在,直到你分配它。让我们再试试,为r分配一些内存:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站5 b) x4 R6 t. M. Y8 ]# m1 W( j

, t% Z4 H. U5 Nwww.520diannao.comchar r[100];我爱电脑技术论坛6 O. ~4 r1 ^; o$ H- }* {# ?
strcpy(r, s);电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站  O1 y- B9 p$ `$ U
strcat(r, t);我爱电脑技术论坛4 z, G: ^- I3 \8 Q5 Q; _. V
打造最好的电脑自学交流论坛( O7 L; d  `5 S, z
         这只有在s和t所指向的字符串不很大的时候才能够工作。不幸的是,C要求我们为数组指定的大小是一个常数,因此无法确定r是否足够大。然而,很多C实现带有一个叫做malloc()的库函数,它接受一个数字并分配这么多的内存。通常还有一个函数strlen(),可以告诉我们一个字符串中有多少个字符:因此,我们可以写:+ S/ N8 Y2 l& C; R: F
www.520diannao.com  v% S2 b4 n8 X- Z) Y3 y
char *r, *malloc();
- U4 o. x# ~% d& a5 B+ R5 `r = malloc(strlen(s) + strlen(t));
9 c8 y# u# R: z- K+ istrcpy(r, s);
7 A1 |+ {# V4 E# h6 I4 F* |打造最好的电脑自学交流论坛strcat(r, t);. O- p: r7 C/ H4 F8 N" Q1 P; t: N
打造最好的电脑自学交流论坛# }" l9 B6 O0 _4 S
        然而这个例子会因为两个原因而失败。首先,malloc()可能会耗尽内存,而这个事件仅通过静静地返回一个空指针来表示。其次,更重要的是,malloc()并没有分配足够的内存。一个字符串是以一个空字符结束的。而strlen()函数返回其字符串参数中所包含字符的数量,但不包括结尾的空字符。因此,如果strlen(s)是n,则s需要n + 1个字符来盛放它。因此我们需要为r分配额外的一个字符。再加上检查malloc()是否成功,我们得到:我爱电脑技术社区--打造最好的电脑技术自学交流平台" u6 }/ N" m: z% l. j5 P
我爱电脑技术社区--打造最好的电脑技术自学交流平台' l  T0 C+ [/ j
char *r, *malloc();
; P1 d* {: ^: g  s: |) `7 F打造最好的电脑自学交流论坛r = malloc(strlen(s) + strlen(t) + 1);我爱电脑技术论坛( j: k, o& n* s- ?; h# Z! K9 D8 V5 q
if(!r) {打造最好的电脑自学交流论坛. D! f6 s& J5 c3 q( D
complain();
5 G, T: a2 r0 I7 @7 O2 ~& [我爱电脑技术论坛exit(1);电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站' |$ [/ L& z/ w+ X& x' h( ?8 n
}
/ y: k+ C' h  Q; T6 ~打造最好的电脑自学交流论坛strcpy(r, s);我爱电脑技术论坛3 e; B6 u/ S9 t& l: P3 h9 L
strcat(r, t);www.520diannao.com1 Q( w+ J. `2 p* Q/ L4 M
打造最好的电脑自学交流论坛0 O- n; V# ^' i  A' W
4.6 避免提喻法我爱电脑技术社区--打造最好的电脑技术自学交流平台3 P5 |8 \+ q3 Q% B; r. i
我爱电脑技术社区--打造最好的电脑技术自学交流平台. O5 B6 r. A0 I  n2 n2 r
    提喻法(Synecdoche, sin-ECK-duh-key)是一种文学手法,有点类似于明喻或暗喻,在牛津英文词典中解释如下:“a more comprehensive term is used for a less comprehensive or vice versa; as whole for part or part for whole, genus for species or species for genus, etc.(将全面的单位用作不全面的单位,或反之;如整体对局部或局部对整体、一般对特殊或特殊对一般,等等。)”这可以精确地描述C中通常将指针误以为是其指向的数据的错误。正将常会在字符串中发生。例如:
( Q! P3 _* Y: K4 H+ W我爱电脑技术社区--打造最好的电脑技术自学交流平台
+ c  a, ~# l( {4 n  @6 G6 c: X打造最好的电脑自学交流论坛char *p, *q;
% P! M4 S& K5 h9 O- K  O4 J我爱电脑技术论坛p = "xyz";$ J- u) j0 L1 _; f7 ~  g

) M% c; ]4 |# X; a3 f: b- J        尽管认为p的值是xyz有时是有用的,但这并不是真的,理解这一点非常重要。p的值是指向
  M: q* Q  V$ u# d1 l" l# e3 r9 |( q% ~! p打造最好的电脑自学交流论坛一个有四个字符的数组中第0个元素的指针,这四个字符是'x'、'y'、'z'和'\0'。因此,如果我们现在执行:www.520diannao.com7 O) z! o# i, O3 h2 b) T( U

( W7 H. _4 e9 P0 p7 b& j! w, ~www.520diannao.comq = p;打造最好的电脑自学交流论坛0 _! L6 Z* P2 m& f6 `
打造最好的电脑自学交流论坛( |3 R0 ~$ V2 f
p和q会指向同一块内存。内存中的字符没有因为赋值而被复制。这种情况看起来是这样的:www.520diannao.com% {2 k7 T' b% P4 K! \* r
我爱电脑技术论坛' I% N' s9 ?9 R% y) {5 f
<center><img src="images/CTraps/CTraps1.gif"></center>2 x" Q% I' W% V8 Y4 q( J0 i

  A, y5 @. h. y8 I( Z打造最好的电脑自学交流论坛要记住的是,复制一个指针并不能复制它所指向的东西。因此,如果之后我们执行:打造最好的电脑自学交流论坛" Z: }- T- e! h. e& x: V

( k) o2 Z4 Z- j, r+ I  b4 t打造最好的电脑自学交流论坛q[1] = 'Y';
7 ?) i5 K3 N3 T8 M9 G1 ^$ K我爱电脑技术论坛( h# n; O2 g: G2 O0 C# Z( g
q所指向的内存包含字符串xYz。p也是,因为p和q指向相同的内存。www.520diannao.com3 n2 g# P# @( v/ e' K4 _" w
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站3 X2 N9 R1 k+ `3 Q
4.7 空指针不是空字符串我爱电脑技术社区--打造最好的电脑技术自学交流平台3 A8 T* _, q. t: N7 W1 r

# q+ y: h6 R9 |% k4 O/ R) Gwww.520diannao.com    将一个整数转换为一个指针的结果是实现相关的(implementation-dependent),除了一个例外。这个例外是常数0,它可以保证被转换为一个与其它任何有效指针都不相等的指针。这个值通常类似这样定义:
) z7 E- t' z8 z, C+ x. R7 e7 \我爱电脑技术社区--打造最好的电脑技术自学交流平台打造最好的电脑自学交流论坛* a1 }5 T$ f' X1 l0 G
#define NULL 0
' [" n' i1 a, ?& Z* {www.520diannao.comwww.520diannao.com9 g+ r8 \1 p8 h* D
        但其效果是相同的。要记住的一个重要的事情是,当用0作为指针时它决不能被解除引用。换句话说,当你将0赋给一个指针变量后,你就不能访问它所指向的内存。不能这样写:我爱电脑技术社区--打造最好的电脑技术自学交流平台1 @+ K1 x3 e5 \# ~/ s
我爱电脑技术论坛; ]  t/ d% P/ u
  K) ^4 e4 [! Y4 X' t/ i9 ~
if(p == (char *)0) ...电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站, A" e3 t/ a0 ]* N* y$ e. P
我爱电脑技术论坛( w& r+ d0 W7 J; A
也不能这样写:
1 {: {( B9 k1 ^/ }. z- Y我爱电脑技术论坛' ~" F- c+ R0 ]' A# [
if(strcmp(p, (char *)0) == 0) ...我爱电脑技术社区--打造最好的电脑技术自学交流平台8 P0 n+ ?$ g5 m6 m2 ^
我爱电脑技术论坛' `% n* D1 E. {3 T0 g. B0 P$ p
因为strcmp()总是通过其参数来查看内存地址的。如果p是一个空指针,这样写也是无效的:
& U# Z, Y8 _3 W* X! q- |电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站www.520diannao.com4 j3 F- A: x* [$ D& n. `# m& ?; k
printf(p);
+ m- S: B& |0 B  }' {. E8 q我爱电脑技术论坛
8 K1 B$ S9 b$ x3 p: k6 O# j9 G0 c# C打造最好的电脑自学交流论坛
" U4 P2 s' o* v+ R, P$ P# Q电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站我爱电脑技术社区--打造最好的电脑技术自学交流平台- e: r8 N9 [$ [- b
printf("%s", p);
5 Z& W- y1 S7 t& K4 {www.520diannao.com
$ ~5 _4 D" j) k& ~$ K9 n% X0 r/ n, r我爱电脑技术论坛4.8 整数溢出
% q7 ^2 V& J: k, E( H0 O我爱电脑技术社区--打造最好的电脑技术自学交流平台
- R/ [& _& J+ D' {" ?* p7 @我爱电脑技术社区--打造最好的电脑技术自学交流平台    C语言关于整数操作的上溢或下溢定义得非常明确。只要有一次操作数是无符号的,结果就是无符号的,并且以2^n为模,其中n为字长。如果两个操作数都是带符号的,则结果是未定义的。例如,假设a和b是两个非负整型变量,你希望测试a + b是否溢出。一个明显的办法是这样的:
0 U% `4 p, f! `3 A5 c5 d7 F我爱电脑技术社区--打造最好的电脑技术自学交流平台
' W8 ^) g. M. O/ V/ \; |我爱电脑技术社区--打造最好的电脑技术自学交流平台if(a + b < 0)电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站7 Q9 h- Z3 L9 h3 j5 U( q
complain();www.520diannao.com% V# H6 p  o6 [! v1 R8 T* e

/ U/ N- \* @' i* }  U: R+ n我爱电脑技术论坛        通常,这是不会工作的。一旦a + b发生了溢出,对于结果的任何赌注都是没有意义的。例如,在某些机器上,一个加法运算会将一个内部寄存器设置为四种状态:正、负、零或溢出。 在这样的机器上,编译器有权将上面的例子实现为首先将a和b加在一起,然后检查内部寄存器状态是否为负。如果该运算溢出,内部寄存器将处于溢出状态,这个测试会失败。使这个特殊的测试能够成功的一个正确的方法是依赖于无符号算术的良好定义,既要在有符号和无符号之间进行转换:
6 Z( @& \- }7 i, @8 [我爱电脑技术社区--打造最好的电脑技术自学交流平台) m0 }$ N3 X9 M  R* b* {! q
if((int)((unsigned)a + (unsigned)b) < 0)电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站& _+ T$ E% a8 t, u, E, c2 y$ o( z
complain();+ m- Q- e1 Y& |" w% C0 r

7 x2 N2 Y8 g  g6 V4 U- w2 w我爱电脑技术社区--打造最好的电脑技术自学交流平台4.9 移位运算符
# g$ @. @4 h) f! R8 X, }; ?www.520diannao.com我爱电脑技术论坛# K+ j& U# }, c. b9 y
    两个原因会令使用移位运算符的人感到烦恼:在右移运算中,空出的位是用0填充还是用符号位填充?移位的数量允许使用哪些数?第一个问题的答案很简单,但有时是实现相关的。如果要进行移位的操作数是无符号的,会移入0。如果操作数是带符号的,则实现有权决定是移入0还是移入符号位。如果在一个右移操作中你很关心空位,那么用unsigned来声明变量。这样你就有权假设空位被设置为电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站3 O/ T0 `" |5 [+ F8 W( U' J
0。第二个问题的答案同样简单:如果待移位的数长度为n,则移位的数量必须大于等于0并且
$ _/ _: m8 T) [4 W$ l- H. \2 Y+ ]www.520diannao.com严格地小于n。因此,在一次单独的操作中不可能将所有的位从变量中移出。例如,如果一个int是32位,且n是一个int,写n << 31和n << 0是合法的,但n << 32和n<< -1是不合法的。注意,即使实现将符号为移入空位,对一个带符号整数的右移运算和除以2的某次幂也不是等价的。为了证明这一点,考虑(-1) >> 1的值,这是不可能为0的。[译注:(-1) / 2的结果是0。]
* S. C: b$ x, t1 E8 Qwww.520diannao.com我爱电脑技术社区--打造最好的电脑技术自学交流平台# ?5 x, J6 w. O* }
5 库函数www.520diannao.com" k4 |$ d: w  R6 E& h9 }- p
3 C3 F2 w1 w- Z1 w; @$ h
    每个有用的C程序都会用到库函数,因为没有办法把输入和输出内建到语言中去。在这一节中,我们将会看到一些广泛使用的库函数在某种情况下会出现的一些非预期行为。
! b# D' @' h& p我爱电脑技术社区--打造最好的电脑技术自学交流平台
; h  r& ]- l( z2 C( u) qwww.520diannao.com5.1 getc()返回整数
- }6 L! o* k# Z1 u" ]8 wwww.520diannao.com
3 G9 l0 ^& F2 i3 ^0 I, C1 S考虑下面的程序:www.520diannao.com: g6 I' N2 X$ P

2 K3 T5 G6 X% a+ h$ F( z, C2 l我爱电脑技术社区--打造最好的电脑技术自学交流平台#include <stdio.h>
0 p9 p; X" v3 D2 N8 m$ C我爱电脑技术社区--打造最好的电脑技术自学交流平台www.520diannao.com1 [+ S0 S* ?% U* B9 A
main() {我爱电脑技术社区--打造最好的电脑技术自学交流平台: H% d# m. d1 I% ?! s
char c;
9 G: A( j0 E7 C6 a0 v2 |5 W! ^www.520diannao.com
4 I' q8 t. b) t# X8 g* |& i- swhile((c = getchar()) != EOF)
& X! `9 r8 l2 {我爱电脑技术社区--打造最好的电脑技术自学交流平台putchar(c);
, h+ D' E$ J" Z7 F+ y" o8 r我爱电脑技术论坛}www.520diannao.com% F) }/ h3 }# [" b

& M' i$ F) v, _电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站        这段程序看起来好像要讲标准输入复制到标准输出。实际上,它并不完全会做这些。原因是c被声明为字符而不是整数。这意味着它将不能接收可能出现的所有字符包括EOF。因此这里有两种可能性。有时一些合法的输入字符会导致c携带和EOF相同的值,有时又会使c无法存放EOF值。在前一种情况下,程序会在文件的中间停止复制。在后一种情况下,程序会陷入一个无限循环。实际上,还存在着第三种可能:程序会偶然地正确工作。C语言参考手册严格地定义了表达式
9 D( u# b: d" G, x1 b1 X电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站/ X7 K) N& _: P, D# G
((c = getchar()) != EOF)打造最好的电脑自学交流论坛# F5 {7 C+ S  u+ ^9 U& D

8 Y) R( f& X6 o: M/ W4 k7 B电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站的结果。我爱电脑技术论坛4 s* T9 U! j( u  c
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站) A4 _% h+ O" H
其6.1节中声明:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站0 X, ^# t. y5 |1 X# F! e- C

) L1 V, l( D; B4 l. k7 U打造最好的电脑自学交流论坛    当一个较长的整数被转换为一个较短的整数或一个char时,它会被截去左侧;超出的位被简单地丢弃。
' l2 X  K3 U, s: a& \5 E! Swww.520diannao.com
; Q. ?" f6 _' B. k$ ^7 j! B. Q5 `7.14节中声明:
% h# C  B/ z) m1 g: R% e4 F: f) w, pwww.520diannao.com
$ T. ^$ A& o4 Z% L& W, T- ?电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    存在着很多赋值运算符,它们都是从右至左结合的。它们都需要一个左值作为左侧的操作数,而赋值表达式的类型就是其左侧的操作数的类型。其值就是已经付过值的左操作数的值。 这两个条款的组合效果就是必须通过丢弃getchar()的结果的高位,将其截短为字符,之后这个被截短的值再与EOF进行比较。作为这个比较的一部分,c必须被扩展为一个整数,或者采取将左侧的位用0填充,或者适当地采取符号扩展。然而,一些编译器并没有正确地实现这个表达式。它们确实将getchar()的值的低几位赋给c。但在c和EOF的比较中,它们却使用了getchar()的值!这样做的编译器会使这个事例程序看起来能够“正确地”工作。
3 s' ]- v2 n& z1 E我爱电脑技术社区--打造最好的电脑技术自学交流平台电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站# x" P; t; B" N
5.2 缓冲输出和内存分配打造最好的电脑自学交流论坛, R4 k4 a, ]* Y% v, \! I

& ]  J/ I. ^7 f我爱电脑技术论坛    当一个程序产生输出时,能够立即看到它有多重要?这取决于程序。例如,终端上显示输出并要求人们坐在终端前面回答一个问题,人们能够看到输出以知道该输入什么就显得至关重要了。另一方面,如果输出到一个文件中,并最终被发送到一个行式打印机,只有所有的输出最终能够到达那里是重要的。立即安排输出的显示通常比将其暂时保存在一大块一起输出要昂贵得多。因此,C实现通常www.520diannao.com. e3 l% x0 W. z+ E" X3 h
允许程序员控制产生多少输出后在实际地写出它们。这个控制通常约定为一个称为setbuf()的库函数。如果buf是一个具有适当大小的字符数组,则电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站& u! i3 t! L" q- ^0 A/ k

4 L8 `" `  \7 D* s5 i我爱电脑技术论坛setbuf(stdout, buf);
! x# ^# n* Y4 V0 {- F- T! a3 [我爱电脑技术社区--打造最好的电脑技术自学交流平台
! P6 L3 M* T$ ]4 P电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站        将告诉I/O库写入到stdout中的输出要以buf作为一个输出缓冲,并且等到buf满了或程序员直接调用fflush()再实际写出。缓冲区的合适的大小在<stdio.h>中定义为BUFSIZ。因此,下面的程序解释了通过使用setbuf()来讲标准输入复制到标准输出:
% o4 K  E0 I9 j9 z5 I3 n我爱电脑技术论坛5 I* q+ l8 g4 n  \: D( ]1 }
#include <stdio.h>
: @( |) G4 P) b: y! J% L- R电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站www.520diannao.com3 K2 A9 s6 }/ ]4 r! M/ R
main() {电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站6 j3 V" B& D5 V  N( y: {+ q
int c;www.520diannao.com+ m  }# y& h- u  q: O* e& @0 k

2 l% ]( y: X; F, h" ?' m! O0 y. ~! Hwww.520diannao.comchar buf[BUFSIZ];
5 g1 [$ p# m4 G# O; ]( f! d. k打造最好的电脑自学交流论坛setbuf(stdout, buf);
- x. Y4 C. P4 j1 Q# c4 I, ?2 w我爱电脑技术社区--打造最好的电脑技术自学交流平台www.520diannao.com6 W; _, J$ ]5 B1 H$ h3 b7 b
while((c = getchar()) != EOF)www.520diannao.com; `+ q' ?1 ?6 D  C. d, u
putchar(c);我爱电脑技术论坛/ m) }; ?1 q- E; Y# \' @3 K- x
}
7 d3 \9 d' L( h! h, o2 W打造最好的电脑自学交流论坛) J$ Z: [6 B: D3 j. s1 S0 q
        不幸的是,这个程序是错误的,因为一个细微的原因。要知道毛病出在哪,我们需要知道缓冲区最后一次刷新是在什么时候。答案:主程序完成之后,作为库在将控制交回到操作系统之前所执行的清理的一部分。在这一时刻,缓冲区已经被释放了!有两种方法可以避免这一问题。首先,是用静态缓冲区,或者将其显式地声明为静态:
) d. b1 n$ |* O( D' awww.520diannao.com
, t$ Q2 g- y0 L9 K# H: o1 Z& fwww.520diannao.comstatic char buf[BUFSIZ];8 m. O+ X9 i: f& ]: P! ~' M
  s2 ?& F, q3 E+ X7 J2 w- `# B$ \
或者将整个声明移到主函数之外。另一种可能的方法是动态地分配缓冲区并且从不释放它:8 }, u3 ~, Q) e* n8 x! H5 l, \
www.520diannao.com& t8 }2 \: V7 @& I, K( X3 n$ H8 c/ i; N
char *malloc();
3 f7 `- R8 X: U  L: C* Z电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站setbuf(stdout, malloc(BUFSIZ));
  o# n1 k$ N2 E8 H" H打造最好的电脑自学交流论坛
# d! @- L$ I' _+ j电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站        注意在后一种情况中,不必检查malloc()的返回值,因为如果它失败了,会返回一个空指针。而setbuf()可以接受一个空指针作为其第二个参数,这将使得stdout变成非缓冲的。这会运行得很慢,但它是可以运行的。www.520diannao.com3 \" S& `5 _5 W( u7 G
打造最好的电脑自学交流论坛2 u6 K1 c: ^) G9 s9 |1 ]
6 预处理器
! p% F6 @* W$ K/ q+ l/ k) k2 W我爱电脑技术社区--打造最好的电脑技术自学交流平台我爱电脑技术论坛' Z) ?2 b" ]7 u) d- I0 i
    运行的程序并不是我们所写的程序:因为C预处理器首先对其进行了转换。出于两个主要原因(和很多次要原因),预处理器为我们提供了一些简化的途径。首先,我们希望可以通过改变一个数字并重新编译程序来改变一个特殊量(如表的大小)的所有实例脚注[9]。其次,我们可能希望定义一些东西,它们看起来象函数但没有函数调用所需的运行开销。例如,putchar()和getchar()通常实现为宏以避免对每一个字符的输入输出都要进行函数调用。打造最好的电脑自学交流论坛# c$ n, \+ d9 G$ K! ^% h! R7 D
www.520diannao.com* m% g$ d6 f! t
6.1 宏不是函数我爱电脑技术论坛- t$ x) K# z, s6 n8 Z

* x+ p. J$ L3 rwww.520diannao.com由于宏可以象函数那样出现,有些程序员有时就会将它们视为等价的。因此,看下面的定义:
. `) P% [/ @1 Q7 \我爱电脑技术社区--打造最好的电脑技术自学交流平台我爱电脑技术社区--打造最好的电脑技术自学交流平台  x0 B" x* o6 \/ G% Z8 }
#define max(a, b) ((a) > (b) ? (a) : (b))电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站" w8 y3 F! ^) {- Z4 k
我爱电脑技术论坛& q# g) I5 c; d
       注意宏体中所有的括号。它们是为了防止出现a和b是带有比>优先级低的表达式的情况。一个重要的问题是,像max()这样定义的宏每个操作数都会出现两次并且会被求值两次。因此,在这个例子中,如果a比b大,则a就会被求值两次:一次是在比较的时候,而另一次是在计算max()值的时候。这不仅是低效的,还会发生错误:
: i/ N. v  y+ ~( o! owww.520diannao.com
3 E+ Q  q) P: f( n/ K我爱电脑技术论坛biggest = x[0];我爱电脑技术社区--打造最好的电脑技术自学交流平台* Q4 h  A7 w  T( ]$ s: X
i = 1;打造最好的电脑自学交流论坛9 T8 g- Z. ?! {' F) B
while(i < n)打造最好的电脑自学交流论坛+ C! {& j8 @# }4 e- y
biggest = max(biggest, x[i++]);打造最好的电脑自学交流论坛+ O) G+ c5 u9 r( p$ i8 k
, y/ n5 {* i( D
        当max()是一个真正的函数时,这会正常地工作,但当max()是一个宏的时候会失败。譬如,假设x[0]是2、x[1]是3、x[2]是1。我们来看看在第一次循环时会发生什么。赋值语句会被扩展为:
. @4 i  V( X; `6 v! E0 c打造最好的电脑自学交流论坛
* }0 c2 i7 D" N+ [# m6 l9 a# pwww.520diannao.combiggest = ((biggest) > (x[i++]) ? (biggest) : (x[i++]));
8 Z# W3 z* d: I我爱电脑技术论坛打造最好的电脑自学交流论坛/ b* ]! U2 A. H% i
        首先,biggest与x[i++]进行比较。由于i是1而x[1]是3,这个关系是“假”。其副作用是,i增长到2。由于关系是“假”,x[i++]的值要赋给biggest。然而,这时的i变成2了,因此赋给biggest的值是x[2]的值,即1。避免这些问题的方法是保证max()宏的参数没有副作用:打造最好的电脑自学交流论坛1 v6 b+ s* `4 u8 ]
www.520diannao.com7 x! A# d& Y& z3 Q1 _7 q
biggest = x[0];www.520diannao.com! r1 V; E" Q3 l2 J, y2 x# ]
for(i = 1; i < n; i++)
5 l% i5 ~0 R4 g7 D# Q5 T3 b7 A/ ?打造最好的电脑自学交流论坛biggest = max(biggest, x);打造最好的电脑自学交流论坛$ [! e( D. R0 z
打造最好的电脑自学交流论坛  [  T$ T2 h$ o+ |5 b3 ~
还有一个危险的例子是混合宏及其副作用。这是来自UNIX第八版的<stdio.h>中putc()宏的定义:
- ^' O4 g# `' Q
2 [9 S: b/ X% x8 x0 l: P; J* z我爱电脑技术社区--打造最好的电脑技术自学交流平台#define putc(x, p) (--(p)->_cnt >= 0 ? (*(p)->_ptr++ = (x)) : _flsbuf(x, p))www.520diannao.com, d2 R' I$ h7 o4 M4 H9 N7 R( D' d, d

, q. q$ ~4 i5 f. M) u0 Y电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站
0 g. q7 j3 W# G  T& v我爱电脑技术论坛        putc()的第一个参数是一个要写入到文件中的字符,第二个参数是一个指向一个表示文件的内部数据结构的指针。注意第一个参数完全可以使用如*z++之类的东西,尽管它在宏中两次出现,但只会被求值一次。而第二个参数会被求值两次(在宏体中,x出现了两次,但由于 它的两次出现分别在一个:的两边,因此在putc()的一个实例中它们之中有且仅有一个被求值)。由于putc()中的文件参数可能带有副作用,这偶尔会出现问题。不过,用户手册文档中提到:“由于putc()被实现为宏,其对待stream可能会具有副作用。特别是putc(c, *f++)不能正确地工作。”但是putc(*c++, f)在这个实现中是可以工作的。有些C实现很不小心。例如,没有人能正确处理putc(*c++, f)。另一个例子,考虑很多C库
- f; K& ]9 k) a) [  @打造最好的电脑自学交流论坛中出现的toupper()函数。它将一个小写字母转换为相应的大写字母,而其它字符不变。如果我们假设所有的小写字母和所有的大写字母都是相邻的(大小写之间可能有所差距),我们可以得到这样的函数:
: j/ O* z+ i% d: ?我爱电脑技术社区--打造最好的电脑技术自学交流平台
4 s( b4 @: A. k6 Jtoupper(c) {
4 S! s' f6 y  T5 d% W5 c4 ^if(c >= 'a' && c <= 'z')
3 M" j5 I8 A; C$ I打造最好的电脑自学交流论坛c += 'A' - 'a';
* V+ U% H: k* m8 O0 X) twww.520diannao.comreturn c;
- p( M8 D% _/ m* V! |打造最好的电脑自学交流论坛}
- [$ [3 j, d' T7 L电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站打造最好的电脑自学交流论坛7 k: D, g- l9 O2 a# Q$ X0 R
在很多C实现中,为了减少比实际计算还要多的调用开销,通常将其实现为宏:
5 @9 M6 I0 }0 L8 I我爱电脑技术论坛
: B+ {( v9 \7 b0 b, B电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + ('A' - 'a') : (c))打造最好的电脑自学交流论坛* K; \/ e1 E) k# E& o

; S+ q5 O6 S6 d" u3 N我爱电脑技术论坛        很多时候这确实比函数要快。然而,当你试着写toupper(*p++)时,会出现奇怪的结果。另一个需要注意的地方是使用宏可能会产生巨大的表达式。例如,继续考虑max()的定义:* o' {! W- [, v, g1 E3 H  m
我爱电脑技术论坛# c  J3 p4 X6 \, I, t* p. z1 x  I
#define max(a, b) ((a) > (b) ? (a) : (b))
1 E0 _( Z! y! Z) q我爱电脑技术论坛
1 G* r! |. A/ i: t电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站假设我们这个定义来查找a、b、c和d中的最大值。如果我们直接写:
/ [" c2 l9 x+ B* A' l8 q, ^
, `# Q2 V8 ~8 A我爱电脑技术论坛max(a, max(b, max(c, d)))
; D; P+ Z7 O' A* R+ N( owww.520diannao.com
% n5 K- F# O$ K5 W我爱电脑技术社区--打造最好的电脑技术自学交流平台它将被扩展为:打造最好的电脑自学交流论坛" L. `- Q$ m# Z7 H/ c: m/ X
www.520diannao.com- G- _" Y6 ^& l+ Z
((a) > (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))) ?
9 A$ \; L! ^- E! fwww.520diannao.com电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站( t4 q+ A1 b$ ?" s$ c) `8 y
(a) : (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))))打造最好的电脑自学交流论坛' P9 O) G6 X# J6 Z$ \$ s
- X  X0 E6 R  C; X

- o* |! T! b9 T" ^& y* j! F, Z电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站这出奇的庞大。我们可以通过平衡操作数来使它短一些:打造最好的电脑自学交流论坛: R6 g7 Y4 p3 {% v2 f  y

2 j% y: G+ C- l2 Pwww.520diannao.commax(max(a, b), max(c, d)); J+ r; a2 }8 }' \
www.520diannao.com* @3 p6 U( k3 E( M
这会得到:
& K& a% N. W; b6 E8 d打造最好的电脑自学交流论坛我爱电脑技术社区--打造最好的电脑技术自学交流平台& s  ]* w. k/ l8 k
((((a) > (b) ? (a) : (b))) > (((c) > (d) ? (c) : (d))) ?我爱电脑技术社区--打造最好的电脑技术自学交流平台, g- ^7 H+ N7 P/ m2 B2 L# r6 ]# W1 m
(((a) > (b) ? (a) : (b))) : (((c) > (d) ? (c) : (d))))
, w' }# c& G/ P* p/ g: k. M我爱电脑技术社区--打造最好的电脑技术自学交流平台
; g# A  g- Z5 i! Z6 Z这看起来还是写:打造最好的电脑自学交流论坛3 g' n, p8 L6 {4 ~* Q

2 T9 g- A2 j" X) L我爱电脑技术论坛biggest = a;
* y7 U. I  p9 e! u3 M/ p- P% rwww.520diannao.comif(biggest < b) biggest = b;
: M0 K: h3 _" @( ~4 O1 \7 c1 H我爱电脑技术论坛if(biggest < c) biggest = c;我爱电脑技术社区--打造最好的电脑技术自学交流平台& R9 ~3 _' d4 c
if(biggest < d) biggest = d;
( }- X6 F9 d/ E; s+ J  s) M; t电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站我爱电脑技术论坛$ D9 y& Y0 H& |2 z) S6 N+ Z
比较好一些。
. k: v6 z+ J7 \& A& A+ N/ J我爱电脑技术论坛
1 r& \, y6 X9 z' z电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站6.2 宏不是类型定义www.520diannao.com, V9 i4 r* L, T& d! J7 ^  S

7 {4 f9 p% C! R2 g/ ]6 O/ ]我爱电脑技术论坛宏的一个通常的用途是保证不同地方的多个事物具有相同的类型:我爱电脑技术社区--打造最好的电脑技术自学交流平台9 \& p2 }1 }* z+ {
/ a4 J/ d5 c* q  c- J0 f
#define FOOTYPE struct foo6 t3 m/ P9 W/ U2 Z
FOOTYPE a;www.520diannao.com" Q7 F4 i, F) b" E
FOOTYPE b, c;电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站- I9 v+ p8 r8 Q6 U4 s7 a- {) a& @$ u

2 c* h  Z" R9 S5 h1 \打造最好的电脑自学交流论坛        这允许程序员可以通过只改变程序中的一行就能改变a、b和c的类型,尽管a、b和c可能声明在很远的不同地方。使用这样的宏定义还有着可移植性的优势——所有的C编译器都支持它。很多C编译器并不支持另一种方法:
4 d( W1 Q. m$ [/ }" r7 B我爱电脑技术论坛
3 A( E8 ^% v* _6 o/ k5 Y" w打造最好的电脑自学交流论坛typedef struct foo FOOTYPE;
$ g& Y9 E9 ^: ]+ `# J" N+ r电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站打造最好的电脑自学交流论坛) w6 h1 C" V5 _# j3 n  t1 ?
        这将FOOTYPE定义为一个与struct foo等价的新类型。这两种为类型命名的方法可以是等价的,但typedef更灵活一些。例如,考虑下面的例子:我爱电脑技术社区--打造最好的电脑技术自学交流平台1 v! M& M* `' }+ I8 d+ p% X
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站9 e& s8 T3 J. K+ H* l& f% [
www.520diannao.com/ _$ g6 U  Z9 m3 F, x0 |  W( L2 E+ S" `
#define T1 struct foo *
5 r: Z# r1 h$ c9 m  _& v- Z6 Zwww.520diannao.comtypedef struct foo * T2;电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站: H: v0 p3 `. p3 b% b- c% Q

) Z3 F- ], s; L% H" N7 K打造最好的电脑自学交流论坛        这两个定义使得T1和T2都等价于一个struct foo的指针。但看看当我们试图在一行中声明多于一个变量的时候会发生什么:我爱电脑技术论坛* F' W& _" P2 a
我爱电脑技术社区--打造最好的电脑技术自学交流平台: w) P6 C5 @7 Q  a
T1 a, b;电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站8 O3 j! S, Z. f7 E0 g. y
T2 c, d;
' d& T( G) e1 g/ k5 c. h  K打造最好的电脑自学交流论坛
- }9 o+ b% S/ }! i8 q打造最好的电脑自学交流论坛第一个声明被扩展为:
+ q7 w! V" g* {www.520diannao.com
  k: @1 G/ q9 T( }打造最好的电脑自学交流论坛struct foo * a, b;我爱电脑技术社区--打造最好的电脑技术自学交流平台9 W; w% {- i. `* s+ m: K. t
我爱电脑技术社区--打造最好的电脑技术自学交流平台1 u5 W& W" ?/ y- x
        这里a被定义为一个结构指针,但b被定义为一个结构(而不是指针)。相反,第二个声明中c和d都被定义为指向结构的指针,因为T2的行为好像真正的类型一样。7 r3 E. k% S( x, ?  X' V, }7 m

- c3 F! h: u8 @( z' @9 H% S+ q我爱电脑技术社区--打造最好的电脑技术自学交流平台7 可移植性缺陷
) v& U. V! J; \" m电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站' `1 N2 Q9 G. V  J5 t. k% x
    C被很多人实现并运行在很多机器上。这也正是在一个地方写的C程序应该能够很容易地转移到另一个编程环境中去的原因。然而,由于有很多的实现者,它们并不和其他人交流。此外,不同的系统有不同的需求,因此一台机器上的C实现和另一台上的多少会有些不同。由于很多早期的C实现都关系到UNIX操作系统,因此这些函数的性质都是专于该系统的。当一些人开始在其他系统中实现C时,他们尝试使库的行为类似于UNIX系统中的行为。但他们并不总是能够成功。更有甚者,很多人从UNIX系统的不同版本入手,一些库函数的本质不可避免地发生分歧。今天,一个C程序员如果想写出对于不同环境中的用户都有用的程序就必须知道很多这些细微的差别。我爱电脑技术论坛3 Z# ]' g% p9 [; B% L

" y" I% j: ~9 z$ A我爱电脑技术社区--打造最好的电脑技术自学交流平台7.1 一个名字中都有什么?打造最好的电脑自学交流论坛; S  s' N# d! w, U1 G

, i, r; H4 K% u我爱电脑技术社区--打造最好的电脑技术自学交流平台    一些C编译器将一个标识符中的所有字符视为签名。而另一些在存贮标识符是会忽略一个极限之外的所有字符。C编译器产生的目标程序同将要被加载器进行处理以访问库中的子程序。加载器对于它们能够处理的名字通常应用自己的约束。一个常见的加载器约束是所有的外部名字必须只能是大写的。面对这样的加载器约束,C实现者会强制要求所有的外部名字都是大写的。这种约束在C语言参考手册中第2.1节由所描述。一个标识符是一个字符和数字序列,第一个字符必须是一个字母。下划线_算作字母。大写字母和小写字母是不同的。只有前八个字符是签名,但可以使用更多的字符。可以被多种汇编器和加载器使用的外部标识符,有着更多的限制: 这里,参考手册中继续给出了一些例子如有些实现要求外部标识符具有单独的大小写格式、或者少于八个字符、或者二者都有。正因为所有这些,在一个希望可以移植的程序中小心地选择标识符是很重要的。为两个子程序选择print_fields和print_float这样的名字不是个好办法。考虑下面这个显著的函数:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站8 A- _6 k: L$ d0 w

, n; [2 [3 B! g& F+ {+ ]$ E) l我爱电脑技术社区--