诉说C语言的前世今生
C语言,从20世纪70年代设计并实现之初,就注定是带有强烈工程师文化,而缺乏一些学术气息的语言。它的许多细节设计,都带有强烈的实用化痕迹。C语言因Unix操作系统而生,是Unix系统的母语。这导致在这个广泛应用的操作系统上开发,必须通过C语言的形式和系统进行交互。这不仅影响了Unix一个平台上的软件,也影响了后来世界上最大的桌面系统Windows,以及越来越多的嵌入式平台。
由于大部分应用软件最终都需要和操作系统打交道,所以用来开发应用软件的语言,绝大部分也需要利用C语言完成和操作系统的通讯。这个世界上绝大部分流行的编程语言,都选择了用C语言来实现其编译器或解释器,以及基础部分的运行时库。无论C语言设计本身有何种缺憾,在今天,它已无可取代。
到了今天,大部分程序员不再需要逐个时间周期地去抠程序的性能。不需要刻意追求速度最快、最节省系统资源的软件。不需要写那些和系统内核紧密联系的程序。但C语言在此之外,依然有其重要的应用领域。我们可以把它作为对最终机器模型的高层次的统一抽象工具,而不必考虑机器环境的差异。经过30多年的发展,证明了C语言的确是对经典机器模型的最佳表述。仅仅通过增加一个非常薄的胶合层就得到了清晰简洁的设计。正是这一点,使得C语言在计算机硬件高速发展的几十年中,一直生机勃勃。
我们在讨论C语言时,其实不仅仅涉及了C语言本身那用三十几个保留字构成的精简的控制结构和简约的语言特征。还包括了一套对#号打头的预处理部分(尤其是基于文本替换的宏处理),以及某些惯用的源代码组织方式(例如所有的接口定义被定义在后缀为h的文件中,并通过预处理方式替换进源代码)和基本的程序库。
这几部分语言核心之外的部分相对独立,以致于使用C语言开发并不一定使用标准化的那些东西。C语言对运行时环境的依赖是非常小的。
而编译预处理器又使得语言富有弹性,甚至可以写出违背C语言哲学的代码。著名的IOCCC大赛展示了许多常人无法理解的C代码。但实际上,C语言主张代码清晰,表里如一。开发者和维护者都能很容易地预测每一行代码背后的行为。避免存在一些阴暗的角落藏着一些罕见的用法导致程序运行时出现诡异的行为。C语言在发展过程中一直坚持着最小意外原则。而这一点,正是C语言的,著名发展分支C++所偏离的东西。
C语言并不是绝对意义上最快的语言,但是它的效率非常好,在切合大部分机器模型并给出统一抽象的基础上,几乎没有其他语言做得更好了。这也是C语言哲学的一部分:在统一硬件抽象模型的基础上,尽可能地利用所在硬件环境的一切资源。有时候C语言程序员会走向某种极端,追求语言细节的优化,觉得某种代码的组织方式会比另一种方式更高效,但几乎总是错的。优化取决于对具体硬件的理解,以及对编译器如何翻译这些代码的了解,但这正是设计C语言想避免的东西。我们不必去争论在语句级上每行代码精确开销的优劣。
同时,C语言的另一设计哲学就是让每行C代码尽量准确地对应相当数量的目标机器码。这使得程序员更为容易地理解程序的运行过程。让程序员脑海里实时地做一个源代码到最终控制流程的映射。基于这个思想,C语言一直没有增加对结构进行操作的操作符(而C++中把类或结构模拟成原生类型的做法相当普遍),甚至于inline关键字也迟迟没有被标准化(inline出现在C99标准中,但这个最新的C语言标准并没有被广泛接受),正是因为它某种程度破坏了这一点。
C语言在坚持以上几点理念时,并非突出某个方面(比如追求性能),而是同时兼顾。
C语言并不是这个世界上唯一的编程语言,可惜的不是所有程序员都认识到了这点。对于把C语言作为自己唯一开发语言的程序员来说,很有必要开拓自己的眼界,这样才能更为清晰地理解C语言的内在精神。并不是说,某某语言本身是用C语言来实现,那么C语言就可以以同样的方式,解决那种语言解决的问题(甚至更为高效)。一些C语言中的概念,到了另一种语言中,很可能用完全不同的方式展现出来。正如自然语言会影响人的思维方式一样,编程语言一样会影响人对某种算法的编码形式。在C里,我们总以为某些写法是自然而然的,但换了种语言很可能并不尽然。
无论如何,C语言的语法和设计影响了许多其他语言,最为彻底的是 C++ ,以及大多数程序员都能叫得出名字的一些流行语言:Java 、PHP、JavaScript、Perl、C#、D、Objective-C等。这给人造成一种错觉:新的语言取代了旧的,对旧语言做了改良和完善。最广泛传播的观点是,C++是C的一个超集,它能做C能做的所有事情,且能做得更好。持有这种观点的C++程序员们甚至把已有的各种C代码用C++重新实现。
但实际上,C和C++更应该被看成是相互平等的存在。C++ 更像是一种借用了几乎全部 C 语法(但还是有细微差异)的全新语言。它们在很多方面都有设计理念上的差异。C++企图完全兼容C的语法却不想完全继承C语言的理念,这使它背负了巨大的包袱。而C的另一个继任者:Objective-C,抛弃了一些东西,则显得清爽一些。
回顾C++出现的时代背景是把面向对象当成解决复杂问题的“银弹”的年代。这使得 C++ 在发明之初,迅速占领了大量原本是C语言的市场,甚至被看成是C语言的替代品。但C++的拥趸们并没有等到这一天。历史证明,面向对象也不是“银弹”。最近十年,C++的粉丝们从C++语言的犄角旮旯里挖掘出来的各种武器,让C++语言变成了包含多种编程范式的巨无霸,却并没有让解决问题变得更容易。这并不完全是语言的问题,可能有很大程度上是面向对象等开发方法本身的问题。这也证明了C语言保持自身的简洁正是其生机昂然的源泉。
和浩如烟海的C++书籍相比较。如果你已经是程序员,但还不了解C语言的话。学习C语言,只需要读一本书,而这本书没有第二选择,就是经典的《The C Programming Language》(K&R)。
薄薄的一本就讲透了语言的方方面面。可惜的是,C语言过于注重对机器模型的抽象,并不适合用来程序员入门。尤其是在国内的教材市场,充斥着大量糟糕的C语言教材。在这些拙劣的教材中,甚至把开发工具(比如特定的C语言开发集成环境)和特定的硬件环境(甚至是过时的8086内存模型)与语言教学混为一谈。
对于C语言不是母语的程序员来说,有充分的理由去学习一下C语言。那是低投入、高产出的。它会使你学会在硬件层次上思考问题(这或许对你是一个新的思维角度),而且C语言已经非常稳定,不会再有(它本身也不希望有)大的变化,不用担心学到的知识会过时。C语言在1990年制定出一个现在通行的标准(C90)以来,在C的主流开发社区中几乎没有变过。虽然,从1999年开始,C语言委员会几经修订C语言的新标准(C99),但似乎并不被广泛接受。虽然有很大程度上,这是源于世界上最大的C/C++商业编译器提供商微软对其不感兴趣,但在开源界,即使有GNU C对C语言新标准的不断推动,那些实际用C语言做开发的大佬们还是纷纷表示,新的标准还不是很成熟。新的特性也不是特别有必要。
我用C99开发有一些年头,但也只使用了其中一个子集,不太敢在正式项目中完全推广。至于C语言近年来的发展,我个人比较欣赏苹果公司对C语言添加的 blocks 扩展以用来实现closure。但并不看好这些新特性会迅速融入C语言社区。
C语言从语言角度上讲,最大缺陷在于要求程序员自己去做内存管理。用C语言去处理复杂的数据结构,程序员大部分的时间都花在了这上面,并且滋生了无数Bug。调试C程序变成了一项独立于编写C程序的技能。防止缓冲区溢出、防止数据读写越界、正确的动态回收内存、避免悬空指针,这些在大部分语言看起来不可思议的关注点,在C语言程序员眼里变得稀松平常,甚至是衡量C程序员技能经验水平的重要标志。可要知道,这些和具体问题的解决过程无关。
也有人试图在C语言层面解决这个问题,例如以库形式提供垃圾回收的机制(我也曾做过类似尝试)。但C语言本身的设计使它无法成为一个完美的解决方案。同样的问题也存在于C++。现在看来,不对语言做大的改造,很难回避。可改造本身又违背了C语言一贯的哲学。C语言的发明人之一的Ken Thompson近年来参与了Go语言的设计和实现,可以看成从另一角度对新的程序开发语言的尝试,可那已经不是C。
这个问题在一定程度上也促使了Java的诞生。Java采用虚拟机和字节码的方式改造了底层的机器模型,并在底层模型的基础上加入了垃圾回收机制,并在语言层面取消了指针。在C语言的原生地,也有更多的动态(脚本)语言出现。先是有Awk这样的简易语言,后有Perl,再是Python等的流行。在Unix风格下,程序员倾向于为特定领域设计特定的语言。
C和Unix的设计哲学是一体的。它们都鼓励清晰的模块化设计,让模块之间独立,再用薄的胶合层联系起来。脚本语言在现代类Unix系统上大量出现并充当这种粘合工作就是一种发展必然。而原本的充当粘合部分的脚本语言,也逐步发展起来,远远超出脚本的用途范畴。作为程序员,尤其是C程序员,必须对它们有所了解并掌握其中的一些,才能适应现代的挑战。
我们不应该指望一门语言解决所有的问题。至于C语言本身,它将在很长的一段时间,带着它的优雅和缺陷,继续扮演它在计算机世界中重要的角色。
由于大部分应用软件最终都需要和操作系统打交道,所以用来开发应用软件的语言,绝大部分也需要利用C语言完成和操作系统的通讯。这个世界上绝大部分流行的编程语言,都选择了用C语言来实现其编译器或解释器,以及基础部分的运行时库。无论C语言设计本身有何种缺憾,在今天,它已无可取代。
到了今天,大部分程序员不再需要逐个时间周期地去抠程序的性能。不需要刻意追求速度最快、最节省系统资源的软件。不需要写那些和系统内核紧密联系的程序。但C语言在此之外,依然有其重要的应用领域。我们可以把它作为对最终机器模型的高层次的统一抽象工具,而不必考虑机器环境的差异。经过30多年的发展,证明了C语言的确是对经典机器模型的最佳表述。仅仅通过增加一个非常薄的胶合层就得到了清晰简洁的设计。正是这一点,使得C语言在计算机硬件高速发展的几十年中,一直生机勃勃。
我们在讨论C语言时,其实不仅仅涉及了C语言本身那用三十几个保留字构成的精简的控制结构和简约的语言特征。还包括了一套对#号打头的预处理部分(尤其是基于文本替换的宏处理),以及某些惯用的源代码组织方式(例如所有的接口定义被定义在后缀为h的文件中,并通过预处理方式替换进源代码)和基本的程序库。
这几部分语言核心之外的部分相对独立,以致于使用C语言开发并不一定使用标准化的那些东西。C语言对运行时环境的依赖是非常小的。
而编译预处理器又使得语言富有弹性,甚至可以写出违背C语言哲学的代码。著名的IOCCC大赛展示了许多常人无法理解的C代码。但实际上,C语言主张代码清晰,表里如一。开发者和维护者都能很容易地预测每一行代码背后的行为。避免存在一些阴暗的角落藏着一些罕见的用法导致程序运行时出现诡异的行为。C语言在发展过程中一直坚持着最小意外原则。而这一点,正是C语言的,著名发展分支C++所偏离的东西。
C语言并不是绝对意义上最快的语言,但是它的效率非常好,在切合大部分机器模型并给出统一抽象的基础上,几乎没有其他语言做得更好了。这也是C语言哲学的一部分:在统一硬件抽象模型的基础上,尽可能地利用所在硬件环境的一切资源。有时候C语言程序员会走向某种极端,追求语言细节的优化,觉得某种代码的组织方式会比另一种方式更高效,但几乎总是错的。优化取决于对具体硬件的理解,以及对编译器如何翻译这些代码的了解,但这正是设计C语言想避免的东西。我们不必去争论在语句级上每行代码精确开销的优劣。
同时,C语言的另一设计哲学就是让每行C代码尽量准确地对应相当数量的目标机器码。这使得程序员更为容易地理解程序的运行过程。让程序员脑海里实时地做一个源代码到最终控制流程的映射。基于这个思想,C语言一直没有增加对结构进行操作的操作符(而C++中把类或结构模拟成原生类型的做法相当普遍),甚至于inline关键字也迟迟没有被标准化(inline出现在C99标准中,但这个最新的C语言标准并没有被广泛接受),正是因为它某种程度破坏了这一点。
C语言在坚持以上几点理念时,并非突出某个方面(比如追求性能),而是同时兼顾。
C语言并不是这个世界上唯一的编程语言,可惜的不是所有程序员都认识到了这点。对于把C语言作为自己唯一开发语言的程序员来说,很有必要开拓自己的眼界,这样才能更为清晰地理解C语言的内在精神。并不是说,某某语言本身是用C语言来实现,那么C语言就可以以同样的方式,解决那种语言解决的问题(甚至更为高效)。一些C语言中的概念,到了另一种语言中,很可能用完全不同的方式展现出来。正如自然语言会影响人的思维方式一样,编程语言一样会影响人对某种算法的编码形式。在C里,我们总以为某些写法是自然而然的,但换了种语言很可能并不尽然。
无论如何,C语言的语法和设计影响了许多其他语言,最为彻底的是 C++ ,以及大多数程序员都能叫得出名字的一些流行语言:Java 、PHP、JavaScript、Perl、C#、D、Objective-C等。这给人造成一种错觉:新的语言取代了旧的,对旧语言做了改良和完善。最广泛传播的观点是,C++是C的一个超集,它能做C能做的所有事情,且能做得更好。持有这种观点的C++程序员们甚至把已有的各种C代码用C++重新实现。
但实际上,C和C++更应该被看成是相互平等的存在。C++ 更像是一种借用了几乎全部 C 语法(但还是有细微差异)的全新语言。它们在很多方面都有设计理念上的差异。C++企图完全兼容C的语法却不想完全继承C语言的理念,这使它背负了巨大的包袱。而C的另一个继任者:Objective-C,抛弃了一些东西,则显得清爽一些。
回顾C++出现的时代背景是把面向对象当成解决复杂问题的“银弹”的年代。这使得 C++ 在发明之初,迅速占领了大量原本是C语言的市场,甚至被看成是C语言的替代品。但C++的拥趸们并没有等到这一天。历史证明,面向对象也不是“银弹”。最近十年,C++的粉丝们从C++语言的犄角旮旯里挖掘出来的各种武器,让C++语言变成了包含多种编程范式的巨无霸,却并没有让解决问题变得更容易。这并不完全是语言的问题,可能有很大程度上是面向对象等开发方法本身的问题。这也证明了C语言保持自身的简洁正是其生机昂然的源泉。
和浩如烟海的C++书籍相比较。如果你已经是程序员,但还不了解C语言的话。学习C语言,只需要读一本书,而这本书没有第二选择,就是经典的《The C Programming Language》(K&R)。
薄薄的一本就讲透了语言的方方面面。可惜的是,C语言过于注重对机器模型的抽象,并不适合用来程序员入门。尤其是在国内的教材市场,充斥着大量糟糕的C语言教材。在这些拙劣的教材中,甚至把开发工具(比如特定的C语言开发集成环境)和特定的硬件环境(甚至是过时的8086内存模型)与语言教学混为一谈。
对于C语言不是母语的程序员来说,有充分的理由去学习一下C语言。那是低投入、高产出的。它会使你学会在硬件层次上思考问题(这或许对你是一个新的思维角度),而且C语言已经非常稳定,不会再有(它本身也不希望有)大的变化,不用担心学到的知识会过时。C语言在1990年制定出一个现在通行的标准(C90)以来,在C的主流开发社区中几乎没有变过。虽然,从1999年开始,C语言委员会几经修订C语言的新标准(C99),但似乎并不被广泛接受。虽然有很大程度上,这是源于世界上最大的C/C++商业编译器提供商微软对其不感兴趣,但在开源界,即使有GNU C对C语言新标准的不断推动,那些实际用C语言做开发的大佬们还是纷纷表示,新的标准还不是很成熟。新的特性也不是特别有必要。
我用C99开发有一些年头,但也只使用了其中一个子集,不太敢在正式项目中完全推广。至于C语言近年来的发展,我个人比较欣赏苹果公司对C语言添加的 blocks 扩展以用来实现closure。但并不看好这些新特性会迅速融入C语言社区。
C语言从语言角度上讲,最大缺陷在于要求程序员自己去做内存管理。用C语言去处理复杂的数据结构,程序员大部分的时间都花在了这上面,并且滋生了无数Bug。调试C程序变成了一项独立于编写C程序的技能。防止缓冲区溢出、防止数据读写越界、正确的动态回收内存、避免悬空指针,这些在大部分语言看起来不可思议的关注点,在C语言程序员眼里变得稀松平常,甚至是衡量C程序员技能经验水平的重要标志。可要知道,这些和具体问题的解决过程无关。
也有人试图在C语言层面解决这个问题,例如以库形式提供垃圾回收的机制(我也曾做过类似尝试)。但C语言本身的设计使它无法成为一个完美的解决方案。同样的问题也存在于C++。现在看来,不对语言做大的改造,很难回避。可改造本身又违背了C语言一贯的哲学。C语言的发明人之一的Ken Thompson近年来参与了Go语言的设计和实现,可以看成从另一角度对新的程序开发语言的尝试,可那已经不是C。
这个问题在一定程度上也促使了Java的诞生。Java采用虚拟机和字节码的方式改造了底层的机器模型,并在底层模型的基础上加入了垃圾回收机制,并在语言层面取消了指针。在C语言的原生地,也有更多的动态(脚本)语言出现。先是有Awk这样的简易语言,后有Perl,再是Python等的流行。在Unix风格下,程序员倾向于为特定领域设计特定的语言。
C和Unix的设计哲学是一体的。它们都鼓励清晰的模块化设计,让模块之间独立,再用薄的胶合层联系起来。脚本语言在现代类Unix系统上大量出现并充当这种粘合工作就是一种发展必然。而原本的充当粘合部分的脚本语言,也逐步发展起来,远远超出脚本的用途范畴。作为程序员,尤其是C程序员,必须对它们有所了解并掌握其中的一些,才能适应现代的挑战。
我们不应该指望一门语言解决所有的问题。至于C语言本身,它将在很长的一段时间,带着它的优雅和缺陷,继续扮演它在计算机世界中重要的角色。