C专家编程
上QQ阅读APP看书,第一时间看更新

1.3 标准I/O库和C预处理器

C编译器不曾实现的一些功能必须通过其他途径实现。在C语言中,它们在运行时进行处理,既可以出现在应用程序代码中,也可以出现在运行时函数库(runtime library)中。在许多其他语言中,编译器会植入一些代码,隐式地调用运行时支持工具,这样程序员就无须操心它们了。但在C语言中,绝大多数库函数或辅助程序都需要显式调用。例如,在C语言中(必要时),程序员必须管理动态内存的使用,创建各种大小的数组,测试数组边界,并自己进行范围检测。

与此类似,C语言原先并没有定义I/O,而是由库函数提供。后来,这实际上成了标准机制。可移植的I/O由Mike Lesk编写,最初出现在1972年左右,可在当时存在的3个平台上通用。实践经验表明,它的性能低于预期值。所以,人们对它又进行了优化和裁剪,后来成为标准I/O函数库。

C预处理器大约也是在这个时候被加入的,倡议者是Alan Snyder。它所实现的3个主要功能如下。

  • 字符串替换。形式类似于“把所有的foo替换为baz”,通常用于为常量提供一个符号名。
  • 头文件包含(这是在BCPL中首创的)。一般性的声明可以被分离到头文件中,并且可以被许多源文件使用。虽然约定采用“.h”作为头文件的扩展名,但在头文件和包含实现代码的对象库之间在命名上却没有相应的约定,这多少令人不快。
  • 通用代码模板的扩展。与函数不同,宏(marco)在连续几个调用中所接收的参数的类型可以不同(宏的实际参数只是按照原样输出)。这个特性的加入比前两个稍晚,而且多少显得有些笨拙。在宏的扩展中,空格会对扩展的结果造成很大的影响。
#define a(y)  a_expanded(y)
a(x);

被扩展为

a_expanded(x);

#define a (y)   a_expanded (y)
a(x);

则被扩展为

(y)    a_expanded (y)(x)

它们所表示的意思风马牛不相及。你可能会以为在宏里面使用花括号就像在C语言的其他部分一样,能把多条语句组合成一条复合语句,但实际上并非如此。

这里对C语言的预处理器并不做太多的讨论。这反映了这样一个观点:对于宏这样的预处理器,只应该适量使用,所以无须深入讨论。C++在这方面引入了一些新的方法,使得预处理器几乎无用武之地。


 

图片 649 软件信条

C并非Algol

20世纪70年代后期,Steve Bourne在贝尔实验室编写UNIX第7版的Shell(命令解释器)时,决定采用C预处理器以使C语言看上去更像Algol-68。早年在英国剑桥大学时,Steve曾编写过一个Algol-68编译器。他发现如果代码中有显式的“结束语句”提示,诸如if ... fi或者case ... esac等,调试起来会更容易。Steve认为仅仅一个“}”是不够的,因此他建立了许多预处理定义:

    #define STRING char *
    #define IF if(
    #define THEN ){
    #define ELSE }else(
    #define FI ;}
    #define WHILE while(
    #define DO ){
    #define OD ;}
    #define INT int
    #define BEGIN {
    #define END }

这样,就可以像下面这样编写代码:

    INT compare(s1, s2)
        STRING s1;
        STRING s2;
    BEGIN
       WHILE *s1++ == *s2
       DO IF *s2++ == 0
          THEN return(0);
          FI
       OD
         return(*--s1 - *s2);
    END

再看一下相应的C代码:

    int compare(s1, s2)
       char *s1, *s2;
    {
       while(*s1++ == *s2){
             if(*s2++ == 0) return(0);
       }
       return (*--s1 - *s2);
    }

Bourne Shell的影响远远超出了贝尔实验室的范围,这也使得这种类似Algol-68的C语言变体名声大噪。但是,有些C程序员对此感到不满。他们抱怨这种记法使别人难以维护代码。时至今日,BSD 4.3 Bourne Shell(保存于/bin/sh)依然是采用这种记法写的。

我有一个特别的理由反对Bourne Shell:在我的书桌上堆满了针对它的Bug报告!我把它们发给Sam,我们都发现了这样的Bug:这个Shell不使用malloc,而是使用sbrk自行负责堆存储的管理。在维护这类软件时,每解决两个问题通常又会引入一个新问题。Steve解释说,他之所以采用这种特制的内存分配器,是为了提高字符串处理的效率,他从来不曾想到其他人会阅读他的代码。


 

Bourne创立的这种C语言变体事实上促成了异想天开的国际C语言混乱代码大赛(The International Obfuscated C Code Competition),比赛要求参赛的程序员尽可能地编写神秘而混乱的程序来赢得对手(关于这个比赛,以后还有更详尽的说明)。

宏最好只用于命名常量,并为一些适当的结构提供简捷的记法。宏名应该大写,这样便很容易与函数调用区分开来。千万不要使用C预处理器来修改语言的基础结构,因为这样一来C语言就不再是C语言了。