《编程之魂》样章试读-第一章 C++ 采访 Bjarne Stroustrup(1)

在诸多语言之中,C++的地位非常有趣:它构建在C语言的基础之上,融入了Simula面向对象的思想;由ISO实现了标准化;而且,它还遵循“你不会为没用的东西白花冤枉钱”和“支持用户自定义和内置类型同等重要”的设计理念。尽管在20世纪80年代和90年代,C++曾广泛用于OO(面向对象)和GUI(图形用户接口)编程,不过,它对软件的最大贡献还是其无处不在的泛型编程技术,它的标准模板库(Standard Template Library)就是例证。Java和C#等一些更新的语言都试图取代C++,不过C++即将发布一个修订版标准,其中就增加了期待已久的新特性。Bjarne Stroustrup是C++语言的创始人,而且他至今仍是C++最坚定的倡导者之一。

1.1  设计决策

为什么您会选择对现有语言进行扩展,而不是创建一种新语言呢?

Bjarne Stroustrup:我在1979年刚开始设计语言时,目的就是帮助程序员构建系统。现在还是这样。一种语言如果想要真正有助于解决问题,而不仅仅用于学术,它对于应用领域来说就必须是完美无缺的。也就是说,一种非研究性的语言,它的使命就是要解决问题。我这里所要解决的问题,是与操作系统设计、组网以及仿真有关的那些问题。我和同事需要一种语言,它不仅能像Simula语言那样表示程序结构(人们大多愿意称它为面向对象编程),还可以像C语言那样编写出高效的底层代码。在1979年,还没有什么语言能够兼具这两种功能,否则我就会直接拿来用了。我并没有特别想去设计一种新的编程语言,我只不过想要帮助解决几个问题而已。

所以,在现有语言的基础上构建新语言就很有意义。你可以使用基础语言的基本句法、语义结构,还可以使用它的库,而且你自己也会融入这种文化。如果当时不是以C为基础,我也会在其他语言的基础上构建C++。那我为什么选择了C呢?我在Bell实验室计算机科学研究中心(Computer Science Research Center)与Dennis Ritchie、Brian Kernighan和其他Unix大师比邻办公,因此,这个问题好像有些多余。不过,我在这个问题上是非常认真的。

特别是,C的类型系统是非形式化和弱强制型的(正如Dennis Ritchie所言,“C是一种强类型、弱检查的语言”)。“弱检查”曾经令我头疼不已,直到现在它仍为C++程序员制造了很多麻烦。而且,当时C语言的应用尚没有今天如此广泛。在C的基础上构建C++,这一方面表明了我对以C为基础的计算模型的信心(“强类型”部分)(译注 ),另一方面也表明了我对同事们的信任。当时,系统编程使用的大多是更高级的编程语言,我是基于这些语言的知识(既是用户,又是实现者),做出了这样的选择。值得一提的是,当时大多数是“接近硬件”工作,而且对性能有严格要求的仍用汇编来完成。Unix在很多方面都有重大突破,其中包括用C完成了最苛求系统的编程任务。

因此,我才选择了用C作为机器的基本模型,而不是选择“强检查”类型的系统。我实际上想用Simula的类作为程序框架,因此,我把它们映射成C的内存和计算模型。结果,它不仅极具表现力和灵活性,而且运行速度很快,甚至可以和没有大规模运行时系统支持的汇编程序相媲美。

为什么您会选择支持多种范式(paradigm)

Bjarne:因为各种编程风格的组合通常会生成最好的代码,这里“最好”的意思是该代码能够最直接地表达设计思想、运行速度更快、可维护性最强等。当人们质疑这种说法时,通常他们要么是定义自己喜欢的编程风格,将每一个有用的结构(例如,“泛型编程只不过是OO的一种方式而已”)包括进去,要么就是限制应用范围(例如,“每个人都要使用1GHz、1GB的机器”)。

Java只关注面向对象编程。这是否会让Java代码在某些情况下变得更为复杂,从而C++可以利用泛型编程取而代之吗?

Bjarne:噢,Java设计者(很可能Java营销人员更是如此)把OO吹捧到荒谬可笑的地步。在号称纯正而且简单的Java刚出现时,我就曾经预言:如果Java能成功的话,它就会在规模和复杂性方面显著增长。结果不出所料,确实如此。

例如,在容器外获得一个值时(例如,(Apple)c.get(i)),会使用“造型(cast)”来转换Object,之所以使用这种非常荒谬的方法,是因为无法假定该容器的对象为何种类型。它不仅繁琐冗长,而且效率低下。现在,Java使用的是泛型,因此它只不过稍微有点慢而已。语言复杂性的增加(帮助程序员)还有其他例证,比如说枚举、反射和内部类等。

复杂性是不可避免的,这是一个简单的事实:如果它没有出现在语言定义中,就会出现在数以千计的应用程序和库中。同样,Java会将每一个算法(运算)都放入类中,这种困扰会导致出现这样的荒唐事:仅仅是由静态函数组成而没有数据的类。这就是在数学里使用f(x)和f(x,y),而不是x.f()、x.f(y)或(x,y)f()的原因所在:后者试图表达两个参数“真正的面向对象方法”的理念,并试图避免x.f(y)固有的不对称性。

