(译)追本溯源 —— C之精神

类别:编程语言 点击:0 评论:0 推荐:

声明:本文原刊于程序员杂志第10期,略有删节,非经杂志社和作者书面许可,不得转载。

原文作者:Greg Colvin

 http://www.artima.com/cppsource/spiritofc.html

 

翻译:涩涩 http://blog.csdn.net/sese

是否从某种意义上说,我们可以认为C,C++和Java“共享某种相同的精神”?至少从最表面的层次上,它们看起来十分相似。比如下面的代码段,如果把它放在上述三种语言相应的程序中,都会按照欧几里得算法计算两个数的最大公约数:

int g cd(int m, int n) {

   while( m > 0 ) {

      if( n > m ) {

         int t = m; m = n; n = t;

      }

      m -= n;

   }

   return n;

}

 

但是,当谈及“共享某种相同的精神”的时候,我们指得是超越语法层面之上的、更为本质的共同点。为了寻找这些精神所在,我们还是来看一看ANSI C标准的设计原则:

 

    标准委员会以存续C的传统精神作为其主要使命。C之精神包括许多方面,究其本源乃是出于C语言社群对语言深层原则的认同感。C之精神的某些方面或以短语形式总结如下:

    1 信任程序员

    2 不要阻拦程序员做那些应该做的事

    3 保持语言小而简洁

    4 任何操作只有一种方式完成

    5 即使移植性不能保证也要保证效率

 

最后一点需要解释一下:编译生成高效代码的潜力是C语言最重要的长处之一。为了确保简单的操作不会导致编译时的代码展开,许多操作被定义成由目标机器的硬件决定而不是一些通用的抽象规则。

 

在很多方面,其实B语言才是这些精神的最佳体现,从那时开始的演化过程可以被看作是对这五条原则的不断折衷与妥协。后面我会更多的谈到这点。

 

“受过二十年教育之后,你被要求日以继夜地工作”——Boy Daylon

“Twenty years of schooling, they put you on the day shift” ——Boy Daylon

 

我先说说我自己和C的恋爱史。早在1983年,在刚得到心理学博士学位后,我加入了我的导师Peter Ossorio刚创立的人工智能公司。公司雇佣我去做语言学分析,不过到了那里我很快就发现最需要我做的其实是编程的活。这个公司签了一个合同,要交付一个运行在当时最新的VAX计算机上的文本分析和提取系统,但他们只有一个简单的用FORTRAN写成的系统原型,而且用的还不是能被VAX上的编译器支持的标准FORTRAN,而是一种古老的、充满各种不再被支持的语言扩展的FORTRAN“方言”。我们公司的常驻数学家——他从到公司开始就在调试这些原型系统的代码——终于决定用C语言重写这些代码。“为什么用C?”,我问。“因为”,他像以往一样简洁的只说了这么一个单词,就塞给了我一本Kernigan & Ritchie的书(译者注:指两人合写的The C Programming Language)。

 

我们当时穷得租用不起VAX机器,只好共用一台PC机,它有128K的内存,和两个320K的软驱,一个驱动器访问Lattice C编译器和库,另一个存取代码。这些代码只能使用64K内存,因为MSDOS已经用了另外的64K。我记得那个8088的CPU的主频只有4.77MHz,低的让人简直要尖叫,很显然只有非常小而简洁的语言才可以运行在其上。

 

就这样,手里拿着一本K&R的书,软驱里放着一张C编译器盘,我们便开始了工作。C语言的表达能力迅速得到体现,我轻而易举地就写好了我的关于矩阵代数的代码(真正的程序员可以用任何语言来做FORTRAN语言能做的工作),而我们的常驻数学家将他的模式匹配算法用密集的指针运算来递归实现。C语言的效率也得到充分体现,因为我们的代码能够塞进紧张的内存,并且运行起来快得超过我们开始的指望。当我们在PC上确认程序工作无误后,我们计划用两个礼拜的时间把它移植到VMS机器上,为此我们租用了附近大学的VAX机器。而结果只用了两天,仅仅修改了两个小错误——因为我把int类型记成16位了——就完成了移植工作。在大学的第二天快结束的时候,我记得一个学生在我身后,看着屏幕问道:“那是什么?”

