ImageVerifierCode 换一换
格式:DOCX , 页数:39 ,大小:53.34KB ,
资源ID:6951705      下载积分:3 金币
快捷下载
登录下载
邮箱/手机:
温馨提示:
快捷下载时,用户名和密码都是您填写的邮箱或者手机号,方便查询和重复下载(系统自动生成)。 如填写123,账号就是123,密码也是123。
特别说明:
请自助下载,系统不会自动发送文件的哦; 如果您已付费,想二次下载,请登录后访问:我的下载记录
支付方式: 支付宝    微信支付   
验证码:   换一换

加入VIP,免费下载
 

温馨提示:由于个人手机设置不同,如果发现不能下载,请复制以下地址【https://www.bdocx.com/down/6951705.html】到电脑端继续下载(重复下载不扣费)。

已注册用户请登录:
账号:
密码:
验证码:   换一换
  忘记密码?
三方登录: 微信登录   QQ登录  

下载须知

1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。
2: 试题试卷类文档,如果标题没有明确说明有答案则都视为没有答案,请知晓。
3: 文件的所有权益归上传用户所有。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 本站仅提供交流平台,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

版权提示 | 免责声明

本文(《C语言陷阱》简体中文经典版1.docx)为本站会员(b****5)主动上传,冰豆网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知冰豆网(发送邮件至service@bdocx.com或直接QQ联系客服),我们立即给予删除!

《C语言陷阱》简体中文经典版1.docx

1、C语言陷阱简体中文经典版10. 介绍:31.1 = 不是 = =41.3 多字符的标记符51.5 字符串和字符62 语法陷阱62.1 理解声明62.2 操作符不具有你所期望的优先级82.3 分号的用法102.4 Switch 语句112.5 函数调用122.6 else 语句问题123 链接133.1 你必须自己检查外部类型定义134、语义陷阱144.1 表达式的运算顺序154.2 逻辑操作符 & 、| 、!154.3 下标从 0 开始164.4 C 语言并不总是对实参进行匹配174.5、指针不是数组184.6 避免使用提喻法184.7 空指针并不是空字符串194.8 整型溢出194.9 移位

2、操作符205 库函数205.1 getc返回一个整数205.2 缓冲输出和内存分配216.1 宏不是函数226.2 宏并非类型定义247 可移植性的缺陷247.1 如何命名247.2 一个整数有多大?257.3 字符是有符号还是无符号?267.4 合适的移位是带符号还是不带符号呢?267.5 除法怎样进行?267.6 一个随机数有多大277.7 大小写转换.277.8 先释放, 再重新分配287.9 一个可移植性问题的例子.298 可利用的资源31C语言陷阱 Andrew KoenigAT&T 贝尔实验室Murray Hill ,新泽西 07974 摘要C 语言就象一把雕刻用刀-简单 ,锋利,

3、在熟手中极其有用。但象所有锋利的工具一样,C语言会伤害到那些不知驾驭的人。本文将展示C是如何伤害那些粗心的人,并将介绍如何避免这种伤害。0. 介绍:对于专家来说,C 语言易于使用和实现。C语言简洁而富于表现力。很少有约束来防止程序员犯一些粗心的错误。人们所得到的错误结果及其原因之间常常没有明显的联系。在这份文献中,我们将看到一些不可预知的结果。因其不可预知性,我们无法对其做出完备的分类。但通过监视运行C程序时那些必然发生的事情,我们竭尽全力这样做了。我们假定读者要对C语言有一定的熟悉程度。第一章的问题是当把C程序分解为标记符时所遇到的。第二章跟踪当编译器将C的标记符分为申明,表达式和语句时的问

4、题。第三章确认C程序由几个单独编译的部分组成。第四章提示一些错误概念:即程序还在运行时发生的状况。第五章检查程序及其调用的库函数之间的关系。第六章提醒我们所写的代码和实际运行的并不一定一致 ; 预处理程序已经先一步获得代码并做出处理。最后,第七章我们探讨可移植性问题,看哪些因素导致程序在一个平台可以正常运行而在其他平台上却得不到所要的结果。 1. 词法陷阱编译器的第一个部分通常被称为词法分析器。词法分析器将程序看成一系列字符并将其分解为标记符。标记符由一个或多个在被编译程序中具有相对统一意思的字符组成。例如,在C 语言中,标识符“ - ”与组成他的两个字符之间有着异常明显的区别,且独立于上下文

