![[报告精品]《C语言陷阱》简体中文版_第1页](http://file2.renrendoc.com/fileroot_temp3/2021-4/16/1140ce12-7895-4cf0-9a23-ed1c133c44c3/1140ce12-7895-4cf0-9a23-ed1c133c44c31.gif)
![[报告精品]《C语言陷阱》简体中文版_第2页](http://file2.renrendoc.com/fileroot_temp3/2021-4/16/1140ce12-7895-4cf0-9a23-ed1c133c44c3/1140ce12-7895-4cf0-9a23-ed1c133c44c32.gif)
![[报告精品]《C语言陷阱》简体中文版_第3页](http://file2.renrendoc.com/fileroot_temp3/2021-4/16/1140ce12-7895-4cf0-9a23-ed1c133c44c3/1140ce12-7895-4cf0-9a23-ed1c133c44c33.gif)
![[报告精品]《C语言陷阱》简体中文版_第4页](http://file2.renrendoc.com/fileroot_temp3/2021-4/16/1140ce12-7895-4cf0-9a23-ed1c133c44c3/1140ce12-7895-4cf0-9a23-ed1c133c44c34.gif)
![[报告精品]《C语言陷阱》简体中文版_第5页](http://file2.renrendoc.com/fileroot_temp3/2021-4/16/1140ce12-7895-4cf0-9a23-ed1c133c44c3/1140ce12-7895-4cf0-9a23-ed1c133c44c35.gif)
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、1. 介绍: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 移位操作符205 库函数205
2、.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的标记符分为申明,表达式和语句时的问题。第三章确认c程序由几个单独编译
4、的部分组成。第四章提示一些错误概念:即程序还在运行时发生的状况。第五章检查程序及其调用的库函数之间的关系。第六章提醒我们所写的代码和实际运行的并不一定一致 ; 预处理程序已经先一步获得代码并做出处理。最后,第七章我们探讨可移植性问题,看哪些因素导致程序在一个平台可以正常运行而在其他平台上却得不到所要的结果。1. 词法陷阱编译器的第一个部分通常被称为词法分析器。词法分析器将程序看成一系列字符并将其分解为标记符。标记符由一个或多个在被编译程序中具有相对统一意思的字符组成。例如,在c 语言中,标识符“ - ”与组成他的两个字符之间有着异常明显的区别,且独立于上下文。另举一个例子,考虑以下表述: if
5、(x big)big = x ;在这个表述中,除关键词 if 和标识符big的两个实例之外,每个非空的字符都是一个独立的标识符。实际上c 语言分两步被分解为标记符。首先经过预处理程序。它必须将程序标识符化以找到定义,有些定义还以宏的形式体现。接着对宏求值并进行替换。最后,宏替换的结果再编译形成编译器专用的字符流。编译器接着第二次将该字符流分解为标记符。 在这一章节中我们将浏览常见的一些对标记符错误理解以及标记符与组成它的字符之间的区别。最后将我们谈谈预处理的问题。1.1 = 不是 = =起源于algol的编程语言如pascal和ada用“:=”表示赋值,用“=”表示比较。然而,c使用“ =”
6、表示赋值,用“= =”表示比较。这是因为赋值使用的频率更多,所以将它用较短的符号表示。此外,c 把赋值看成是一种操作,故可以很容易的写出如“a=b=c”之类的多重赋值,且赋值能够包含到非常复杂的表达式中。这种便利导致一些潜在的问题:人们可能不经意的于计划比较处进行赋值操作。比如以下表达式,看起来好像是在比较 x 和 y 是否相等:非0-1表示真,0表示假) if( x = y )foo();实际上却是将 y 的值赋给 x ,并判断该值是否非 0 。看一下下面的循环,它试图在一个文件中跳过空格,tab,换行符: while( c = = | c = t | c = =n)c= getc(f);程
7、序员在比较 t 时错用了=。这个“比较”实际上将t赋给 c 并将这个新值与 0 进行比较。因为 t 非 0,这个比较会一直为真,故这个循环会持续到整个文件结束。此后的事情将取决于是否允许程序在读到文件末尾后继续读文件。如果允许,该循环将成为一个死循环。一些 c 编译器尝试帮助用户于 “e1 = e2”之类的情况下提供告警信息。当你需要将一个值赋给一个变量,然后判断其值是否为零时 ,为避免告警信息可考虑将这个比较表达的清晰些。比如将:if ( x = y )foo();这句话写成:if (x=y)! =0)foo ();当然,这也会令你的意图更为简明。1.2 &和| 不是 &和 | 因为很多高级
8、语言以 = 作为比较的关键词,导致我们非常容易于无意中以 = 取代= =。& 和&,| 和 | |也会出现这种情况,尤其是在 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 divisor */;都会按照注释中的意思进行除法运算。
10、此类含糊不清在其他的上下文中也会导致麻烦。例如,老版本的c中用 = + 表示现在的 + = 。这样的编译器会把 a=-1;看成是a =- 1;即a = a - 1;这会令一个预期执行 a = -1;的程序员感到惊奇。另外,尽管 /* 看起来象注释,老版本的 c编译器会把a=/*b;解释为a=/ * b;1.4 例外复合赋值操作如 + = 是真正的多重记号。即,a + /* strange */ = 1;表示的意思同以下:a + = 1;这些操作符是唯一的,只有他们才可能看起来是单独的记号,而实际上却是多重记号。而p - a ;就是非法的。它并非p - a ;的同义字。另举一个例子,操作符 是一
11、个单独的记号,所以 = 是由两个记号而不是三个组成的。另一方面,那些视 =+ 为 += 的老编译器将 =+ 当作单独的记号。1.5 字符串和字符 单引号和双引号在 c 中表示的意思截然不同,在某些上下文中,混淆它们所引发的结果会比错误信息更令人吃惊。字符使用单引号,它只是整数的另一种写法。该整数的值为在符号表中与字符相应的序号。因此,在ascii序列中 a 表示0141或者97。另一方面,字符串使用双引号,提供一种简洁的写法,将一个指针指向一个未命名的数组,该数组被初始化为双引号之间的字符和一个额外的值为 0 的字符。下面的两个程序片断表示相同意思:printf(“hello worldn”)
12、;char hello=h,e,l,l,o, ,w,o,r,l,d,n,0;printf (hello); 用指针代替整数或其它常常引发告警信息,故以双引号替代单引号的错误通常可以被编译器发现。然而在函数调用时有一个例外,此处多数编译器并不检查参数类型。因此,以printf(n);代替printf (n);找一个严格的工具,只允许一种格式存在,相当于pascal。在编译时并不会报错,在运行时却会引发令人惊讶的结果。 因为整型数通常都足够大,可以同时表示几个字符,所以有的 c 编译器允许字符常量中有多个字符。这表明当用yes替代“yes”时可能不会被发现。后者表示四个分别装有y,e,s,null
13、字符的连续地址空间的首地址。前者表示一个由y,e,s以某种方式定义的字符的整数。在这两个变量之间的任何相似之处都只是一种巧合。2 语法陷阱要去深入理解一段程序,仅仅了解组成这些语句的的记号是不够的,你必须要理解这些记号是怎样相互配合来构成变量定义和声明,最后来组合成一段程序的。在本章,我们来讨论一些不太清晰的语法构造。2.1 理解声明我曾经和一个人谈论过这方面的问题,他当时正在写一个运行在单个微处理器上的程序,当这台机器启动时,硬件会自动去调用存储在地址为0的这段程序。为了仿真机器的启动过程,我们必须要去设计一段程序去显式调用这个子程序,经过讨论,采用如下语句来执行:(* (void (*)(
14、 ) ) 0)( );象这样的表达式对于c程序员来说简直是一个噩梦。实际上,他们不必害怕,有一个简单的原则:declare it the way you use it ,可以很容易的解释清楚。为了理解这条语句,我们先看一些简单的c语法。每个c变量的声明包括两个部分,一部分是类型表示符,另一部分是对应此类型的变量表达式,最简单的表达式就是一个变量,如下所示:floatf, g;在此表达式中,f 和g 的类型是float类型。由于可以用一个表达式进行变量定义,因此可以自由使用(),如:float ( ( f ) ) ; 定义( ( f ) )这个表达式的计算结果是一个float型数,因此f 也是一
15、个float型数。同样原理适用于函数和指针类型,看下面例子:float ff ( );表示表达式ff ( )是一个float数,因此 ff 是一个返回float的函数。与之类似:float *pf;意味着*pf 是一个浮点数,因此 pf 是一个指向float数的指针。根据上面表达式的组合方法,下面语句:float *g( ), (*h)( );表明*g( ) 和 (*h)( ) 都是 float 型表达式,因为()的运算优先级比*高,因此*g( ) 等同于*(g( ),g是一个返回float型指针的函数,h是一个返回float型变量的函数指针。一旦知道怎样去定义一个指定类型的变量,那就很容易写
16、出此指定类型的原型,只要从定义表达式中去掉变量名和分号,然后将整个语句用()包含就行了。因而语句float *g( );定义g为一个返回float型指针的函数,则(float *()就是此类函数的原型。掌握以上原理后,我们来分析(*(void (*)()0)()语句。我们可以把此条声明分作两部分,首先我们假设有一函数指针变量fp,当想去调用由fp所指向的函数时,可以用以下的语句:(*fp)();fp是一函数指针,*fp就是函数本身,则(*fp)()就是调用此函数的方法,在(*fp)中()是必须的,如果去掉,表达式就等同于*(fp()。现在我们把问题简化成如何去寻找一个合适的表达式来代替fp。现
17、在来进一步分析以上的问题,假设c编译器能理解我们的意图,则以上的语句可以写成:(*0)();但这条语句是不能工作的,因为*操作符坚持要用指针变量作为它的操作数,而且此操作数必须是一个函数指针,这样加上*操作符后才能够被调用。因此我们必须将0转换成一个指向返回void型的函数指针。如果fp是一个指向void型函数的指针,则(*fp)()是一个void型的数,它的声明形式应当为:void (*fp)();一旦知道怎样声明一个变量,我们就能够将一个常量转换为此类型:只要在此常量前加上变量类型的声明即可。因此我们可以用如下的方式将0转换成一个指向返回void型的函数指针:(void(*)()0现在可以
18、用(void(*)()0来代替fp进行函数调用,如下所示:(*(void(*)()0)();到这里问题就讨论结束了,另外还有一种更清晰方法来实现上面的功能,使用“typedef”出定义一种新的变量类型:typedef void (*funcptr)();(* (funcptr) 0)();2.2 操作符不具有你所期望的优先级假设flag是一个int型的常量,其中有一bit为1,当想去检测一int型的变量flags的对应bit是否为1,通常采用以下方法:if (flags & flag) .这条语句检查位于()中的运算表达式是否为0,这对于大多数c程序员来说是比较简单的。但采用以下方式可以使检测
19、语句更为清晰: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;不幸的是,这条语句也是错误的。因为+的运算优先级比 的高,因此上面的语句等效为:+的优先级也r = h (4 + l);有两个方法去实现正确的功能:r =
20、(h 4) + l;r = h ),它们的结合方向是自左向右。接着就是一元运算符,它们在参与运算的操作符中具有最高优先级。因为函数调用的优先级高于一元运算符,因此当p为一函数指针时,必须用(*p)( )来调用函数,*p( )则表明p为返回一指针的函数。一元运算符的结合方向是自右向左,因此*p+应于*(p+)相同,而不与(*p)+相同。再接着就是二元运算符,算术运算符在里面具有最高的优先级,再下面是移位运算符,关系运算符,逻辑运算符,条件运算符,赋值运算符,最后是逗号运算符,当判断这些运算符的优先级时,有两点要记住:1、每个逻辑运算符的优先级低于关系运算符的优先级2、移位运算符的优先级高于关系运
21、算符,但是低于算术运算符在不同运算符类别中,没有特别需要注意的地方。乘法、除法、取余具有相同的优先级,加法、减法具有相同的优先级,两个移位运算符具有相同的优先级。有一点要注意的是六个关系运算符具有不相同的优先级:“=”和“!=的优先级比其他四个要低。因此下面的表达式的意思是判断a和b比较结果和c和d的比较结果是否相等。a b = c d (ab)=(c三元条件运算符的优先级比上面提及的运算符的都要低,这就允许在选择表达式中有关系运算符的逻辑组合,如下所示:z = a b & b c ? d : eif(a&(bc) z=d; else z=e; 上面语句同时也说明了赋值运算符的优先级比关系运算
22、符的低,而且所有的复合赋值运算符具有相同的优先级,并且它们的运算方向是从右到左。因此:a = b = c等同于b = c; a = b;优先级最低的是逗号运算符,这比较容易理解,因为当一条语句由多个表达式组成时,逗号在这里相当于分号的功能。在混合优先级判断中,赋值运算符是比较棘手的。考虑下面的例子,它执行的功能是拷贝一个文件:while (c=getc(in) != eof)putc(c,out);“while”语句中要实现的功能是给变量c赋值,然后与eof进行比较来终止循环,不幸的是,赋值操作的优先级低于比较操作的优先级,因此c的值是getc(in)与eof比较的结果,getc(in)的值将
23、被弃掉,因此拷贝生成的文件将是一连串的1。想实现以上的功能并不困难,可以简单修改如下:while (c=getc(in) != eof)putc(c,out);然而,在复杂语句中,这种优先级混淆的问题是很难被发现的。在unix系统下面的几个不同版本的连接程序中曾经出现过如下的错误语句:if( (t=btype(pt1-aty)=strty) | t=unionty )此条语句要实现的功能是给变量t赋值,然后判断是否与strty相等或者与unionty相等,但是这条语句真正实现的功能并不是这样。c语言中,逻辑运算符的优先级分配有其历史的原因。b语言,也就是c语言的前身,也有相当于c语言中的 &
24、和 | 操作符,尽管它们被定义用作位运算符,但是当用于条件上下文时,编译器会自动将它们当作 & 和 | 运算。2.3 分号的用法语句中一个额外的分号通常会产生小的分歧:他可以是一条不产生任何影响的空语句,或者是用于使编译器产生一个诊断信息,使之容易去掉。然而如果在if 或while语句后面加一个分号,会产生严重的歧义。看下面的例子:if (xi big);big = xi;第一行后面的分号编译时能够通过,但这段程序的功能跟下面的程序完全不同:if (xi big)big = xi;前面的那段程序相当于:if (xi big) big = xi;当x、i、big不是宏,不具有其他影响时,可以再简
25、化为:big = xi;另外一个能产生重大歧义的地方是在一个函数的定义前面有一个类型的声明。看下面的一段程序:struct foo int x;f(). . .在第一个后面忘记写了一个分号,然后函数f就紧跟在后面定义,那这样就定义了函数 f 返回结构体foo的一个变量。假如分号存在的话,则 f 返回的是缺省的int型数。2.4 switch 语句在c语言中,switch语句中的各个case分支能够相互作用,这和别的程序语言不同。现在看一下c和pascal的两个例子:switch (color) case 1: printf (red);break;case 2: printf (yellow)
26、;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标号是一个真实的标号,程序可以自由的访问它。现在以另外的方法来看这个问题,下面这段程序是仿照pascal的样式写的:switch (color) case 1: printf (red);ca
27、se 2: printf (yellow);case 3: printf (blue);现在假设color的值等于2,那样程序将打印“yellow”和“blue”。因为当程序调用完第二个printf()后,就很自然的执行它以后的程序。这既是c语言switch语句的优点,也是它的缺点。说它是缺点是因为在程序的编写中很容易漏写一个break,而这样会导致程序的错误执行,并且错误不容易发现。作为优点,你可以故意漏掉一些break语句,很乐意让多个分支共用相同的处理程序。特别是在一些具有很多case分支的程序中,你经常会发现一些case分支共用相同的处理程序。下面是一个虚拟机器的解释程序,程序里面包含
28、了一段switch语句去处理不同的指令,在这种机器上,在第二个操作数的符号反转后,加法与减法的操作是一样的,因此程序编写成下面这样是很好的:case subtract:opnd2 = -opnd2;/* no break */case add:. . .另外一个典型的例子是编辑器中的一段小程序,它实现的功能是忽略空格,得到一个记号(token),算法中空格、tab键、换行符都是同样处理,除了换行符能使行计数器加1,程序如下所示:case n:linecount+;/* no break */case t:case :. . .2.5 函数调用跟别的程序设计语言不一样,c语言要求函数调用时必须有
29、参数列表,即使此参数列表是空的。假设 f 是一个函数,则f();就是进行函数调用的语句,但是f;则不作任何事情,它表示这个函数的地址,但是并不进行函数调用。2.6 else 语句问题假如现在就结束语法陷阱的分析,而不讨论下面的问题,那语法分析将会是不完整的。尽管这个问题不是c语言中独有的,但是也需要c程序员花很长的时间来体会。看下面的这段例子程序:if (x = 0)if (y = 0) error();else z = x + y;f (&z);这段程序的意图是程序分两个分支运行,第一个分值是x=0,另外一个是x!=0。当x=0时,除了在y=0时,调用error( ),其他条件下不作任何事情
30、。当x!=0时,将使z=x+y,然后用z的地址作为参数去进行函数调用。然而上面这段程序的执行过程并不是这样,原因是有这样一个规则,else总是与它最靠近的没有匹配的if进行匹配,假设我们把上面的程序根据它的执行过程,采用缩进格式编写,程序如下所示:if (x = 0) if (y = 0)error();else z = x + y;f (&z);也就是说,当x!=0时,程序什么事情都没做。要想让程序按原来的想法执行,程序编写如下:if (x = 0)if (y = 0)error();elsez = x + y;f (&z);3 链接一个c程序通常由几个小程序组成,将这几个小程序分别编译,然
31、后通过链接程序将它们组合在一起形成一个目标代码。由于编译器每次只能编译一个文件,因此它不能立即检查需要几个源文件合在一起才有的错误。在本章,讨论几个这种类型的错误。有一个叫做lint的程序可以检查出很多这方面的错误。但是不能过分依赖于这个查错程序。3.1 你必须自己检查外部类型定义假设有一个c程序分成两个文件,一个文件包含如下的定义:int n;另外一个文件包号如下定义:long n;这是一个非法的c程序,因为同名的一个外部变量在两个文件中被声明称两种不同的类型,编译程序将检测不到这个错误,因为当编译器在编译一个文件时,并不知道另外一个文件的内容。这就需要用linker(或一些程序像lint)
32、来检查这种类型不一致的错误。当这个程序运行时,到底有什么发生呢?有许多可能性:1、在生成目标代码的过程中,能够检测到这种类型不统一的问题,你可以看到一个提示信息说明在两个文件中变量n的类型不一致。2、当你使用的机器是32位时,int 和 long 是同样类型,你的程序可能会工作正常,但这仅仅是一个偶然现象。3、这两个变量在内存总共享同一个地址,给一个变量赋值,此值对另外一个变量也是合法的。这是有可能发生的,例如:联结程序给int变量分配的地址正好是long变量的低地址部分,但这需要依赖于机器和操作系统,即使程序工作正常,也仅仅是一个偶然现象。4、这两个变量在内存总共享同一个地址,给一个变量赋值
33、,但此值对另外一个变量是非法的,程序将会运行失败。令人惊讶的,这种问题经常发生。如下面的例子,程序中的一个文件包含下面的一段语句:char filename = /etc/passwd;同时另外一个文件中有如下的语句:char *filename;尽管数组和指针在有些地方被同等对待,但是它们不是完全相同的。在第一条语句中,filename 被声明为一个字符串数组,尽管用数组的名字能够得到数组中的第一个元素的指针,但是这个指针并不是一个真正的指针,它只能指向固定的地址。在第二条语句中,filename 被声明为一个字符串的指针,这个指针可以指向任何地址,假设程序不给它分配地址,它会有一个缺省的值
34、等于0。filename的这两种不同的定义采用了不同的存储方法,因此不能共存。避免这种问题出现的方法之一是使用lint程序帮助查错。为了检查出这种类型冲突的问题,必须在编译当前文件时同时检查所有与之相关的文件,一般的编译器做不到这点,但是lint 可以。另外一个方法是将这些外部声明放在一个头文件中,这样外部变量的声明就只会出现一次。4、语义陷阱一条语句可能书写完全正确,也没有语法错误,但可能是毫无意义的。在本章中,我们将列举一些编程范例,它们实现的功能和语义截然不同。我们还会讨论那些看上去很好,但将导致未知结果的例子。这里我们只讨论在任何c 环境中都不能运行的情况。关于那些只在某些c 环境中能
35、够运行的情况,将在第7章讨论,那时将会看到一些有趣的问题。4.1 表达式的运算顺序有些c 操作符按照固定的顺序计算操作数,有些c 操作符不这么做。例如,我们来看下面的表达式:a b & c d先判断 a b ,如果条件成立,再判断 c d 是否成立。但如果 a b 不成立,则 c d 根本不再进行判断。另一方面,在判断 a b 时,有些编译器先计算 a 或 b ,而有些编译器则是同时计算a和b的值。只有4个c 操作符“|”、“&”、“?:”和“,”具有固定的运算顺序。对“&”和“|”操作符,先计算左侧,有必要时才计算右侧。“?:”操作符是三目操作符: a ? b : c ,它先计算 a ,然后
36、根据 a 的值计算 b 或者 c 。而“,”操作符总是先计算左侧,不管计算结果如何,再计算右侧。 几个并列时,按左到右的顺序进行。需要注意的是,用于间隔函数参数的逗号并不是逗号操作符,例如,在函数 f( x, y ) 中可以以任意顺序提取 x 和 y 的值,但在函数 g( ( x, y ) ) 中则不然。对函数 g( ( x, y ) ) ,它只有一个参数 ( x, y ) ,它的值是这样确定的,先计算 x 的值,不管 x 为何值,再计算 y 的值。其它的c 操作符都按照不定的顺序运算,特别是赋值操作符的运算更不能保证确定的顺序。所以下面这段试图将 x 数组中的前 n 个元素拷贝到 y 数组中
37、的代码不一定成功:i = 0;while ( i n ) yi = xi+;它的问题在于没有保证在 i 进行 + 操作之前先计算 yi 的地址。在有些c 环境中可以实现,但有些c 环境则不可以。下面这段代码也有类似同样的问题:i = 0;while ( i n ) yi+ = xi;但是,下面的代码是正确的:i = 0;while ( i n ) yi = xi; i+;下面的代码不仅可以实现以上功能,而且结构简洁:for ( i = 0; i n; i+ ) yi = xi;4.2 逻辑操作符 & 、| 、!c 语言有两类操作符有时可以互换:一类是位操作符&、| 和 ;一类是逻辑操作符&、|
38、 和!。如果你用其中一类操作符代替另一类操作符,你会惊奇地发现:程序看起来没问题,可仅仅偶尔能正常工作。&、| 和 操作符把它们的操作数当作一个为序列,按位单独进行操作。比如:10 & 12 = 8,这是因为“&”操作符把 10 和 12 当作二进制描述 1010 和 1100 ,所以只有当两个操作数的相同位同时为 1 时,产生的结果中相应位才为 1 。同理,10 | 12 = 14 ( 1110 ),通过补码运算,10 = -11 ( 11.110101 )。&、| 和!操作符把它们的操作数当作“真”或“假”,并且用 0 代表“假”,任何非 0 值被认为是“真”。它们返回 1 代表“真”,0
39、 代表“假”,对于“&”和“|”操作符,如果左侧的操作数的值就可以决定表达式的值,它们根本就不去计算右侧的操作数。所以,!10 是 0 ,因为 10 非 0 ;10 & 12 是 1 ,因为 10 和 12 均非 0 ;10 | 12也是 1 ,因为 10 非 0 。并且,在最后一个表达式中,12 根本就没被计算,在表达式 10 | f( ) 中也是如此。请看下面这段程序,它想找出表中指定的元素:i = 0;while ( i tablesize & tablei != x ) i+;当循环结束时,如果 i = tablesize ,则该元素没有找到。设想一下,若“&”被无意间换为“&”,这个
40、循环看起来没错,由于以下两个特例,它也能正常运行。其一,本例中两个判断都是当条件为假时得 0 ,条件为真时得 1 。只要 x 和 y 都为 1 或 0 ,那么表达式“x & y”和“x & y”结果相同。但是,若其中一个判断用其它非 0 值而不是 1 代表“真”,这个循环将不能正常工作。其二,程序不对表中的元素进行改动,只从表外取一个元素通常不会有害。用“&”代替“&”后,程序的搜索范围可能会超过数组的界限,因为与“&”不同,“&”总是对两侧的操作数都进行计算。因此,在最后一次循环中,当i 等于 tablesize 时依然会取 tablei 的值。如果 tablesize 是表中元素的个数,那
41、么将取到表外的一个数值。4.3 下标从 0 开始在大多数语言中,对有 n 个元素的数组,元素的下标从 1 到 n 。在c 语言中并非如此。因为数组元素的下标是从 0 到 n - 1 ,所以一个包含 n 个元素的c 数组中没有下标为 n 的元素。熟悉其它语言的程序员一定要注意这一点。请看下面的程序:int i, a10;for ( i = 1; i = 10; i+ ) ai = 0;本例希望把数组 a 中的元素都赋为 0 ,却会造成意外的边界效应。因为在 for 语句中是判断 i = 10,而不是 i 10,实际不存在的第 10 个数组元素将被置 0 ,内存中 a 数组后存放的字将被破坏。编译
42、器以递减的顺序为用户变量分配内存地址,所以 a 后面的那个字变为 i 。而将 i 置为 0 将导致死循环。4.4 c 语言并不总是对实参进行匹配下面这段简单的程序将不能正常运行:double s;s = sqrt( 2 );printf( %gn, s );第一因为sqrt( )要求一个 double 型数据作为他的参数,第二因为它返回一个 double 型结果,却没有被这样声明。可以这样改正:double s, sqrt( );s = sqrt( 2 );printf( %gn, s );c 语言有两条函数参数类型转换的规则:(1)、整形数据长度小于 int 型的转换为 int 型;(2)、
43、浮点型数据长度小于 double 型的转换为 double 型。所有其它类型的数据不进行转换。程序员有责任保证函数参数的类型匹配。因此,程序员在运用如 sqrt( ) 之类要求参数为 double 型的函数时,一定要注意赋给它 float 或 double 型的参数,“2”是 int 型,类型不匹配。当在表达式中应用函数的返回值时,这个值被自动转换为合适的类型。但是编译器要首先知道函数返回值的真实类型才能做相应转换。函数的缺省返回值类型是 int 型,所以返回值为 int 型的函数可以不必声明。但上面例子中 sqrt( ) 返回值为 double 型,所以在使用前必须声明为 double 型。
44、函数缺少的返回值为int)实际上,c 语言提供了一个专门包含库函数声明的文件,在应用程序中我们可以用“include”命令把它包含进来。但是对于那些编写自己函数(特别是重要函数)的程序员,函数声明是必要的。这里有一个更好的例子:main() int i; char c; for ( i = 0; i 5; i+ ) scanf( %d, &c ); printf( %d, i ); printf( n );表面上,这个程序从标准输入设备上读入 5 个数,再把 0,1,2,3,4 写到标准输出设备上。实际并非如此,在某些编译器上,它输出0,0,0,0,0,0,1,2,3,4。为什么会这样?关键在
45、于把 c 声明为 char 型而非 int 型。当你要求 scanf 输入一个整型数据,它期待一个指向整型数据的指针。而程序中得到的是一个指向字符型数据的指针。scanf 无法告诉你它没有得到他想要的指针,他仍把输入当作一个整型指针并存放整型数据。由于整型数据比字符型数据占据更多的内存空间,所以程序占据了 c 附近的内存区。实际内存中 c 附近的数据是什么取决于编译器,本例中是 i 的低位循环部分。所以,每读一次 c 的值,i 便被置 0 。当程序执行到最后,scanf 停止向 c 赋新值,所以 i 就可以正常地递增到循环结束。4.5、指针不是数组c 程序通常用一个字符串存储一个组字符,在后面
46、加一个空字符。假设我们现在有两个字符串 s 和 t ,我们想把它们连接为字符串 r 。于是我们用了熟知的库函数 strcpy 和 strcat ,但下面这种做法是不正确的:char *r;strcpy ( r, s );strcat ( r, t );原因在于 r 没有被初始化。虽然 r 可能被潜在地定义为某一块内存区,但在你分配这块区域之前它并不存在。我们重新改过,为 r 分配内存区:char r100;strcpy ( r, s );strcat ( r, t );只要 s 和 t 不是太长就可以了。可是,c 要求定义数组长度为定值,所以不可能保证 r 足够大。但是,大多数c 环境有一个库
47、函数 malloc( ) 可以为多个字符分配大量的内存空间。也有一个我们熟知的函数 strlen( ) 可以告诉我们字符串中有多少个字符。因此,我们似乎可以这样做:char *r, *malloc();r = malloc( strlen( s ) + strlen( t ) );strcpy ( r, s );strcat ( r, t );但这样也不行,因为:第一,malloc( ) 有可能内存溢出,这时它会返回一个空指针;第二,更重要的一点是,malloc( ) 没有分配足够的内存空间。想一想上面的约定,字符串以一个空字符结束。strlen( ) 返回字符串中字符的个数,但不包含最后的空
48、字符。所以,如果 strlen(s) 为 n ,实际 s 需要 n + 1 个字符空间。所以我们必须为 r 多分配一个字符空间。所以正确的做法是这样的:char *r, *malloc();r = malloc( strlen( s ) + strlen( t ) + 1 );if ( !r ) complain(); exit( 1 );strcpy ( r, s );strcat ( r, t );4.6 避免使用提喻法提喻法是一个文学用语,类似明喻和暗喻。他准确描述了c 语言的常见陷阱:容易混淆指向数据的指针和数据本身。这种情况对于字符串更常见,例如:char *p, *q;p = xy
49、z;在赋值后把 p 的值认为是字符串 xyz 有时会有帮助,但实际上这是不正确的。理解这一点很重要。相反,p 的值是指向字符数组中第 0 个元素的指针,这个数组包含 4 个字符,它们是“x”、“y”、“z”和“0”。所以,如果我们现在让q = p;则 p 和 q 是指向同一内存地址的两个指针。内存中的字符并没有被复制,如下图所示:需要注意的一点是:复制指针并不复制指针所指向的内容。所以如果我们接着执行q1 = y;q 将指向包含字符串 xyz 的内存区。p 也一样,因为 p 和 q 指向同一片内存区。4.7 空指针并不是空字符串把一个整数转换为一个指针的结果与不同的环境有关,但有一点例外。这就
50、是常数 0 ,它一定被转换为一个与任何有效指针都不同的指针。在文档中,经常被这样描述:#define null 0其实效果是一样的。需要记住的重要的一点是:当你用 0 作为一个指针的时候,它从不需释放。换句话说,当你把 0 作为一个指针变量,你必须不让它指向任何内存。这样写是正确的:if ( p = ( char * )0 ) .而这样写是不正确的:if ( strcmp( p, ( char * )0 ) = 0 ) .因为 strcmp( ) 总是去寻找它的参数所指向的内存区。如果 p 是一个空指针,这样说也是不对的:printf( p );或printf( %s, p );4.8 整型溢
51、出c 语言定义对发生整型上溢或下溢的情况非常明确。如果每一个操作数都是无符号数,那么结果是无符号数,并被定义为以为模,这里 n 是一个字的长度。如果两个操作数都是有符号数,则结果的符号不定。我们假定 a 和 b 是两个整型变量,并且他们不是负数,现在要测试一下 a + b 是否会有溢出。最明显的方法是这样:if ( a + b 0 ) complain();通常情况下,这样是不行的。原因是一旦溢出,结果中的所有位都不是想象中的那样。比如,在有些机器上,加操作指定一个内部寄存器来标识以下四个状态之一:正、负、0 或溢出。在这样一个机器上,编译器会先加 a 和 b ,然后检查这个寄存器是否是负状态。如果操作溢出,寄存器将会处于溢出状态,而此次实验也会失败。c 语言的无符号算术运算对所有类型都具有明确定义,我们可以
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 数学-福建省莆田市2025届高中毕业班第二次教学质量检测试卷(莆田二检)试题和答案
- 2025年中考道德与法治二轮复习:热点时政专题练习题(含答案)
- 2025年中考道德与法治二轮复习:七~九年级高频考点提纲
- 刀片刺网施工方案
- 轻钢平顶施工方案
- 苗木养护施工方案
- 2025年中考物理二轮复习:简单机械、功和机械能 尖子生测试卷(含答案解析)
- 四川省金堂县2025届中考考前最后一卷生物试卷含解析
- 山西省朔州市朔城区重点名校2025届中考生物模拟试卷含解析
- 别墅房建合同范例
- 南京财经大学C语言期末(共六卷)含答案解析
- 课题申报书:极端雨雪天气下城市交通多层动态网络韧性建模及优化研究
- 2024北京东城初一(上)期末语文试卷及答案
- 2025年煤矿从业人员安全培训考试题库
- 四年级数学(四则混合运算带括号)计算题专项练习与答案
- 压铸车间生产管理制度(4篇)
- 《商务沟通-策略、方法与案例》课件 第七章 自我沟通
- 2024解析:第十二章机械效率-基础练(解析版)
- 2024年度5G基站建设材料采购合同3篇
- 危险化学品目录(2024版)
- Unit 2 Special Days(说课稿)2023-2024学年人教新起点版英语五年级下册
评论
0/150
提交评论