我回答说“C代码。”

“什么是C”,他问道。

“可移植的结构化汇编语言。”,我这样回答他。

“这正是这个世界所需要的”,他说。

我直到现在还不能确定当时到底他是认真的还是只是在讥讽。但我当时已经深深的、不能回头地爱上了C语言。

 

一切源于需要。——匿名

Necessity is a Mother.——Anonymous

 

那么C语言这个强大的工具是怎么产生的呢?1969年,Ken Thompson打算为他刚刚和Dennis Ritchie、Doug Mcllroy等人在PDP-7机器上创造的Unix系统编写一个FORTRAN语言编译器。Ritchie回忆道:

       我记得,Ken Thompson构造FORTRAN编译器的打算持续了一个礼拜。而他最后给出的却是一种新语言——B语言的定义和编译器。B语言受到BCPL语言的深刻影响,其它的一些特性包括Thompson简化语法的尝试和编译器必须满足的非常小的自身体积。

       类似于BCPL语言,B语言是一种没有类型的语言,提供了关于机器字操作的广泛支持,机器字可以用来保存整数、位模式、字符、数据和函数的地址。我们前面的最大公约数的地址可以很容易地转为B代码:

gcd(m, n) {

   while( m > 0 ) {

      if( n > m ) {

         auto t = m; m = n; n = t;

      }

      m = m - n;

   }

   return n;

}

      

B语言是否诠释了前面提及的那些“精神”?我认为它诠释得近乎完美。“信任程序员”和“不要阻拦程序员做那些应该做的事”这两条原则是无类型语言的自身的固有特征。而8K的空间限制和Thompson使语言变得简洁的尝试印证了“保持语言小而简洁”的原则——除了auto修饰符——那是唯一在语法上的白璧微瑕。如此简洁的语法自然保证了“任何操作只有一种方式完成”,而且,只能对机器字操作的规则保证了编译器的高效率。

 

       如此说来,难道从B语言开始的语言演化不过真是从天堂“堕落”的历程?

 

 

只是分别善恶树上的果子,你不可吃。——创世纪2:17

But of the tree of the knowledge of good and evil, thou shalt not eat…——Genesis 2:17

PDP-7是一台字寻址的机器,而PDP-11则是字节寻址的。由于B语言设计上笨拙的字节处理机制,将字节打包成字或是从字解包成字成为效率的重大障碍。同时,PDP-11的设计者当时承诺将很快提供一个浮点计算单元,但机器只有16位字长,不足以保存浮点数值。为了获得更好的性能,Dennis Ritchie决定放手一试,他在B语言中引入了char和float的类型。这事实上做了妥协,违背了C之精神的第二和第三条,为了最好的性能,牺牲了无类型编程语言的简洁性。

 

完成这个修改之后,Ritchie又进一步改进了B语言:提供了由用户自定义的struct和union类型;并令数组名在表达式中被解释成数组的首地址;同时还提供了定义类型的语法。从B语言到C语言的演化就这样轰轰列列地开始了。

 

int被作为默认的数据类型,且指针正好可以放进一个PDP-11上的int变量,这两条规则使得早期的C语言编译器可以编译大部分的B代码。Unix系统的大部分汇编代码,也被用无类型的C语言重写。就这样,我们奠定了两条后来对C和C++的演化产生深远影响的基本原则:“程序员不需要它们用不着的东西”,“改进必须向后兼容”。

 

自然,有得必有失。虽然C仍是种相当简单的语言,但复杂的类型系统也带来了更多产生错误的机会,依赖程序员主动避免错误的设计原则变得更加艰难。类似lint这样的工具逐渐涌现出来,用来检查潜在的类型错误;编译器也变得更加严格。因此C之精神的第一条原则也在被人们折中考虑,更多地依赖工具来检查错误而不是信任程序员的主观行为。

 

你是否曾怀疑那场赌博是否值得?——Joni Mitchell

