收藏 分享(赏)

C语言缺陷与陷阱(学习笔记).doc

上传人:weiwoduzun 文档编号:2707384 上传时间:2018-09-25 格式:DOC 页数:18 大小:48.08KB
下载 相关 举报
C语言缺陷与陷阱(学习笔记).doc_第1页
第1页 / 共18页
C语言缺陷与陷阱(学习笔记).doc_第2页
第2页 / 共18页
C语言缺陷与陷阱(学习笔记).doc_第3页
第3页 / 共18页
C语言缺陷与陷阱(学习笔记).doc_第4页
第4页 / 共18页
C语言缺陷与陷阱(学习笔记).doc_第5页
第5页 / 共18页
点击查看更多>>
资源描述

1、C 语 言 缺 陷 与 陷 阱 ( 笔 记 )C 语言像一把雕刻刀,锋利,并且在技师手中非常有用。和任何锋利的工具一样,C 会伤到那些不能掌握它的人。本文介绍 C 语言伤害粗心的人的方法,以及如何避免伤害。第一部分研究了当程序被划分为记号时会发生的问题。第二部分继续研究了当程序的记号被编译器组合为声明、表达式和语句时会出现的问题。第三部分研究了由多个部分组成、分别编译并绑定到一起的 C 程序。第四部分处理了概念上的误解:当一个程序具体执行时会发生的事情。第五部分研究了我们的程序和它们所使用的常用库之间的关系。在第六部分中,我们注意到了我们所写的程序也许并不是我们所运行的程序;预处理器将首先运行

2、。最后,第七部分讨论了可移植性问题:一个能在一个实现中运行的程序无法在另一个实现中运行的原因。词法分析器(lexical analyzer):检查组成程序的字符序列,并将它们划分为记号(token)一个记号是一个由一个或多个字符构成的序列,它在语言被编译时具有一个(相关地)统一的意义。C 程序被两次划分为记号,首先是预处理器读取程序,它必须对程序进行记号划分以发现标识宏的标识符。通过对每个宏进行求值来替换宏调用,最后,经过宏替换的程序又被汇集成字符流送给编译器。编译器再第二次将这个流划分为记号。1.1= 不是 =:C 语言则是用=表示赋值而用=表示比较。这是因为赋值的频率要高于比较,因此为其分