5、。另举一个例子,考虑以下表述: if(x big)big = x ;在这个表述中,除关键词 if 和标识符big的两个实例之外,每个非空的字符都是一个独立的标识符。实际上C 语言分两步被分解为标记符。首先经过预处理程序。它必须将程序标识符化以找到定义,有些定义还以宏的形式体现。接着对宏求值并进行替换。最后,宏替换的结果再编译形成编译器专用的字符流。编译器接着第二次将该字符流分解为标记符。 在这一章节中我们将浏览常见的一些对标记符错误理解以及标记符与组成它的字符之间的区别。最后将我们谈谈预处理的问题。1.1 = 不是 = =起源于Algol的编程语言如Pascal和Ada用“:=”表示赋值,用“

6、=”表示比较。然而,C使用“ =” 表示赋值,用“= =”表示比较。这是因为赋值使用的频率更多,所以将它用较短的符号表示。此外,C 把赋值看成是一种操作,故可以很容易的写出如“a=b=c”之类的多重赋值,且赋值能够包含到非常复杂的表达式中。这种便利导致一些潜在的问题:人们可能不经意的于计划比较处进行赋值操作。比如以下表达式,看起来好像是在比较 x 和 y 是否相等:非0-1表示真,0表示假) if( x = y ) foo();实际上却是将 y 的值赋给 x ,并判断该值是否非 0 。看一下下面的循环,它试图在一个文件中跳过空格,TAB,换行符: while( c = = | c = t |

7、c = =n) c= getc(f);程序员在比较 t 时错用了=。这个“比较”实际上将t赋给 c 并将这个新值与 0 进行比较。因为 t 非 0,这个比较会一直为真,故这个循环会持续到整个文件结束。此后的事情将取决于是否允许程序在读到文件末尾后继续读文件。如果允许,该循环将成为一个死循环。一些 C 编译器尝试帮助用户于 “e1 = e2”之类的情况下提供告警信息。当你需要将一个值赋给一个变量,然后判断其值是否为零时 ,为避免告警信息可考虑将这个比较表达的清晰些。比如将:if ( x = y ) foo();这句话写成: if (x=y)! =0) foo ();当然,这也会令你的意图更为简明

8、。1.2 &和| 不是 &和 | 因为很多高级语言以 = 作为比较的关键词,导致我们非常容易于无意中以 = 取代= =。& 和&,| 和 | |也会出现这种情况,尤其是在 C 语言中,& 和 | 不同于其他语言中相应的配对。在第四章我们将进一步分析这些操作符。1.3 多字符的记号 一些C 记号(token)如 / ,* ,和 = ,都是一个字符长。其他的如 = = ,/ * ,标识符等,都包含了不止一个字符。当编译器遇到后面跟着 * 的 / 时,它必须能够判断是将这两个字符看成分别独立的记号还是看成一个记号。C 的参考手册描述了如何进行判决:“如果输入流在一个给定的字符处已经被分解为记号,则下

9、一个记号将包含尽可能多的字符。”即,假如 / 是某个记号的第一个字符,且其后紧跟 * ,则不管上下文如何,这两个字符都表示注释的开始,下面的表述看起来象是设定 y 的值为 x 除以指针 p 所指向的值: y = x/*p /* p points to the divisor */;实际上,由于/* 表示注释开始,编译器会简单的忽略代码直至*/的出现。换句话说,上面的表达式实际上仅仅将 x 的值赋给 y ,甚至都没有引用 p。重写上述表达式为: y = x/ *p /* p points to the divisor */;或者: y = x /(*p) /* p points to the d

10、ivisor */;都会按照注释中的意思进行除法运算。此类含糊不清在其他的上下文中也会导致麻烦。例如,老版本的C中用 = + 表示现在的 + = 。这样的编译器会把 a=-1;看成是 a =- 1;即 a = a - 1;这会令一个预期执行 a = -1;的程序员感到惊奇。另外,尽管 /* 看起来象注释,老版本的 C编译器会把 a=/*b;解释为 a=/ * b;1.4 例外复合赋值操作如 + = 是真正的多重记号。即, a + /* strange */ = 1;表示的意思同以下: a + = 1;这些操作符是唯一的,只有他们才可能看起来是单独的记号,而实际上却是多重记号。而 p - a ;