C++通过将数据抽象和泛型编程技术相结合,使用面向对象方法,解决了很多逻辑和符号的问题。vector <T>就是一个典型的例子,其中T是可以复制的任何类型:包括内置类型、OO层次的指针,以及用户自定义的类型,比如字符串和复数等。完成这项工作,既没有增加运行时开销,也没有对数据规划附加限制,更没有对标准库组件使用特殊规则。还有一个例子是需要访问两个类的运算,它也不符合传统的单分派的OO层次模型,比如操作符*(Matrix(矩阵)、Vector(向量))等,该运算并不是任何一个类天然的“方法”。

C++和Java的一个根本区别是指针实现的方式。在某种意义上,您可以说Java并没有真正的指针。这两种方式之间有什么区别呢?

Bjarne:噢,Java当然有指针。事实上,Java中的每个操作几乎都暗含一个指针。它们只是把这些“指针”称为引用而已。这种隐式指针有利有弊。使用真正的本地对象(像在C++中一样)也各有优缺点。

C++选择支持栈分配(stack-allocated)的局部变量和各种类型的真正的成员变量,这样可以具有好的统一语义(uniform semantics),同时也支持值语义(value semantics)的概念,可以实现紧凑布局和最小的访问成本,而且它还是C++支持一般资源管理(general resource management)的基础。这是主要的,而且Java到处使用隐式指针(亦称 引用)关闭了通向这一切的所有大门。

考虑布局的折衷平衡:在C++中,vector <complex>(10)表示为自由存储区中一个10个复数数组的句柄。它总共有25个字:3个字用于向量,20个词用于复数,再加上一个用于自由存储区(堆内存)数组的两个字头。在Java中,则应该是56个字(用于一个用户自定义类型对象的用户自定义的容器):1个字用于容器引用,3个字用于容器,10个字用于对象引用,20个字用于对象,还有24个字用于12个独立分配对象的自由存储头。显然,这些数字是近似值,因为自由存储区(堆内存)的开销就是在两种语言中定义的实现。不过,结论是非常清楚的:通过无处不在的隐式引用,Java可能已经简化了编程模型和垃圾收集器的实现,不过它大大地增加了内存开销,并成比例地增加了内存访问成本(需要更多的间接访问)和分配费用开销。

C和C++可能会通过指针算术运算误用指针,而Java并不具有这种能力,这对Java来说也是好事。不过如果C++程序写得很好,也不会遇到这样的问题:人们使用更高级别的抽象,比如iostream(译注 )、容器和算法,而不是乱用指针。本质上,所有的数组和大多数指针应深藏于大多数程序员并无必要看到的实现之中。不幸的是,大量写得很糟的和多余的底层C++程序随处可见。

不过,有一个重要的场合使用指针(和指针操作)会非常方便:直接、高效的数据结构表达。它没有Java引用:例如,你无法用Java表示交换运算。另一个例子是简单地使用指针来直接访问底层(实际)内存;对于每一个系统来说,一些语言不得不这样做,而且这种语言通常就是C++。

拥有指针(和C风格的数组)的“负面影响”当然是潜在滥用的可能:缓冲区溢出、指向已删除内存的指针、未初始化的指针等。不过,在写得很好的C++程序中,这不是主要问题。你只不过是没有碰到在抽象之内使用的指针和数组(比如向量、字符串和映射等)的那些问题。限定作用域的资源管理满足了大多数的需要;大多数遗留问题都可以使用智能指针和专门的句柄来处理。以前主要使用C或旧版C++的人对此很难相信,不过基于限定作用域的资源管理这个工具功能非常强大,而且它还是用户自定义的,自身带有的合适的运算,可以使用比不安全的老式hack程序(译注 )更少的代码解决经典问题。例如,以下是典型的缓冲区溢出和安全性问题的最简单的形式:

char buf[MAX_BUF];
gets(buf); // Yuck!
使用一个标准库字符串,问题会迎刃而解:

string s;
cin>> s;//读取使用空白字符分开的字符

这些显然是不起眼的小例子,不过使用合适的“字符串”和“容器”可能会基本满足所有需求,而且标准库还可以让你轻松上手。

您前面提到“值语义”和“一般资源管理”,这是什么意思?

Bjarne:“值语义”通常是指这样的类:其中的对象属性在复制时,你会得到两个独立的副本(具有相同的值)。

当然,对于常见的数值类型,我们就是这样处理的,比如说ints、doubles和复数等数值类型,以及向量等数学抽象。这是一个非常有用的想法,在这里C++支持内置类型和我们想要的任何用户自定义的类型。它和Java区别很大,Java会支持char和int等内置类型,而不支持用户自定义的类型,而实际上也确实无法支持那些类型。正如在Simula中一样,Java中用户自定义的所有类型都有引用语义。在C++中,如果一种类型的语义需要,程序员也可以支持引用语义。在支持带有值语义的用户自定义类型方面,C#(不完全)遵从C++。

“一般资源管理”是指让一个对象拥有一种资源(例如,文件句柄或者锁)的流行技术。如果该对象是一个限定作用域的变量,它的生命周期就是对持有资源时间的一个最大限制。典型的情形是,构造器获取资源和析构器释放资源。这通常被称为RAII(Resource Acquisition Is Initialization,资源获取初始化),而且它会和使用异常的错误处理完美地集成在一起。显然,并不是每一种资源都可以用这种方式来处理,不过仍然有很多资源可以使用这种方式,而且对于那些资源来说,资源管理变得既隐蔽,又高效。
posted @ 2010-08-20 11:05  博文视点  阅读(526)  评论(0编辑  收藏  举报