3、配更短的符号。C还将赋值视为一个运算符,因此可以很容易地写出多重赋值(如 a = b = c),并且可以将赋值嵌入到一个大的表达式中。1.2 表示*g()和(*h)()都是 float 表达式。由于()比*绑定得更紧密,*g()和*(g()表示同样的东西:g 是一个返回指 float 指针的函数,而 h 是一个指向返回 float 的函数的指针。当我们知道如何声明一个给定类型的变量以后,就能够很容易地写出一个类型的模型(cast):只要删除变量名和分号并将所有的东西包围在一对圆括号中即可。float *g();声明 g 是一个返回 float 指针的函数,所以(float *()就是它的模型。

4、(*(void(*)()0)();硬件会调用地址为 0 处的子程序(*0)(); 但这样并不行,因为*运算符要求必须有一个指针作为它的操作数。另外,这个操作数必须是一个指向函数的指针,以保证*的结果可以被调用。需要将 0 转换为一个可以描述“指向一个返回 void 的函数的指针”的类型。(Void(*)()0在这里,我们解决这个问题时没有使用 typedef 声明。通过使用它,我们可以更清晰地解决这个问题:typedef void (*funcptr)();/ typedef funcptr void (*)();指向返回 void 的函数的指针(*(funcptr)0)();/调用地址为 0

5、 处的子程序2.2 运算符并不总是具有你所想象的优先级绑定得最紧密的运算符并不是真正的运算符:下标、函数调用和结构选择。这些都与左边相关联。接下来是一元运算符。它们具有真正的运算符中的最高优先级。由于函数调用比一元运算符绑定得更紧密,你必须写(*p)()来调用 p 指向的函数;*p()表示 p 是一个返回一个指针的函数。转换是一元运算符,并且和其他一元运算符具有相同的优先级。一元运算符是右结合的,因此*p+表示*(p+),而不是(*p)+。在接下来是真正的二元运算符。其中数学运算符具有最高的优先级,然后是移位运算符、关系运算符、逻辑运算符、赋值运算符,最后是条件运算符。需要记住的两个重要的东西

6、是:1. 所有的逻辑运算符具有比所有关系运算符都低的优先级。2. 移位运算符比关系运算符绑定得更紧密,但又不如数学运算符。乘法、除法和求余具有相同的优先级,加法和减法具有相同的优先级,以及移位运算符具有相同的优先级。还有就是六个关系运算符并不具有相同的优先级:=和!=的优先级比其他关系运算符要低。在逻辑运算符中,没有任何两个具有相同的优先级。按位运算符比所有顺序运算符绑定得都紧密,每种与运算符都比相应的或运算符绑定得更紧密,并且按位异或()运算符介于按位与和按位或之间。三元运算符的优先级比我们提到过的所有运算符的优先级都低。这个例子还说明了赋值运算符具有比条件运算符更低的优先级是有意义的。另外

7、,所有的复合赋值运算符具有相同的优先级并且是自右至左结合的具有最低优先级的是逗号运算符。赋值是另一种运算符,通常具有混合的优先级。2.3 看看这些分号!或者是一个空语句,无任何效果;或者编译器可能提出一个诊断消息,可以方便除去掉它。一个重要的区别是在必须跟有一个语句的 if 和 while 语句中。另一个因分号引起巨大不同的地方是函数定义前面的结构声明的末尾,考虑下面的程序片段:struct foo int x;f() .在紧挨着 f 的第一个后面丢失了一个分号。它的效果是声明了一个函数 f,返回值类型是 struct foo,这个结构成了函数声明的一部分。如果这里出现了分号,则 f 将被定义

8、为具有默认的整型返回值5。2.4 switch 语句C 中的 case 标签是真正的标签:控制流程可以无限制地进入到一个 case 标签中。看看另一种形式,假设 C 程序段看起来更像 Pascal:switch(color) case 1: printf (“red“);case 2: printf (“yellow“);case 3: printf (“blue“);并且假设 color 的值是 2。则该程序将打印 yellowblue,因为控制自然地转入到下一个 printf()的调用。这既是 C 语言 switch 语句的优点又是它的弱点。说它是弱点,是因为很容易忘记一个 break 语

9、句,从而导致程序出现隐晦的异常行为。说它是优点,是因为通过故意去掉 break 语句,可以很容易实现其他方法难以实现的控制结构。尤其是在一个大型的 switch 语句中,我们经常发现对一个 case 的处理可以简化其他一些特殊的处理。2.5 函数调用和其他程序设计语言不同,C 要求一个函数调用必须有一个参数列表,但可以没有参数。因此,如果 f 是一个函数,f();就是对该函数进行调用的语句,而f;什么也不做。它会作为函数地址被求值,但不会调用它6。2.6 悬挂 else 问题一个 else 总是与其最近的 if 相关联。3 连接一个 C 程序可能有很多部分组成,它们被分别编译,并由一个通常称为

10、连接器、连接编辑器或加载器的程序绑定到一起。由于编译器一次通常只能看到一个文件,因此它无法检测到需要程序的多个源文件的内容才能发现的错误。3.1 你必须自己检查外部类型假设你有一个 C 程序,被划分为两个文件。其中一个包含如下声明:int n;而令一个包含如下声明:long n;这不是一个有效的 C 程序,因为一些外部名称在两个文件中被声明为不同的类型。然而,很多实现检测不到这个错误,因为编译器在编译其中一个文件时并不知道另一个文件的内容。因此,检查类型的工作只能由连接器(或一些工具程序如 lint)来完成;如果操作系统的连接器不能识别数据类型,C 编译器也没法过多地强制它。那么,这个程序运行

11、时实际会发生什么?这有很多可能性:1. 实现足够聪明,能够检测到类型冲突。则我们会得到一个诊断消息,说明 n 在两个文件中具有不同的类型。2. 你所使用的实现将 int 和 long 视为相同的类型。典型的情况是机器可以自然地进行 32 位运算。在这种情况下你的程序或许能够工作,好象你两次都将变量声明为 long(或 int)。但这种程序的工作纯属偶然。3. n 的两个实例需要不同的存储,它们以某种方式共享存储区,即对其中一个的赋值对另一个也有效。这可能发生,例如,编译器可以将 int 安排在 long 的低位。不论这是基于系统的还是基于机器的,这种程序的运行同样是偶然。4. n 的两个实例以

12、另一种方式共享存储区,即对其中一个赋值的效果是对另一个赋以不同的值。在这种情况下,程序可能失败。这种情况发生的另一个例子出奇地频繁。程序的某一个文件包含下面的声明:char filename = “etc/passwd“;而另一个文件包含这样的声明:char *filename;尽管在某些环境中数组和指针的行为非常相似,但它们是不同的。在第一个声明中,filename 是一个字符数组的名字。尽管使用数组的名字可以产生数组第一个元素的指针,但这个指针只有在需要的时候才产生并且不会持续。在第二个声明中,filename 是一个指针的名字。这个指针可以指向程序员让它指向的任何地方。如果程序员没有给它

13、赋一个值,它将具有一个默认的 0 值(NULL)(译注实际上,在 C 中一个为初始化的指针通常具有一个随机的值,这是很危险的!)。这两个声明以不同的方式使用存储区,它们不可能共存。避免这种类型冲突的一个方法是使用像 lint 这样的工具(如果可以的话)。为了在一个程序的不同编译单元之间检查类型冲突,一些程序需要一次看到其所有部分。典型的编译器无法完成,但 lint 可以。避免该问题的另一种方法是将外部声明放到包含文件中。这时,一个外部对象的类型仅出现一次7。4 语义缺陷4.1 表达式求值顺序一些 C 运算符以一种已知的、特定的顺序对其操作数进行求值。但另一些不能。例如,考虑下面的表达式:a 1

14、 的值,这是不可能为 0 的。译注:(-1) / 2 的结果是 0。5 库函数5.1 getc()返回整数考虑下面的程序:#include main() char c;/int c;while(c = getchar() != EOF)putchar(c);这段程序看起来好像要将标准输入复制到标准输出。实际上,它并不完全会做这些。原因是 c 被声明为字符而不是整数。这意味着它将不能接收可能出现的所有字符包括 EOF。因此这里有两种可能性。有时一些合法的输入字符会导致 c 携带和 EOF 相同的值,有时又会使 c 无法存放 EOF 值。在前一种情况下,程序会在文件的中间停止复制。在后一种情况下,

15、程序会陷入一个无限循环。实际上,还存在着第三种可能:程序会偶然地正确工作。C 语言参考手册严格地定义了表达式(c = getchar() != EOF)的结果。其 6.1 节中声明:当一个较长的整数被转换为一个较短的整数或一个 char 时,它会被截去左侧;超出的位被简单地丢弃。7.14 节声明:存在着很多赋值运算符,它们都是从右至左结合的。它们都需要一个左值作为左侧的操作数,而赋值表达式的类型就是其左侧的操作数的类型。其值就是已经赋过值的左操作数的值。这两个条款的组合效果就是必须通过丢弃 getchar()的结果的高位,将其截短为字符,之后这个被截短的值再与 EOF 进行比较。作为这个比较的

16、一部分,c 必须被扩展为一个整数,或者采取将左侧的位用 0填充,或者适当地采取符号扩展。然而,一些编译器并没有正确地实现这个表达式。它们确实将 getchar()的值的低几位赋给 c。但在c 和 EOF 的比较中,它们却使用了 getchar()的值!这样做的编译器会使这个事例程序看起来能够“正确地”工作。5.2 缓冲输出和内存分配立即安排输出的显示通常比将其暂时保存在一大块一起输出要昂贵得多。因此,C 实现通常允许程序员控制产生多少输出后在实际地写出它们。这个控制通常约定为一个称为 setbuf()的库函数。如果 buf 是一个具有适当大小的字符数组,则setbuf(stdout, buf)

17、;将告诉 I/O 库写入到 stdout 中的输出要以 buf 作为一个输出缓冲,并且等到 buf 满了或程序员直接调用 fflush()再实际写出。缓冲区的合适的大小在中定义为 BUFSIZ。因此,下面的程序解释了通过使用 setbuf()来讲标准输入复制到标准输出:#include main() int c;char bufBUFSIZ;setbuf(stdout, buf);while(c = getchar() != EOF)putchar(c);不幸的是,这个程序是错误的,因为一个细微的原因。要知道毛病出在哪,我们需要知道缓冲区最后一次刷新是在什么时候。答案;主程序完成之后,库将控制

18、交回到操作系统之前所执行的清理的一部分。在这一时刻,缓冲区已经被释放了!有两种方法可以避免这一问题。首先,使用静态缓冲区,或者将其显式地声明为静态:static char bufBUFSIZ;或者将整个声明移到主函数之外。另一种可能的方法是动态地分配缓冲区并且从不释放它:char *malloc();setbuf(stdout, malloc(BUFSIZ);注意在后一种情况中,不必检查 malloc()的返回值,因为如果它失败了,会返回一个空指针。而setbuf()可以接受一个空指针作为其第二个参数,这将使得 stdout 变成非缓冲的。这会运行得很慢,但它是可以运行的。6 预处理器6.1

19、宏不是函数由于宏可以象函数那样出现,有些程序员有时就会将它们视为等价的。因此,看下面的定义:#define max(a, b) (a) (b) ? (a) : (b)注意宏体中所有的括号。它们是为了防止出现 a 和 b 是带有比优先级低的表达式的情况。一个重要的问题是,像 max()这样定义的宏每个操作数都会出现两次并且会被求值两次。因此,在这个例子中,如果 a 比 b 大,则 a 就会被求值两次:一次是在比较的时候,而另一次是在计算 max()值的时候。这不仅是低效的,还会发生错误:biggest = x0;i = 1;while(i (xi+) ? (biggest) : (xi+);首先

20、,biggest 与 xi+进行比较。由于 i 是 1 而 x1是 3,这个关系是“假”。其副作用是,i 增长到 2。由于关系是“假”,xi+的值要赋给 biggest。然而,这时的 i 变成 2 了,因此赋给 biggest的值是 x2的值,即 1。避免这些问题的方法是保证 max()宏的参数没有副作用:biggest = x0;for(i = 1; i _cnt = 0 ? (*(p)-_ptr+ = (x) : _flsbuf(x, p)putc()的第一个参数是一个要写入到文件中的字符,第二个参数是一个指向一个表示文件的内部数据结构的指针。注意第一个参数完全可以使用如*z+之类的东西,

21、尽管它在宏中两次出现,但只会被求值一次。而第二个参数会被求值两次(在宏体中,x 出现了两次,但由于它的两次出现分别在一个:的两边,因此在 putc()的一个实例中它们之中有且仅有一个被求值)。由于 putc()中的文件参数可能带有副作用,这偶尔会出现问题。不过,用户手册文档中提到:“由于 putc()被实现为宏,其对待 stream 可能会具有副作用。特别是 putc(c, *f+)不能正确地工作。”但是 putc(*c+, f)在这个实现中是可以工作的。有些 C 实现很不小心。例如,没有人能正确处理 putc(*c+, f)。另一个例子,考虑很多 C 库中出现的 toupper()函数。它将

22、一个小写字母转换为相应的大写字母,而其它字符不变。如果我们假设所有的小写字母和所有的大写字母都是相邻的(大小写之间可能有所差距),我们可以得到这样的函数:toupper(c) if(c = a if(biggest 0。1. 最重要的,我们期望 q * b + r = a,因为这是对余数的定义。2. 如果 a 的符号发生改变,我们期望 q 的符号也发生改变,但绝对值不变。3. 我们希望保证 r = 0 且 r -HASHSIZE,因此我们可以写:h = n % HASHSIZE;if(n = a 而不用担心调用 free()会导致 p-next 不可用。不用说,这种技术是不推荐的,因为不是所有

23、 C 实现都能在内存被释放后将它的内容保留足够长的时间。然而,第七版的手册遗留了一个未声明的问题:realloc()的原始实现实际上是必须要先释放再重新分配的。出于这个原因,一些 C 程序都是先释放内存再重新分配的,而当这些程序移植到其他实现中时就会出现问题。7.9 可移植性问题的一个实例下面的程序带有两个参数:一个长整数和一个函数(的指针)。它将整数转换位十进制数,并用代表其中每一个数字的字符来调用给定的函数。void printnum(long n, void (*p)() if(n = 10)printnum(n / 10, p);(*p)(n % 10 + 0);这个程序非常简单。首先

24、检查 n 是否为负数;如果是,则打印一个符号并将 n 变为正数。接下来,测试是否 n = 10。如果是,则它的十进制表示中包含两个或更多个数字,因此我们递归地调用 printnum()来打印除最后一个数字外的所有数字。最后,我们打印最后一个数字。这个程序由于它的简单具有很多可移植性问题。首先是将 n 的低位数字转换成字符形式的方法。用 n % 10 来获取低位数字的值是好的,但为它加上0来获得相应的字符表示就不好了。这个加法假设机器中顺序的数字所对应的字符数顺序的,没有间隔,因此0 + 5 和5的值是相同的,等等。尽管这个假设对于 ASCII 和 EBCDIC 字符集是成立的,但对于其他一些机

25、器可能不成立。避免这个问题的方法是使用一个表:void printnum(long n, void (*p)() if(n = 10)printnum(n / 10, p);(*p)(“0123456789“n % 10);另一个问题发生在当 n 0) r -= 10;q+;if(n = -10) printneg(q, p);(*p)(“0123456789“-r);8 这里是空闲空间参考The C Programming Language(Kernighan and Ritchie, Prentice-Hall 1978)是最具权威的C 著作。它包含了一个优秀的教程,面向那些熟悉其他高级语

26、言程序设计的人,和一个参考手册,简洁地描述了整个语言。尽管自 1978 年以来这门语言发生了不少变化,这本书对于很多主题来说仍然是个定论。这本书同时还包含了本文中多次提到的“C 语言参考手册”。The C Puzzle Book(Feuer, Prentice-Hall, 1982)是一本少见的磨炼人们文法能力的书。这本书收集了很多谜题(和答案),它们的解决方法能够测试读者对于 C 语言精妙之处的知识。C: A Referenct Manual(Harbison and Steele, Prentice Hall 1984)是特意为实现者编写的一本参考资料。其他人也会发现它是特别有用的因为他能从中参考细节。1.这本书是基于图书C Traps and Pitfalls(Addison-Wesley, 1989, ISBN 0-201-17928-8)的一个扩充,有兴趣的读者可以读一读它。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 企业管理 > 管理学资料

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报