Were you wondering was the gamble worth the price?——Joni Mitchell

如果说Dennis Ritchie是第一个吃苹果的人,Bjarne Stroustrup就是名副其实的Johnny Appleseed(译者注:Johnny Appleseed是美国历史上一位传奇人物,他在十九世纪沿着拓荒者的路线到处种植苹果树,历时四十年,今天美国是世界上最大的苹果种植国很大程度上要归功于他)。C++语言是准类型安全语言,这并不是坏事,但其类型体系按说是所有语言中最复杂的。在源于C的基本及衍生类型外,C++又引入了引用、继承、多继承、虚拟继承、虚函数和纯虚航数、运行时信息、函数模板,类型模板,类型推导等语言特征。C之精神的第三条被完全丢在一边,C++牺牲了简洁,获得了强大的表达能力。

 

虽然“增加面向对象机制”是把C扩展到C++的最初诱因,但最强大的扩展确是由引入模板带来的泛型编程能力。模板的引进是为了提供类型安全的容器,你可以只定义一次类似于list<T>的类模板,就可以把它用于所有类型的元素。在1994年Erwin Unruh把一段看起来很正确的小程序带到了Santa Curz(译者注:当年C++标准委员会在那里开会),而它不能编译通过,但却在编译错误提示中打印出了质数序列。记得那时我一开始觉得糊里糊涂,随即又不禁被逗乐了。当时我们其实可以通过限制模板的能力来防止这样的“元编程”,但我们还是赌了一把张开双臂接受了它,结果模板给我们带来了无尽的麻烦——因为它使语言和程序库变得乱糟糟。

 

类型安全是否意味着程序员不再被信任了呢?这很难说。在C语言所有的无定义行为(译者注:指一些没有被C标准明确规定的语言特性,例如在栈上定义的变量的初值)在C++中仍然没有被限制。在我看来, 一个愚蠢的家伙仍然可以用C++产生无止境的伤害。但C++同样没有限制住熟练的程序库设计者,他们可以在简单、安全和优雅的C++接口背后隐藏复杂的实现细节。特别是泛型编程,那是一个令人称奇的工具,能够在简单的接口之后实现最为高效的实现复杂的功能。因此我觉得我们当年的那场赌博并没有输。

 

朴素的橡树

(译者注:Java原来的开发代号是橡树“Oak”)

因为那混沌之环正在逼近王国,英雄找到了巨人,并用武力制服了他,令他说出如何驱赶混沌的秘密。巨人说:给我你的左眼球,我就告诉你。英雄深爱着那些正在担惊受怕中度日的人民,为此他没有丝毫犹豫。他抠出自己的左眼球交给了巨人。巨人说:控制混沌的方法便是用双眼注视它。——John Gardner

 

       Java诞生于那轰轰列列的浏览器争霸的鏖战中。当时人们产生了对可以在不安全的浏览器中安全地被编译运行的可移植编程语言的商业需求,而且人们希望这种语言语法简单,能够被当时的程序员快速的学会。至今仍神秘的“橡树”计划,这个最初面向类似于有线电视机顶盒的设备的项目,在被改名为Java后,由Sun微系统公司投入了众多资源推向市场。正如已经发生的一样,Java并没有在浏览器插件领域取得突破,却在基于网页的电子商务服务器和期待便宜、简单的面向对象编程语言的教育领域得到广泛关注。

 

Java的设计特别强调了前述“C之精神”的第三款和第四款,为了安全性和简洁性牺牲了计算能力和效率。Java不仅是完全类型安全的,亦没有任何的无定义的行为。这种安全性由语言本身和Java虚拟机同时保证,因此恶意代码不可能对机器造成损害。有鉴于此,Java提供了足以完成大部分计算工作所需求的表达能力。但例外的情形是,对于需要完全控制硬件或是最高效利用机器资源的场合,人们仍然必须使用更为底层的C、C++或是汇编语言来编写程序。

 

       那么Java语言本身高度的安全性是否意味着程序员不再被信任了呢?至少,Java的自动内存管理机制和取消指针运算意味着不可能再出现“野指针”或是“悬挂指针”问题。但简而言之,对上面问题的答案是否定的,这里有三个例子证明:

 

