上QQ阅读APP看书,第一时间看更新
1.2 C语言的早期体验
增加类型系统的主要目的是帮助编译器设计者区分新型PDP-11机器所拥有的不同数据类型,如单精度浮点数、双精度浮点数和字符等。这与其他一些语言(如Pascal)形成了鲜明的对比。在Pascal中,类型系统的目的是保护程序员,防止他们在数据上进行无效的操作。由于设计哲学不同,C语言排斥强类型,它允许程序员在需要时可以在不同类型的对象间赋值。类型系统的加入可以说是事后诸葛,从未在可用性方面进行过认真的评估和严格的测试。时至今日,许多C程序员仍然认为“强类型”只不过是增加了敲击键盘的无用功。
除了类型系统之外,C语言的许多其他特性是为了方便编译器设计者而建立的(为什么不呢?开始几年C语言的主要客户就是那些编译器设计者)。根据编译器设计者的思路而发展形成的语言特性如下所示。
- 数组下标从0而不是1开始。绝大多数人习惯从1而不是0开始计数。编译器设计者则选择从0开始,因为偏移量的概念在他们心中已是根深蒂固。但这种设计让一般人感觉很别扭。尽管我们定义了一个数组a[100],但千万别往a[100]里存储数据,因为这个数组的合法范围是从a[0]到a[99]。
- C语言的基本数据类型直接与底层硬件相对应。例如,不像Fortran,C语言中不存在内置的复数类型。某种语言要素如果没有得到底层硬件的直接支持,那么编译器设计者就不会在它上面浪费任何精力。C语言一开始并不支持浮点类型,直到硬件系统能够直接支持浮点数之后才增加了对它的支持。
- auto关键字显然是摆设。这个关键字只对创建符号表入口的编译器设计者有意义。它的意思是“在进入程序块时自动进行内存分配”(与全局静态分配或在堆上动态分配相反)。其他程序员不必操心auto关键字,它是缺省的变量内存分配模式。
- 表达式中的数组名可以看作是指针。把数组当作指针简化了很多东西。我们不再需要一种复杂的机制区分它们,把它们传递到一个函数时不必忍受必须复制所有数组内容的低效率。不过,数组和指针并不是在任何情况下都是等效的,更详细的讨论参见第4章。
- float被自动扩展为double。尽管在ANSI C中情况不再如此,但最初浮点数常量的精度都是double型的,所有表达式中的float变量总被自动转换成double。这样做的理由从未公诸于众,但它与PDP-11中浮点数的硬件表示方式有关。首先,在PDP-11或VAX中,从float转换到double的代价非常小,只要在后面增加一个每个位均为0的字即可。如果要转换回来,去掉第二个字就可以了。其次,要知道在某些PDP-11的浮点数硬件表示形式中有一个运算模式位(mode bit),你既可以只进行float的运算,也可以只进行double的运算,但如果想在这两种方式间进行切换,就必须修改这个位来改变运算模式。在早期的UNIX程序中,float用得不是太多,所以把运算模式固定为double是比较方便的,省得编译器设计者去跟踪它的变化。
- 不允许嵌套函数(函数内部包含另一个函数的定义)。这简化了编译器,并稍微提高了C程序的运行时组织结构。具体的机理在第6章中详细描述。
- register关键字。这个关键字能给编译器设计者提供线索,即程序中的哪些变量属于热门(经常被使用),就可以把它们存放到寄存器中。这个设计可以说是一个失误。如果让编译器在使用各个变量时自动处理寄存器的分配工作,显然比一经声明就把这类变量在生命期内始终保留在寄存器里要好。尽管使用register关键字简化了编译器,但却把包袱丢给了程序员。
为了C编译器设计者的方便而建立的其他语言特性还有很多。这本身不是一件坏事,它大大简化了C语言本身,而且通过回避一些复杂的语言要素(如Ada中的泛型和任务,PL/I中的字符串处理,C++中的模板和多重继承),C语言更容易学习和实现,而且效率非常高。
和其他大多数语言不同,C语言有一个漫长的进化过程。在目前这个形式之前,它经历了许多中间状态。它历经多年,从一个实用工具进化为一种经过大量试验和测试的语言。第一个C编译器大约出现在1970年。时光荏苒,作为它的根基的UNIX系统得到了广泛使用,C语言也随之茁壮成长。它对直接由硬件支持的底层操作的强调,带来了极高的效率和移植性,反过来也帮助UNIX获得了巨大的成功。