《C陷阱与缺陷》学习笔记(下):连接、库函数、预处理器、可移植性缺陷及其他

11月18日

第四章  连接

  连接器并不理解C语言,然而它能理解机器语言和内存布局。作者强调连接器并不能处理连接时和C语言相关的一些错误,如果C语言提供了lint,要善加利用。

  每个外部对象都必须在程序某个地方进行定义。这就意味着如果一个程序中包括了语句extern int a;就应该在别的某个地方包括语句int a;。同时为了免两次定义同一个外部对象(无论有无初值)可能引起的错误,唯一的解决办法是每个外部变量只定义一次。

  static可以把变量和函数的作用域限制在一个源文件中,避免命名冲突。

  函数必须在调用它之前进行定义或声明,否则它的返回类型就默认为整型,这样当它与函数连接时就会得到错误的结果。为了表明形参实参可能导致的错误,作者用在不同情形下可以接受不同类型的参数的函数printf和scanf举例来进行了说明。

#include <stdio.h>
int main()
{
int i;
char c;
for (i = 0; i < 5; i++) {
scanf("%d", &c);
printf("%d ",i);
}
return 0;
}
//一种可能的输出结果
//0 0 0 0 0 1 2 3 4
//而不是
//0 1 2 3 4
/*
原因在于scanf期望读入一个指向整数的指针,然而得到的却是指向字符的指针。整数所占存储空间大于字符所占,所以字符c附近内存会被覆盖。可能的输出结果是c附近存放的是i的低端部分,每次输入都会覆盖为0*/

  检查外部类型,比如在两个文件中的extern int n;和long n;,运行存在着很多可能情况:编译器检测到冲突并返回诊断消息;使用的C语言实现对int和long在内部表示上是一样的,很巧合地,可以正常工作;二者虽然需要存储空间大小不同,但它们共享存储空间恰好可以保证赋给其中之一的值对另一个有效(比如低端部分共享存储空间),本来错误的程序因某种巧合却能正常工作,类似第二种情况;共享存储空间时给一个赋值却相当于对另一个赋予不同的值,不能正常工作。这样的引申例子:char filename[] = "/etc/passwd";和extern char* filename;,虽然前者的引用filename的值将得到指向该数组起始元素的指针,但filename类型是“字符数组”,而不是后者的“字符指针”,二者无法以一种合乎情理的方式共存。

/* 改法 1 */
char filename[] = "/etc/passwd"; /* 文件1 */
extern char filename[]; /* 文件2 */

/* 改法 2 */
char* filename = "/etc/passwd"; /* 文件1 */
extern char* filename; /* 文件2 */

  在这章的最后,作者表示解决外部对象在哪声明的好方法是使用头文件。

 

第五章  库函数

  本章主要讨论对于常用库函数的使用。对于getchar:

#include <stdio.h>
int main ()
{
char c;
while ((c = getchar()) != EOF)
putchar();
}

c被声明为char而不是int,无法容下所有可能的字符,特别是可能无法容下EOF。最终结果的三种可能:某些合法输入的字符被“截断”后使得c取值与EOF相同;另一种可能是c根本不能取到EOF这个值;出于巧合能够正常工作,这个巧合是编译器比较getchar返回值和EOF。这里作者没有给出解决方案,印象中The C Programming Language里类似程序是把getchar()放在while循环体内部的,不过手头的书遗失了,暂无法确认。
  对于fread和fwrite的举例是对使用过程加入fseek的用法,没有特别留意。

  对于setbuf,不是很明白作者举的例子的目的,而且程序本身运行起来难以发现问题。参考了一个帖子,分析的比较全面:点击查看。练习5-1可以看作是对这个问题的补充。

  使用外部变量errno检测错误,调用库函数后应该先确定失败再检测errno,而不是先设置errno=0、调用库函数后检测errno。原因是这个库函数可能要调用其他的库函数,如果调用到的其他库函数设置了errno,即使库函数返回没有错误,也会使得errno非0。

  在异步问题上,信号非常棘手,具有一些本质上的不可移植特性。具体的例子就不重述了,解决问题最好采取“守势”,让库函数signal尽可能简单,并把它们(应该是指信号和signal函数)组织在一起。

 