首先,在某些情况下,自动的内存管理实际上使得程序的内存需求量更难被控制。在Java语言的各实现版本所收到的Bug报告中,占最大比重的便是关于垃圾回收器本身的内存泄漏问题。但是这些Bug报告大多数是不正确的,问题的原因是那些Java代码本身持有了那些不需要的对象。而如果要解决这一问题又会影响效率,因为重量级的对象会被不断重新构造,而不是一直保存在内存中。因此许多缓存机制被引入,来预防有价值的对象被提前不恰当地释放,这也正是系统类WeakReference,SoftReference, PhantomReference, Reference Queue和WeakHashMap的来由。

 

其次,在栈上分配的内存以外的其它资源比内存更难管理。比如我自己的第一个Java程序就很快地用完了文件描述符:

void scanFile(String name, Filter filter) {

   File file = new File(name);

   filter.scan(file);

}

这个错误明显地让人难堪,而这种难堪却也是常有的事。我其后不久就学到了用finally块来应对Java中没有析构函数的问题。

 

       再次,虽然Java中没有任何未定义的行为,死锁和活锁的情况却总是让人难堪地轻易发生。Java没有提供预防死锁的语言级别机制来实现并发控制,仅仅提供了底层的同步原语,把控制冲突发生的责任留给了程序员。早在Per Brinch Hansen设计Concurrent Pascal(译者注:加入并发控制语义的Pascal语言)的时候就引入了一种简单的监视机制来控制并发,三十年过去了,并发安全领域已经取得了长足的进步,可是不知道因为什么理由,Java居然对这些视而不见。

 

上面说的头两个例子说明了所谓Java对象生命周期控制要比C++容易的观点不过是个神话而已,更何况Java的线程比Unix系统的进程更难被正确使用。但是尽管存在这些限制,Java已经被证明是一种成功的语言。它在令人惊讶的广阔范围内,兑现了令程序安全、可移植的承诺。

 

对你满心期待的事物应当保持谨慎——匿名

Be careful what you wish for——Anoymous

 

Java,作为一种全新的语言,相比于C++,更为清爽由C演化而成。却。尽管如此,它所带来的高度简洁性却是以凭借着为C和C++所不能接受的性能的速度的开销。那么人们是否会希望C++能变得更加简单一些?

 

我相信“从优雅的B语言开始的堕落”(译者注:指从B语言演化到C以及C++)带来的最大损失是丢掉了无类型编程的简洁。这些损失是必要的,在C的时代这种损失是可以接受的,但到了C++,其复杂性变成了吓人的屏障。许多的复杂性的引入是为了支持泛型编程,许多技术高手将模板的语法发展到远远超过了模板最初被设计出来的目的。有点似是而非的结论是:复杂的模板机制激进地简化了语法。

 

泛型编程的威力来源于编译器能够根据上下文推导类型。极端地说,这种能力甚至可以免去定义类型的必要性。例如,一个不需要知道类型的求最大公约数代码看起来是这样的:

template<typename T>

T gcd(T m,T n){

   while( m > 0 ) {

      if( n > m )

         swap(m,n);

      m = m - n;

   }

   return n;

}

这看起来并不坏,但随着类型数量的增加事情很快变得很糟糕,并且这几乎不可能表示结果类型依赖于两个以上参数类型的情况。因此我反而希望模板的语法不再是语法强制的要求,而让编译器来干这个活:

gcd(m,n){

   while( m > 0 ) {

      if( n > m )

         swap(m,n);

      m = m - n;

   }

   return n;

}

如此地清爽和简洁,一如从前的B语言。同时它还非常的安全,因为编译器可以保证所有推导出来的类型是匹配的(artima编辑注:这正是D语言中的模板的工作机制!)

 

我真有点忍不住列出对语言更多的期望,不过我想那该是我下一篇文章的内容了吧。

 

致谢
感谢Angelika Langer对本文第一稿的评论。

本文地址:http://com.8s8s.com/it/it25623.htm