11、就是非法的。它并非 p - a ;的同义字。另举一个例子,操作符 是一个单独的记号,所以 = 是由两个记号而不是三个组成的。另一方面,那些视 =+ 为 += 的老编译器将 =+ 当作单独的记号。1.5 字符串和字符 单引号和双引号在 C 中表示的意思截然不同,在某些上下文中,混淆它们所引发的结果会比错误信息更令人吃惊。字符使用单引号,它只是整数的另一种写法。该整数的值为在符号表中与字符相应的序号。因此,在ASCII序列中 a 表示0141或者97。另一方面,字符串使用双引号,提供一种简洁的写法,将一个指针指向一个未命名的数组,该数组被初始化为双引号之间的字符和一个额外的值为 0 的字符。下面的

12、两个程序片断表示相同意思: printf(“Hello Worldn”); char hello=H,e,l,l,o, ,W,o,r,l,d,n,0; printf (hello); 用指针代替整数或其它常常引发告警信息,故以双引号替代单引号的错误通常可以被编译器发现。然而在函数调用时有一个例外,此处多数编译器并不检查参数类型。因此,以 printf(n);代替 printf (n);找一个严格的工具,只允许一种格式存在,相当于Pascal。 在编译时并不会报错,在运行时却会引发令人惊讶的结果。 因为整型数通常都足够大,可以同时表示几个字符,所以有的 C 编译器允许字符常量中有多个字符。这表明

13、当用yes替代“yes”时可能不会被发现。后者表示四个分别装有y,e,s,null字符的连续地址空间的首地址。前者表示一个由y,e,s以某种方式定义的字符的整数。在这两个变量之间的任何相似之处都只是一种巧合。2 语法陷阱要去深入理解一段程序,仅仅了解组成这些语句的的记号是不够的,你必须要理解这些记号是怎样相互配合来构成变量定义和声明,最后来组合成一段程序的。在本章,我们来讨论一些不太清晰的语法构造。2.1 理解声明我曾经和一个人谈论过这方面的问题,他当时正在写一个运行在单个微处理器上的程序,当这台机器启动时,硬件会自动去调用存储在地址为0的这段程序。为了仿真机器的启动过程,我们必须要去设计一段

14、程序去显式调用这个子程序,经过讨论,采用如下语句来执行:(* (void (*)( ) ) 0)( );象这样的表达式对于C程序员来说简直是一个噩梦。实际上,他们不必害怕,有一个简单的原则:declare it the way you use it ,可以很容易的解释清楚。为了理解这条语句,我们先看一些简单的c语法。每个C变量的声明包括两个部分,一部分是类型表示符,另一部分是对应此类型的变量表达式,最简单的表达式就是一个变量,如下所示:float f, g;在此表达式中,f 和g 的类型是float类型。由于可以用一个表达式进行变量定义,因此可以自由使用(),如:float ( ( f ) )

