#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + 'A' - 'a' : (c)) #define tolower(c) ((c) >= 'A' && (c) <= 'Z' ? (c) + 'a' - 'A' : (c))
但要知道,这里c的三次出现都要被求值,这会破坏如toupper(*p++)这样的表达式。因此,可以考虑将toupper()和tolower()重写为函数。toupper()看起来可能像这样:
int toupper(int c) { if(c >= 'a' && c <= 'z') return c + 'A' - 'a'; return c; }
tolower()类似。
这个改变带来更多的问题,每次使用这些函数的时候都会引入函数调用开销。我们的英雄认为一些人可能不愿意支付这些开销,因此他们将这个宏重命名为:
#define _toupper(c) ((c) + 'A' - 'a') #define _tolower(c) ((c) + 'a' - 'A')
这就允许用户选择方便或速度。
这里面其实只有一个问题:伯克利的人们和其他的C实现者并没有跟着这么做。 这意味着一个在AT&T系统上编写的使用了toupper()或tolower()的程序,如果没有为其传递正确大小写字母参数,在其他C实现中可能不会正常工作。
如果不知道这些历史,可能很难对这类错误进行跟踪。
7.8 先释放,再重新分配
很多C实现为用户提供了三个内存分配函数:malloc()、realloc()和free()。调用malloc(n)返回一个指向有n个字符的新分配的内存的指针,这个指针可以由程序员使用。给free()传递一个指向由malloc()分配的内存的指针可以使这块内存得以再次使用。通过一个指向已分配区域的指针和一个新的大小调用realloc()可以将这块内存扩大或缩小到新尺寸,这个过程中可能要复制内存。
也许有人会想,真相真是有点微妙啊。下面是System V接口定义中出现的对realloc()的描述:
realloc改变一个由ptr指向的size个字节的块,并返回该块(可能被移动)的指针。 在新旧尺寸中比较小的一个尺寸之下的内容不会被改变。
而UNIX系统第七版的参考手册中包含了这一段的副本。此外,还包含了描述realloc()的另外一段:
如果在最后一次调用malloc、realloc或calloc后释放了ptr所指向的块,realloc依旧可以工作;因此,free、malloc和realloc的顺序可以利用malloc压缩存贮的查找策略。
31
因此,下面的代码片段在UNIX第七版中是合法的: free (p);
p = realloc(p, newsize);
这一特性保留在从UNIX第七版衍生出来的系统中:可以先释放一块存储区域,然后再重新分配它。这意味着,在这些系统中释放的内存中的内容在下一次内存分配之前可以保证不变。因此,在这些系统中,我们可以用下面这种奇特的思想来释放一个链表中的所有元素:
for(p = head; p != NULL; p = p->next) free((char *)p);
而不用担心调用free()会导致p->next不可用。
不用说,这种技术是不推荐的,因为不是所有C实现都能在内存被释放后将它的内容保留足够长的时间。然而,第七版的手册遗留了一个未声明的问题:realloc()的原始实现实际上是必须要先释放再重新分配的。出于这个原因,一些C程序都是先释放内存再重新分配的,而当这些程序移植到其他实现中时就会出现问题。
7.9 可移植性问题的一个实例
让我们来看一个已经被很多人在很多时候解决了的问题。下面的程序带有两个参数:一个长整数和一个函数(的指针)。它将整数转换位十进制数,并用代表其中每一个数字的字符来调用给定的函数。
void printnum(long n, void (*p)()) { if(n < 0) { (*p)('-'); n = -n; }
if(n >= 10)
printnum(n / 10, p); (*p)(n % 10 + '0'); }
这个程序非常简单。首先检查n是否为负数;如果是,则打印一个符号并将n变为正数。接下来,测试是否n >= 10。如果是,则它的十进制表示中包含两个或更多个数字,因此我们递归地调用printnum()来打印除最后一个数字外的所有数字。最后,我们打印最后一个数字。
这个程序——由于它的简单——具有很多可移植性问题。首先是将n的低位数字转换成字符形式的方法。用n % 10来获取低位数字的值是好的,但为它加上'0'来获得相应的字符表示就不好了。这个加法假设机器中顺序的数字所对应的字符数顺序的,没有间隔,因此'0' + 5和'5'的值是相同的,等等。尽管这个假设对于ASCII和EBCDIC字符集是成立的,但对于其他一些机器可能不成立。避免这个问题的方法是使用一个表:
void printnum(long n, void (*p)()) {
32
if(n < 0) { (*p)('-'); n = -n; }
if(n >= 10)
printnum(n / 10, p); (*p)(\}
另一个问题发生在当n < 0时。这时程序会打印一个负号并将n设置为-n。这个赋值会发生溢出,因为在使用2的补码的机器上通常能够表示的负数比正数要多。例如,一个(长)整数有k位和一个附加位表示符号,则-2k可以表示而2k却不能。
解决这一问题有很多方法。最直观的一种是将n赋给一个unsigned long值。然而,一些C便一起可能没有实现unsigned long,因此我们来看看没有它怎么办。
在第一个实现和第二个实现的机器上,改变一个正整数的符号保证不会发生溢出。问题仅出在改变一个负数的符号时。因此,我们可以通过避免将n变为正数来避免这个问题。
当然,一旦我们打印了负数的符号,我们就能够将负数和正数视为是一样的。下面的方法就强制在打印符号之后n为负数,并且用负数值完成我们所有的算法。如果我们这么做,我们就必须保证程序中打印符号的部分只执行一次;一个简单的方法是将这个程序划分为两个函数:
void printnum(long n, void (*p)()) { if(n < 0) { (*p)('-'); printneg(n, p); } else
printneg(-n, p); }
void printneg(long n, void (*p)()) { if(n <= -10)
printneg(n / 10, p); (*p)(\}
printnum()现在只检查要打印的数是否为负数;如果是的话则打印一个符号。否则,它以n的负绝对值来调用printneg()。我们同时改变了printneg()的函数体来适应n永远是负数或零这一事实。
我们得到什么?我们使用n / 10和n % 10来获取n的前导数字和结尾数字(经过适当的符号变换)。调用整数除法的行为在其中一个操作数为负的时候是实现相关的。因此,n % 10有可能是正的!这时,-(n % 10)是负数,将会超出我们的数字字符数组的末尾。
33
为了解决这一问题,我们建立两个临时变量来存放商和余数。作完除法后,我们检查余数是否在正确的范围内,如果不是的话则调整这两个变量。printnum()没有改变,因此我们只列出printneg():
void printneg(long n, void (*p)()) { long q; int r; if(r > 0) { r -= 10; q++; }
if(n <= -10) { printneg(q, p); }
(*p)(\}
8 这里是空闲空间
还有很多可能让C程序员误入迷途的地方本文没有提到。如果你发现了,请联系作者。在以后的版本中它会被包含进来,并添加一个表示感谢的脚注。 参考
《The C Programming Language》(Kernighan and Ritchie, Prentice-Hall 1978)是最具权威的C著作。它包含了一个优秀的教程,面向那些熟悉其他高级语言程序设计的人,和一个参考手册,简洁地描述了整个语言。尽管自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)的一个扩充,有兴趣的读者可以读一读它。 2. 因为!=的结果不是1就是0。 3. 感谢Guy Harris为我指出这个问题。
4. Dennis Ritchie和Steve Johnson同时向我指出了这个问题。 5. 感谢一位不知名的志愿者提出这个问题。 6. 感谢Richard Stevens指出了这个问题。
34
7. 一些C编译器要求每个外部对象仅有一个定义,但可以有多个声明。使用这样的编译器时,我们何以很容易地将一个声明放到一个包含文件中,并将其定义放到其它地方。这意味着每个外部对象的类型将出现两次,但这比出现多于两次要好。
8. 分离函数参数用的逗号不是逗号运算符。例如在f(x, y)中,x和y的获取顺序是未定义的,但在g((x, y))中不是这样的。其中g只有一个参数。它的值是通过对x进行求值、抛弃这个值、再对y进行求值来确定的。
9. 预处理器还可以很容易地组织这样的显式常量以能够方便地找到它们。 10. PDP-11和VAX-11是数组设备集团(DEC)的商标。 本
文
来
自
CSDN
博
客
,
转
载
请
标
http://blog.csdn.net/milan25429688/archive/2005/03/24/328944.aspx#contents
出
处
:
35
明