第六章  预处理器

  宏定义时空格的使用可能会带来错误,#define f (x) ((x)-1)不同于#define f(x) ((x)-1)。然而这不适用于宏调用,后者定义后,f(3)和f (3)都是2。宏不是函数,在使用类似函数的宏定义时,一般把参数加括号,避免宏展开后产生的结合性问题。有时即使采用了括号,宏仍可能造成副作用,比如做替换时,参数被计算了不止一次。后面举例toupper()有误,不知是PDF扫描问题还是原书第一版的问题,查阅了一下ctype.h,c += 'A' ? 'a'应为c += 'A' - 'a'。

  宏不是语句,assert的真实定义比较出乎意料,而这个发掘过程表明了用宏代替语句会有非常大的困难。

  宏不是类型定义,不同于typedef。以前总是混淆这一点,但是下面的例子确实很能说明问题。

#define FOOTYPE struct foo
typedef struct foo FOOTYPE;//两句看上去功能相同


#define T1 struct foo *
typedef struct foo *T2;//T1与T2似乎一样
/*
问题出现在声明多个变量时*/
T1 a, b;//被扩展为struct foo * a, b
T2 a, b;


第七章  可移植性缺陷

 

  作者提到了对于C标准的变化细节以及是否采用的看法、标识符应尽可能不以大小写区分、不同机器的整数长度大小。

  如果特别关心一个将要用到的变量最高位是0还是1可以将它声明为无符号数。得到一个字符变量c的与c等价的无符号整数不是(unsigned) c,这时c会先被转换成int从而可能导致非预期结果。正确方式是(unsigned char) c。

  移位比除法(除以2的幂)快得多,但有符号数不推荐这么做。

  null指针不指向任何对象,除非用于比较运算和赋值,出于任何其他目的使用null指针都是非法的。这条说得简单明确,很好记。作者又提到,由于对内存位置0的读写可能存在读保护、只读、可写三种情况,所以含有调用null指向的内容的程序在不同的机器上有不同的结果。

  除法截断问题提到的负数被除数和负数余数究竟应该怎样计算我以前从未考虑过,但作为C语言的运算符,/和%运算必须考虑到这一点。对于设计这种除法(q = a /b; r = a % b)的三原则,C语言只取了其一(q*b+r == a),以及当a>=0且b>0时,保证|r|<|b|且r>=0。对于负数除法构造哈希表有可行的方法,但作者认为更好的做法是把哈希函数的自变量设置为无符号数。

  作者介绍到如果程序中(伪)随机数函数rand,移植时必须做“剪裁”,其根源分歧始于C语言移植到VAX-11之时,它与PDP-11支持的最大整数长度不同,当时出现了两种解决方案:和PDP-11保持一致、和自身最大长度保持一致。

  后来作者又提到C语言发展过程中一位开发人员的改写宏toupper和tolower的小历史,最终的结果是两者被改写为函数,而用_toupper和_tolower的关键字重新引入宏,实现原先宏的功能。

  早期的C实现中realloc要求待重新分配的内存必须首先被释放。

  本章最后给出的提高可移植性的例子充分考虑了各种情况,但有说明多数情况下这么做是为了确保边界条件的正确。这段文字比较长,就不收录了,值得注意的是 "0123456789"[n%10]这种写法。

 

第八章  建议与答案

  这里指出有个预料把==误写成=这种错误出现并预防的方法:把常量放在判断相等的比较表达式的左侧,如‘\t’ == c 。这样写使得发生错误时编译器可以捕获。避免使用”生僻“的语言特性(即非众所周知的部分)预防Bug和方便移植。

  防御性编程?很生疏的词汇,了解了一下。这么做的原因是再怎么不可能的事在某些时候还是可能发生的,所以应该充分考虑异常情况,毕竟C编译器不可能捕获所有的程序错误。

 

附录

  printf(s)和printf("%s",s)不同,前者会把s含有的%后当作格式项,如果不是%%这样的内容而后又没有参数,会带来麻烦。

  预处理器的作用范围不能到达字符的内部。下面的例子给出了对于相关一个问题的解决方法:

#define NAMESIZE 14
char name[NAMESIZE];
......
printf("...%.NAMESIZE ...", ... , name, ...);//需要改进

printf("...%.*s ...", ... , .NAMESIZE, name, ...);//用*替换修饰符,在参数列表里使用

  varargs.h的使用方式比较独特,提供了对变长参数的支持,在此不再详写。stdarg.h是其的ANSI版本。

posted @ 2011-11-18 22:32  五岳  阅读(553)  评论(0编辑  收藏  举报
回到顶部