15、 ; 定义( ( f ) )这个表达式的计算结果是一个float型数,因此f 也是一个float型数。同样原理适用于函数和指针类型,看下面例子:float ff ( );表示表达式ff ( )是一个float数,因此 ff 是一个返回float的函数。与之类似:float *pf;意味着*pf 是一个浮点数,因此 pf 是一个指向float数的指针。根据上面表达式的组合方法,下面语句:float *g( ), (*h)( );表明*g( ) 和 (*h)( ) 都是 float 型表达式,因为()的运算优先级比*高,因此*g( ) 等同于*(g( ),g是一个返回float型指针的函数,h是一

16、个返回float型变量的函数指针。一旦知道怎样去定义一个指定类型的变量,那就很容易写出此指定类型的原型,只要从定义表达式中去掉变量名和分号,然后将整个语句用()包含就行了。因而语句float *g( );定义g为一个返回float型指针的函数,则(float *()就是此类函数的原型。掌握以上原理后,我们来分析(*(Void (*)()0)()语句。我们可以把此条声明分作两部分,首先我们假设有一函数指针变量fp,当想去调用由fp所指向的函数时,可以用以下的语句:(*fp)();fp是一函数指针,*fp就是函数本身,则(*fp)()就是调用此函数的方法,在(*fp)中()是必须的,如果去掉,表达

17、式就等同于*(fp()。现在我们把问题简化成如何去寻找一个合适的表达式来代替fp。现在来进一步分析以上的问题,假设C编译器能理解我们的意图,则以上的语句可以写成:(*0)();但这条语句是不能工作的,因为*操作符坚持要用指针变量作为它的操作数,而且此操作数必须是一个函数指针,这样加上*操作符后才能够被调用。因此我们必须将0转换成一个指向返回void型的函数指针。如果fp是一个指向void型函数的指针,则(*fp)()是一个void型的数,它的声明形式应当为:void (*fp)();一旦知道怎样声明一个变量,我们就能够将一个常量转换为此类型:只要在此常量前加上变量类型的声明即可。因此我们可以用

18、如下的方式将0转换成一个指向返回void型的函数指针:(void(*)()0现在可以用(void(*)()0来代替fp进行函数调用,如下所示:(*(void(*)()0)();到这里问题就讨论结束了,另外还有一种更清晰方法来实现上面的功能,使用“typedef”出定义一种新的变量类型:typedef void (*funcptr)();(* (funcptr) 0)();2.2 操作符不具有你所期望的优先级假设FLAG是一个int型的常量,其中有一bit为1,当想去检测一int型的变量flags的对应bit是否为1,通常采用以下方法:if (flags & FLAG) .这条语句检查位于()中

19、的运算表达式是否为0,这对于大多数C程序员来说是比较简单的。但采用以下方式可以使检测语句更为清晰:if (flags & FLAG != 0) .这条语句更容易理解,但同时产生了一个错误:因为!=的优先级比&高,这上面的语句相当于:if (flags & (FLAG != 0) .程序的执行并不按照预想的进行。假设有两个int型变量h和l,他们的取值在0-15之间。当你想给一个8bit的变量r赋值,让它的低4bit与变量 l 的低4bit相同,高4bit与变量 h 的低4bit相同,一般的做法如下:r = h 4 + l;不幸的是,这条语句也是错误的。因为+的运算优先级比 的高,因此上面的语句

20、等效为:+的优先级也r = h (4 + l);有两个方法去实现正确的功能:r = (h 4) + l;r = h ),它们的结合方向是自左向右。接着就是一元运算符,它们在参与运算的操作符中具有最高优先级。因为函数调用的优先级高于一元运算符,因此当p为一函数指针时,必须用(*p)( )来调用函数,*p( )则表明p为返回一指针的函数。一元运算符的结合方向是自右向左,因此*p+应于*(p+)相同,而不与(*p)+相同。再接着就是二元运算符,算术运算符在里面具有最高的优先级,再下面是移位运算符,关系运算符,逻辑运算符,条件运算符,赋值运算符,最后是逗号运算符,当判断这些运算符的优先级时,有两点要记

21、住:1、每个逻辑运算符的优先级低于关系运算符的优先级2、移位运算符的优先级高于关系运算符,但是低于算术运算符在不同运算符类别中,没有特别需要注意的地方。乘法、除法、取余具有相同的优先级,加法、减法具有相同的优先级,两个移位运算符具有相同的优先级。有一点要注意的是六个关系运算符具有不相同的优先级:“=”和“!=的优先级比其他四个要低。因此下面的表达式的意思是判断a和b比较结果和c和d的比较结果是否相等。a b = c d (ab)=(c三元条件运算符的优先级比上面提及的运算符的都要低,这就允许在选择表达式中有关系运算符的逻辑组合,如下所示:z = a b & b c ? d : eif(a&(b

22、c) Z=d; Else Z=e; 上面语句同时也说明了赋值运算符的优先级比关系运算符的低,而且所有的复合赋值运算符具有相同的优先级,并且它们的运算方向是从右到左。因此:a = b = c等同于b = c; a = b;优先级最低的是逗号运算符,这比较容易理解,因为当一条语句由多个表达式组成时,逗号在这里相当于分号的功能。在混合优先级判断中,赋值运算符是比较棘手的。考虑下面的例子,它执行的功能是拷贝一个文件:while (c=getc(in) != EOF) putc(c,out);“while”语句中要实现的功能是给变量c赋值,然后与EOF进行比较来终止循环,不幸的是,赋值操作的优先级低于比

23、较操作的优先级,因此c的值是getc(in)与EOF比较的结果,getc(in)的值将被弃掉,因此拷贝生成的文件将是一连串的1。想实现以上的功能并不困难,可以简单修改如下:while (c=getc(in) != EOF) putc(c,out);然而,在复杂语句中,这种优先级混淆的问题是很难被发现的。在UNIX系统下面的几个不同版本的连接程序中曾经出现过如下的错误语句:if( (t=BTYPE(pt1-aty)=STRTY) | t=UNIONTY )此条语句要实现的功能是给变量t赋值,然后判断是否与STRTY相等或者与UNIONTY相等,但是这条语句真正实现的功能并不是这样。C语言中,逻辑

24、运算符的优先级分配有其历史的原因。B语言,也就是C语言的前身,也有相当于C语言中的 & 和 | 操作符,尽管它们被定义用作位运算符,但是当用于条件上下文时,编译器会自动将它们当作 & 和 | 运算。2.3 分号的用法语句中一个额外的分号通常会产生小的分歧:他可以是一条不产生任何影响的空语句,或者是用于使编译器产生一个诊断信息,使之容易去掉。然而如果在if 或while语句后面加一个分号,会产生严重的歧义。看下面的例子:if (xi big); big = xi;第一行后面的分号编译时能够通过,但这段程序的功能跟下面的程序完全不同:if (xi big) big = xi;前面的那段程序相当于:

25、if (xi big) big = xi;当x、i、big不是宏,不具有其他影响时,可以再简化为:big = xi;另外一个能产生重大歧义的地方是在一个函数的定义前面有一个类型的声明。看下面的一段程序:struct foo int x;f(). . .在第一个后面忘记写了一个分号,然后函数f就紧跟在后面定义,那这样就定义了函数 f 返回结构体foo的一个变量。假如分号存在的话,则 f 返回的是缺省的int型数。2.4 Switch 语句在C语言中,Switch语句中的各个case分支能够相互作用,这和别的程序语言不同。现在看一下C和Pascal的两个例子:switch (color) case

26、 1: printf (red);break;case 2: printf (yellow);break;case 3: printf (blue);break;case color of1: write (red);2: write (yellow);3: write (blue)end这两段程序做同样的工作:根据变量color的值为1、2、3,在当前行打印“red”、“yellow”、“blue”字符串。这两段程序非常相似,仅仅是Pascal程序没有break语句。这是因为在C语言中case标号是一个真实的标号,程序可以自由的访问它。现在以另外的方法来看这个问题,下面这段程序是仿照Pasc

27、al的样式写的:switch (color) case 1: printf (red);case 2: printf (yellow);case 3: printf (blue);现在假设color的值等于2,那样程序将打印“yellow”和“blue”。因为当程序调用完第二个printf()后,就很自然的执行它以后的程序。这既是C语言switch语句的优点,也是它的缺点。说它是缺点是因为在程序的编写中很容易漏写一个break,而这样会导致程序的错误执行,并且错误不容易发现。作为优点,你可以故意漏掉一些break语句,很乐意让多个分支共用相同的处理程序。特别是在一些具有很多case分支的程序中

28、,你经常会发现一些case分支共用相同的处理程序。下面是一个虚拟机器的解释程序,程序里面包含了一段switch语句去处理不同的指令,在这种机器上,在第二个操作数的符号反转后,加法与减法的操作是一样的,因此程序编写成下面这样是很好的:case SUBTRACT:opnd2 = -opnd2;/* no break */case ADD:. . .另外一个典型的例子是编辑器中的一段小程序,它实现的功能是忽略空格,得到一个记号(token),算法中空格、tab键、换行符都是同样处理,除了换行符能使行计数器加1,程序如下所示:case n:linecount+;/* no break */case t:case :. . .2.5 函数调用跟别的程序设计语言不一样,C语言要求函数调用时必须有参数列表,即使此参数列表是空的。假设 f 是一个函数,则f();就是进行函数调用的语句,但是f;则不作任何事情,它表示这个函数的地址,但是并不进行函数调用。2.6 else 语句

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1