C++沉思录(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-17 12:33:28

点击下载

作者:(美)Andrew Koenig,Barbara Moo

出版社:信息技术第一出版分社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

C++沉思录

C++沉思录试读:

前言

原由

1988年初,大概是我刚刚写完C Traps and Pitfalls(本书中文版《C陷阱与缺陷》由人民邮电出版社出版)的时候,Bjarne Stroustrup跑来告诉我,他刚刚被邀请参加了一个新杂志的编委会,那个杂志叫做《面向对象编程月刊》(Journal of Object-Oriented Programming,JOOP)。该杂志试图在那些面孔冰冷的学术期刊与满是产品介绍和广告的庸俗杂志之间寻求一个折中。他们在找一个C++专栏作家,问我是否感兴趣。

那时,C++对于编程界的重要影响才刚刚开始。Usenix其时才刚刚在新墨西哥圣达菲举办了第一届C++交流会。他们预期有50人参加,结果到场的有200人。更多的人希望搭上C++快车,这意味着C++社群急需一个准确而理智的声音,去对抗必然汹涌而至的谣言大潮。需要有个人能够在谣言和实质之间明辨是非,在任何混乱之中保持冷静的头脑。无论如何,我顶了上去。

在写下这些话的时候,我正在构思我为JOOP撰写的第63期专栏。这个专栏每期或者每两期就会刊登。其间,我也有过非常想中断的时候,非常幸运的是,Jonathan Shopiro接替了我。偶尔,我只是写一些当期专栏的介绍,然后到卓越的丹麦计算机科学家Bjørn Stavtrup[1]那里去求助。此外,Livleen Singh曾跟我谈起为季刊C++Journal撰写稿件的事,那个杂志在发行6期之后停刊了。Stan Lippman也甜言蜜语地哄着我在C++ Report上开了个专栏,当时这本杂志刚刚从一分简陋的通信时刊正式成为成熟的杂志。加上我在C++Report上发表的29篇专栏文章,我一共发表了98篇文章。

在这么多的杂志刊物里,分布着大量的材料。如果这些文章单独看来是有用的,那么集结起来应该会更有用。所以,Barbara[2]和我(主要是Barbara)重新回顾了所有的专栏,选择出其中最好的,并根据一致性和连续性的原则增补和重写了这些文章。本书正是世界所需的又一本C++书籍

既然你已经知道了本书的由来,我就再讲讲为什么要读这本书,而不是其他的C++书籍。天知道!C++方面的书籍太多了,为什么要选这一本呢?

第一个原因是,我想你们会喜欢它。大部分C++书籍都没有顾及到这点:它们应该是基于科目教学式的。吸引人最多不过是次要目标。

杂志专栏则不同。我猜想肯定会有一些人站在书店里,手里拿着一本JOOP,扫一眼我Koenig的专栏之后,便立刻决定购买整本杂志。但是要是我自认为这种情况很多的话,就未免太狂妄自大了。绝大多数读者都是在买了书之后读我的专栏的,也就是说他们有绝对的自由来决定是否读我的专栏。所以,我得让我的每期专栏都货真价实。

本书不对那些晦涩生僻的细节进行琐碎烦人的长篇大论。初学者不应该指望只读这本书就能学会C++。具备了一定基础的人,比如已经知道几种编程语言的人,以及已经体会到如何通过阅读代码推断出一门新语言的规则的人,将能够通过本书对C++有所了解。大部分从头开始学的读者先读Bjarne Stroustrup的The C++Programming Language(Addison-Wesley 1991)或者Stan Lippman的C++Primer(Addison-Wesley 1991),然后再读这本书,效果可能会更好。[3]

这是一本关于思想和技术的书,不是关于细节的。如果你试图了解怎样用虚基类实现向后翻腾两周半,就请到别处去找吧。这里所能找到的是许多等待你去阅读分析的代码。请试一试这些范例。根据我们的课堂经验,想办法使这些程序运行起来,然后加以改进,能够很好地巩固你的理解。至于那些更愿意从分析代码开始学习的人,我们也从本书中挑选了一些范例,放在ftp.aw.com的目录cseng/authors/koenig/ruminations下,可以匿名登录获取。

如果你已经对C++有所了解,那么本书不仅能让你过一把瘾,而且能对你有所启示。这也是你应该阅读本书的第二个原因。我的意图并不是教C++本身,而是想告诉你用C++编程时怎样进行思考,以及如何思考问题并用C++表述解决方案。知识可以通过系统学习获取,智慧则不能。组织

就专栏来说,我尽力使每期文章都独立成章,但我相信,对于结集来说,如果能根据概念进行编排,将更易于阅读,也更有趣味。因此,本书划分为6篇。

第一篇是对主题的扩展介绍,这些主题将遍布本书的其余部分中。本部分中没有太多的代码,但是所展现的有关抽象和务实的基本思想贯穿本书,更重要的是,这些思想渗透了C++设计原则和应用策略。

第二篇着眼于继承和面向对象编程,大多数人都认为这些是C++中最重要的思想。你将知道继承的重要性何在,它能做什么。你还会知道为什么将继承对用户隐藏起来是有益的,以及什么时候要避免继承。

第三篇探索模板技术,我认为这才是C++里最重要的思想。我之所以这样认为,是因为这些模板提供了一种特别的强大的抽象机制。它们不仅可以构造对所包含的对象类型一无所知的容器,还可以建立远远超出类型范畴的泛型抽象。

继承和模板之所以重要的另一个原因是,它们能够扩展C++,而不必等待(或者雇佣)人去开发新的语言和编译器。进行扩展的方法之一就是通过类库。第四篇谈到了库——包括库的设计和使用。

对基础有了很好的理解以后,我们可以学习第五篇中的一些特殊编程技术了。在这部分,你可以知道如何把类紧密地组合在一起,或者把它们尽可能地分离开。

最后,在第六篇,我们将返回头来对本书所涉及到的内容做一个回顾。编译和编辑

这些经年累月写出来的文章有一个缺陷,就是它们通常都没有用到语言的现有特性。这就导致了一个问题:我们是应该在C++标准尚未最终定稿的时候,假装ISO C++已经成熟了,然后重写这些专栏,还是维持古迹,保留老掉牙的过时风格呢?[4]

还有许多这样的问题,我们选择了折中。对那些原来的栏目有错的地方——无论是由于后来语言规则的变化而导致的错误,还是由于我们看待事物的方式改变而导致的错误——我们都做了修正。一个很普遍的例子就是对const的使用,自从const加入到语言中以来,它的重要性就在我们的意识中日益加强。

另一方面,例如,尽管标准委员会已经接受bool作为内建数据类型,这里大量的范例还是使用int来表示真或者假的值。这是因为这些专栏文章早在这之前就完成了,使用int作为真、假值还将继续有效,而且要使绝大多数编译器支持bool还需要一些年头。致谢

除了在JOOP、C++ Report、C++ Journal中发表我们的观点外,我们还在许多地方通过发表讲演(和听取学生的意见)来对它们进行提炼。尤其值得感谢的是Usenix Association和SIGS Publications举办的会议,以及JOOP和C++Report的发行人。另外,在Western Institute in Computer Science的赞助下,我们俩在斯坦福大学讲授过多次单周课程,在贝尔实验室我们为声学研究实验室和网络服务研究实验室的成员讲过课。还有Dag Brück曾为我们在瑞典组织了一系列的课程和讲座。Dag Brück当时在朗德理工学院自动控制系任教,现在在Dynasim AB。

我们也非常感谢那些阅读过本书草稿以及那些专栏并对它们发表意见的人:Dag Brück、Jim Coplien、Tony Hansen、Bill Hopkins、Brian Kernighan(他曾笔不离手地认真阅读了两遍)、Stan Lippman、Rob Murray、George Otto和Bjarne Stroustrup。

如果没有以下人员的帮助,这些专栏永远也成不了书。他们是 Deborah Lafferty、Loren Stevens、Addison-Welsey的Tom Stone以及本书编辑Lyn Dupré。

我们特别感谢AT&T开通的经理们,是他们使得编写这些专栏并编辑成书成为可能。他们是Dave Belanger、Ron Brachman、Jim Finucane、Sandy Fraser、Wayne Hunt、Brian Kernighan、Rob Murray、Ravi Sethi、Bjarne Stroustrup和Eric Sumner。Andrew KoenigBarbara Moo新泽西州吉列1996年4月[1].就是C++创造者Bjarne Stroustrup,这里可能是丹麦文。——译者注[2].本书合作者Barbara Moo是Andrew Koenig的夫人,退休前是Bell实验室高级项目管理人员,曾负责Fortran和CFront编译器的项目管理。——译者注[3].这两本C++百科大全类的名著分别于1997年和1998年推出了各自的第三版,Bjarne Stroustrup还于2000年推出了The C++ Programming Language特别版。——译者注[4].本书编写于1996年底,当时C++标准已经发布了草案第二版,非常接近最终标准。次年(1997),C++标准正式定稿。本书内容是完全符合C++标准的。——译者注大师的沉思——读C++经典著作Ruminations on C++有感

人民邮电出版社即将推出C++编程领域的又一部经典著作 Ruminations on C++中文版——《C++沉思录》。作为一个普通的C++程序员,我很荣幸能有机会成为本书中文版的第一个读者,先饱眼福。原书英文版我也有,虽然也不时拿出来翻看,但是随意的摘读与通篇的浏览不同,通篇的浏览与技术审校又不同,此番对照中英文,从头到尾把此书读过一遍,想过一遍,确实是收获丰厚,感慨良多。

本书作者,不需要我过多的介绍。虽然我们不赞成论资排辈的习气,但是所谓公道自在人心,Andrew Koenig在C++发展历史中不可置疑的权威地位,是勿庸置疑的。作为Bjarne Stroustrup的亲密朋友,ANSI C++标准委员会的项目编辑,Koenig在C++的整个发展过程中都发挥了极其重要的作用,是C++社群中最受尊敬的大师之一。特别值得一提的是,在C++大师中,Koenig的教学实践和文字能力历来备受好评,在前后十几年的时间里,他在各大技术刊物上发表了近百篇C++技术文章。这些文章长时间以来以其朴实而又精深的思想,准确而又权威的论述,高屋建瓴而又平易近人的表达方式,成为业界公认的“正统 C++之声”。本书第二作者 Barbara Moo是 Koenig的夫人,也是他在贝尔实验室的同事,曾经领导AT&T的Fortran77和CFront编译器项目,可谓计算机科学领域中的巾帼英雄。这本书正是在Barbara Moo的建议下,由两人共同从Koenig所发表的文章中精选、编修、升华而成的一本结集之作。由于源自杂志的专栏文章,因此书中的内容具有高度的可读性,知识密度高,表现力强。更重要的是,这些文章是在发表之后若干年,由原作者挑选出来,经过了多年的沉淀和反思,重新编辑整理,加上自己多年的心得与思考,自然有一种千锤百炼的韧性和纯度。也正因为如此,作者当仁不让地把这本书命名为Ruminations on C++,rumination 一词,充分显现出作者的自信和对这本书的珍爱。

这两位C++发展史上的重要人物夫唱妇随,一同出版著作,本身就足以引起整个C++社群的高度重视,而这本书不平凡的来历和出版之后5年间所获得的极高赞誉,更加确立了它在C++技术书籍中的经典地位。Bjarne Stroustrup在他的主页上特别推荐人们去阅读这本书,ACCU的主席Francis Glassborow在书评中慷慨地向读者最热诚地推荐此书,说“我对这本书没什么更多可说的,因为每个C++程序员都应该去读这本书。如果你在阅读的过程中既没有感到快乐,又没学到什么东西,那你可真是罕见的人物”。而著名C++专家Chuck Allison在他自己的书C & C++ Code Capsules(本书中文版《C和C++代码精粹》将由人民邮电出版社出版,编者注)中,更是直截了当地说:“对我来说,这是我所有C++藏书中最好的一本。”

对我来说,给这本书一个合适的评价超出了我现在的能力。究竟它能够为我的学习和工作带来怎样的启发,还需要更长时间的实践来验证。不过就目前而言,这本书的一些特色已经给我留下很深刻的印象。

首先,作者对C++有着居高临下的见识,对于C++的设计理念和实际应用有非常清晰的观点。众多纷繁复杂的C++特性如何组合运用,如何有效运作,什么是主流,什么是旁支,哪些是通用技术,哪些是特殊的技巧,在书中都有清晰明白的介绍。我们都知道,C++有自己的一套思想体系,它虽然有庞大的体积,繁多的特性,无穷无尽的技术组合能力,但是其核心理念也是很朴实、很简单的。掌握了C++的核心理念,在实践中就会“有主心骨”,有自己的技术判断力。但是在很多 C++书籍,甚至某些经典名著中,C++的核心理念被纷繁的技术细节所遮掩,变得模糊不清,读者很容易偏重于技术细节,最后陷入其中,不能自拔。而在这本书中,作者毫不含糊地把C++的核心观念展现在读者面前,为读者引导方向。全书中多次强调,C++最基本的设计理念就是“用类来表示概念”,C++解决复杂性的基本原则是抽象,面向对象思想是C++的手段之一,而不是全部,等等。这些言论可以说是掷地有声,对我们很多程序员来说都是一剂纠偏良药。

其次,这本书在C++的教学方式上有独到之妙。作者循循善诱,娓娓道来,所举的例子虽然小,但是非常典型,切中要害,让你花费不大的精力就可以掌握相当多的东西。比如本书讲述面向对象编程时先后只讲了几项技术,举了两个例子,但是细细读来,你会对C++面向对象编程有一个基本的正确观念,知道应该用具体类型来隐藏一个派生层次,知道应该如何处理动态内存管理的问题。从这一点点内容中能够得到的收获,比看一大堆厚书都来得清晰深刻。对于STL的介绍,更是独具匠心。作者不是一上来就讲STL,而是把STL之前的那些类似技术一一道来,把优点缺点讲清楚,然后从道理上给你讲清楚STL的设计和运用,让你不仅知其然而且知其所以然,胸有成竹。

书毕竟不厚,我想更重要的东西并不是这本书教给了你什么技术。所谓授人以鱼不如授人以渔。这本书最大的特点就在于,不仅仅告诉你什么是答案,更重要的是告诉你思考的方法,解决问题的步骤和方向。书中遍布了大量宝贵的建议,正是这些建议,为这本书增添了永不磨灭的价值。Francis Glassborow甚至说,仅仅这本书的第32章给出的建议,就足以体现全书的价值。

当前,C++面临其发展历史中的一个非常重要的时期。一方面,它受到了不公正的质疑和诋毁,个别新兴语言的狂热拥护者甚至迫不及待地想宣布C++的死讯。而另一方面,C++在学术界和工业界都在稳定地发展,符合ISO标准的C++编译器呼之欲出,人们对于C++特性的合理运用的认识也越来越丰富,越来越成熟和全面。事实上,根据我个人从业界了解到的情形,以及从近期C++的出版物的内容和质量上看,C++经过这么多年的积淀,已经开始真正的成熟发展时期,它的步子越来越稳健,思路越来越清晰,越来越演化成为一种强大而又实用的编程语言。作为工业界的基础技术,C++还将在很长的一段时间里扮演不可替代的重要角色。因此,这本书也会在很长的时间里伴随我们的学习与实践,并且引导我们以正确的观点看待技术的发展,帮助我们中国程序员形成属于我们自己的、成熟的、独立的技术判断力。孟岩2002年10月第0章序幕

有一次,我遇到一个人,他曾经用各种语言写过程序,唯独没用过C和C++。他提了一个问题:“你能说服我去学习C++,而不是C吗?”,这个问题还真让我想了一会儿。我给许许多多人讲过C++,可是突然间我发现他们全都是C程序员出身。到底该如何向从没用过C的人解释C++呢?

于是,我首先问他使用过什么与 C 相近的语言。他曾用 Ada[1]编写过大量程序——但这对我毫无用处,我不了解 Ada。还好他知道 Pascal,我也知道。于是我打算在我们两个之间有限的共通点之上找到一个例子。

下面看看我是如何向他解释什么事情是C++可以做好而C做不好的。0.1 第一次尝试

C++的核心概念就是类,所以我一开始就定义了一个类。我想写一个完整的类定义,它要尽量小,要足够说明问题,而且要有用。另外,我还想在例子中展示数据隐藏(data hiding),因此希望它有公有数据(public data)和私有数据(private data)。经过几分钟的思索,我写下这样的代码:# include class Trace {public:void print(char* s) { printf("%s",s); }};

我解释了这段代码是如何定义一个名叫 Trace的新类,以及如何用 Trace 对象来打印输出消息:int main(){Trace t;t.print("begin main()\n");// main函数的主体t.print("end main()\n");}

到目前为止,我所做的一切都和其他语言很相似。实际上,即使是C++,直接使用printf也是很不错的,这种先定义类,然后创建类的对象,再来打印这些消息的方法,简直舍近求远。然而,当我继续解释类Trace定义的工作方式时,我意识到,即便是如此简单的例子,也已经触及到某些重要的因素,正是这些因素使得C++如此强大而灵活。0.1.1 改进

例如,一旦我开始使用Trace类,就会发现,如果能够在必要时关闭跟踪输出(trace output),这将会是个有用的功能。小意思,只要改一下类的定义就行:#include class Trace {public:Trace() {noisy = 0; }void print(char* s) { if (noisy) printf("%s",s); }void on() { noisy = 1; }void off() { noisy = 0; }private:int noisy;};

此时类定义包括了两个公有成员函数on和off,它们影响私有成员noisy的状态。只有noisy为on(非零)才可以输出。因此,t.off();

会关闭t的对外输出,直到我们通过下面的语句恢复t的输出能力:t.on();

我还指出,由于这些成员函数定义在Trace类自身的定义内,C++会内联(inline)扩展它们,所以就使得即使在不进行跟踪的情况下,在程序中保留Trace对象也不必付出许多代价。我立刻想到,只要让print函数不做任何事情,然后重新编译程序,就可以有效地关闭所有Trace对象的输出。0.1.2 另一种改进

当我问自己“如果用户想要修改这样的类,将会如何?”时,我获得了更深层的理解。

用户总是要求修改程序。通常,这些修改是一般性的,例如“你能让它随时关闭吗?”或者“你能让它打印到标准输出设备以外的东西上吗?”我刚才已经回答了第一个问题。接下来着手解决第二个问题,后来证明这个问题在C++里可以轻而易举地解决,而在C里却得大动干戈。

我当然可以通过继承来创建一种新的Trace类。但是,我还是决定尽量让示例简单,避免介绍新的概念。所以,我修改了Trace类,用一个私有数据来存储输出文件的标识,并提供了构造函数,让用户指定输出文件:#include class Trace {public:Trace() { noisy = 0; f = stdout; }Trace (FILE* ff) { noisy = 0; f = ff; }void print(char* s){ if (noisy) fprintf(f,"%s",s); }void on() { noisy = 1; }void off() { noisy = 0; }private:int noisy;FILE* f;};

这样改动,基于一个事实:printf(args);

等价于:fprintf(stdout,args);

创建一个没有特殊要求的Trace类,则其对象的成员f为stdout。因此,调用fprintf所做的工作与调用前一个版本的printf是一样的。

类Trace有两个构造函数:一个是无参构造函数,跟上例一样输出到stdout;另一个构造函数允许明确指定输出文件。因此,上面那个使用了Trace类的示例程序可以继续工作,但也可以将输出定向到比如说stderr上:int main(){Trace t(stderr);t.print("begin main()\n");// main 函数的主体t.print("end main()\n");}

简而言之,我运用C++类的特殊方式,使得对程序的改进变得轻而易举,而且不会影响使用这些类的代码。0.2 不用类来实现

此时,我又开始想,对于这个问题,典型的C解决方案会是怎样的。它可能会从一个类似于函数trace()(而不是类)的东西开始:#include void trace(char *s){printf("%s\n",s);}

它还可能允许我以如下形式控制输出:#include static int noisy = 1;void trace(char *s){if(noisy)printf("%s\n",s);}void trace_on() { noisy = 1; }void trace_off() { noisy = 0; }

这个方法是有效的,但与C++方法比较起来有3个明显的缺点。

首先,函数trace不是内联的,因此即使当跟踪关闭时,它还保持着函数调用的开销[2]。在很多C的实现中,这个额外负担都是无法避免的。

第二,C版本引入了3个全局名字:trace、trace_on和trace_off,而C++只引入了1个。

第三,也是最重要的一点,我们很难将这个例子一般化,使之能输出到一个以上的文件中。为什么呢?考虑一下我们会怎样使用这个trace函数:int main(){trace("begin main()\n");// main 函数主体trace("end main()\n");}

采用C++,可以只在创建Trace对象时一次性指定文件名。而在C版本中,情况相反,没有合适的位置指定文件名。一个显而易见的办法就是给函数trace增加一个参数,但是需要找到所有对trace函数的调用,并插入这个新增的参数。另一种办法是引入名为trace_out的第4个函数,用来将跟踪输出转向到其他文件。这当然也得要求判断和记录跟踪输出是打开还是关闭。考虑一下,譬如,main调用的一个函数恰好利用了trace_out向另一个文件输出,则何时切换输出的开关状态呢?显然,要想使结果正确需要花费相当的精力。0.3 为什么用C++更简单

为什么在C方案中进行扩展会如此困难呢?难就难在没有一个合适的位置来存储辅助的状态信息——在本例中是文件名和“noisy”标记。在这里,这个问题尤其让人恼火,因为在原来的情况下根本就不需要状态信息,只是到后来才知道需要存储状态。

往原本没有考虑存储状态信息的设计中添加这项能力是很难的。在C中,最常见的做法就是找个地方把它藏起来,就像我这里采用“noisy”标记一样。但是这种技术也只能做到这样;如果同时出现多个输出文件来搅局,就很难有效控制了。C++版本则更简单,因为C++鼓励采用类来表示类似于输出流的事物,而类就提供了一个理想的位置来放置状态信息。

结果是,C倾向于不存储状态信息,除非事先已经规划妥当。因此,C程序员趋向于假设有这样一个“环境”:存在一个位置集合,他们可以在其中找到系统的当前状态。如果只有一个环境和一个系统,这样考虑毫无问题。但是,系统在不断增长的过程中往往需要引入某些独一无二的东西,并且创建更多这类东西。0.4 一个更大的例子

我的客人认为这个例子很有说服力。他走后,我意识到刚刚所揭示的东西跟我认识的另一个人在一个非常大的项目里得到的经验非常相似。

他们开发交互式事务处理系统:屏幕上显示着纸样表单的电子版本,一群人围坐在跟前。人们填写表单,表单的内容用于更新数据库,等等。在项目接近尾声的时候,客户要求做些改动:划分屏幕以同时显示两个无关的表单。

这样的改动是很恐怖的。这种程序通常充满了各种库函数调用,都假设知道“屏幕”在哪里和如何更新。这种改变通常要求查找出每一条用到了“屏幕”的代码,并要把它们替换为表示“屏幕的当前部分”的代码。

当然,这些概念就是我们在前面的例子中看到的隐藏状态(hidden state)的一种。因此,如果说在C++版本中修改这类应用程序比在C版本中容易,就不足为奇了。所需要做的事就是改变屏幕显示程序本身。相关的状态信息已经包含在类中,这样在类的多个对象中复制它们只是小事一桩。0.5 结论

是什么使得对系统的改变如此容易?关键在于,一项计算的状态作为对象的一部分应当是显式可用的,而不是某些隐藏在幕后的东西。实际上,将一项计算的状态显式化,这个理念对于整个面向对象编程思想来说,都是一个基础[3]。

小例子里可能还看不出这些考虑的重要性,但在大程序中它们就对程序的可理解性和可修改性产生很大的影响。如果我们看到如下的代码:push(x);push(y);add();z=pop();

我们可以理所当然地猜测存在一个被操作的堆栈,并设置z为x和y的和,但是我们还必须知道应该到何处去找这个堆栈。反之,如果我们看到s.push(x);s.push(y);s.add();z=s.pop();

猜想堆栈就是s准没错。确实,即使在C中,我们也可能会看到push(s,x);push(s,y);add(s);z=pop(s);

但是C程序员对这样的编程风格通常不以为然,以至于在实践中很少采用这种方式——除非他们发现确实需要更多的堆栈。原因就是C++采用类将状态和动作绑在一起,而C则不然。C不赞成上述最后一个例子的风格,因为要使例子运行起来,就要在函数push、add和pop之外单独定义一个s类型。C++提供了单个地方来描述所有这些东西,表明所有东西都是相互关联的。通过把有关系的事物联系起来,我们就能更加清晰地用C++来表达自己的意图。[1].Ada语言是在美国国防部组织下于20世纪70年代末开发的基于对象的高级语言,特别适合于高可靠性、实时的大型嵌入式系统软件,在1998年之前是美国国防部唯一准许的军用软件开发语言,至今仍然是最重要的军用系统软件开发语言。——译者注[2].Dag Brück指出,首先考虑效率问题,是C/C++文化的“商标”。我在写这段文字时,不由自主地首先把效率问题提出来,可见这种文化对我的影响有多深![3].关于面向对象程序设计和函数式程序设计(functional programming)之间的区别,下面的这种说法可能算是无伤大雅的:在面向对象程序设计中,某项计算的结果状态将取代先前的状态,而在函数式程序设计中,并非如此。第一篇动机

抽象是有选择的忽略。比如你要驾驶一辆汽车,但你又必须时时关注每样东西是如何运行的:发动机、传动装置、方向盘和车轮之间的连接等;那么你要么永远没法开动这辆车,要么一上路就马上发生事故。与此类似,编程也依赖于一种选择,选择忽略什么和何时忽略。也就是说,编程就是通过建立抽象来忽略那些我们此刻并不重视的因素。C++很有趣,它允许我们进行范围极其宽广的抽象。C++使我们更容易把程序看作抽象的集合,同时也隐藏了那些用户无须关心的抽象工作细节。

C++之所以有趣的第二个原因是,它设计时考虑了特殊用户群的需求。许多语言被设计用于探索特定的理论原理,还有些是面向特定的应用种类。C++不然,它使程序员可以以一种更抽象的风格来编程,与此同时,又保留了C中那些有用的和已经深入人心的特色。因此,C++保留了不少C的优点,比如偏重执行速度快、可移植性强、与硬件和其他软件系统的接口简单等。

C++是为那些信奉实用主义的用户群准备的。C和C++程序员通常都要处理杂乱而现实的问题;他们需要能够解决这些问题的工具。这种实用主义在某种程度上体现了C++语言及其使用者的灵活性。例如,C++程序员总是为了特定的目的编写不完整的抽象:他们会为了解决特定问题设计一个很小的类,而不在乎这个类是否提供所有用户希望的所有功能。如果这个类够用了,则他们可以对那些不尽如人意的地方视而不见。有的情况下,现在的折衷方案比未来的理想方案好得多。

但是,实用主义和懒惰是有区别的。虽然很可能把C++程序写得极其难以维护,但是也可以用 C++把问题精心划分为分割良好的模块,使模块与模块之间的信息得到良好的隐藏。

本书坚持以两个思想为核心:实用和抽象。在这一篇中我们开始探讨C++如何支持这些思想,后面几篇将探索C++允许我们使用的各种抽象机制。第1章为什么我用C++

本章介绍一些个人经历:我会谈到那些使我第一次对使用C++产生兴趣的事情以及学习过程中的心得体会。因此,我不会去说哪些东西是C++最重要的部分,相反会讲讲我是如何在特定情况下发现了C++的优点。

这些情形很有意思,因为它们是真实的历史。我的问题不属于类似于图形、交互式用户界面等“典型面向对象的问题”,而是属于一类复杂问题;人们最初用汇编语言来解决这些问题,后来多用C来解决。系统必须能在许多不同的机器上高效地运行,要与一大堆已有的系统软件实现交互,还要足够可靠,以满足用户群的苛刻要求。1.1 问题

我想做的事情是,使程序员们能更简单地把自己的工作发布到不断增加的机器中。解决方案必须可移植,还要使用一些操作系统提供的机制。当时还没有C++,所以对于那些特定的机器来说,C基本上就是唯一的选择。我的第一个方案效果不错,但实现之困难令人咋舌,主要是因为要在程序中避免武断的限制。

机器的数目迅速增加,终于超过负荷,到了必须对程序进行大幅度修改的时候了。但是程序已经够复杂了,既要保证可靠性,又要保证正确性,如果让我用C语言来扩展这个程序,我真担心搞不定。

于是我决定尝试用C++进行改进工作。结果是成功的:重写后的版本较之老版本在效率上有了极大的提高,同时可靠性丝毫不打折扣。尽管C++程序天生不如相应的C程序快,但是C++使我能在自己的智力所及的范围内使用一些高超的技术,而对我来说,用C来实现这些技术太困难了。

我被 C++吸引住,很大程度上是由于数据抽象,而不是面向对象编程。C++允许我定义数据结构的属性,还允许我在用到这些数据结构时,把它们当作“黑匣子”使用。这些特性用C实现起来将困难许多。而且,其他的语言都不能把我所需的效率和可靠性结合起来,同时还允许我对付已有的系统(和用户)。1.2 历史背景

1980年,当时我还是AT&T贝尔实验室计算科学研究中心的一名成员。早期的局域网原型刚刚作为试验运行,管理方希望能鼓励人们更多地利用这种新技术。为了达到这个目的,我们打算增加5台机器,这超过了我们现有机器数目的两倍。此外,根据硬件行情的趋势来看,我们最终还会拥有多得多的机器(实际上,他们承诺使中心的网络拥有50台左右的机器)。这样一来,我们将不得不应对由此引发的软件系统维护问题。

维护问题肯定比你想象的还要困难得多。另外,类似于编译器这样的关键程序总在不断变化。这些程序需要仔细安装;磁盘空间不够或者安装时遇到硬件故障,都可能导致整台机器报废。而且,我们不具备计算中心站的优越条件:所有的机器都由使用的人共同合作负责维护。因此,一个新程序要想运行到另一台机器上,唯一的方法就是有人自愿负责把它放到上面。当然,程序的设计者通常是不愿意做这件事的。所以,我们需要一个全局性的方法来解决维护问题。

Mike Lesk多年前就意识到了这个问题,并用一个名叫uucp的程序“部分地”加以解决,这个程序此后很有名气。我说“部分地”,是因为Mike故意忽略了安全性问题。另外,uucp一次只允许传递一个文件,而且发送者无法确定传输是否成功。1.3 自动软件发布

我决定扛着Mike的大旗继续往下走。我采用uucp作为传输工具,通过编写一个名叫ASD(Automatic Software Distribution,自动软件发布)的软件包来为程序员提供一个安全的方法,使他们能够把自己的作品移植到其他机器上,我预料这些机器的数量会很快变得非常巨大。我决定采用两种方式来增强uucp:更新完成后通知发送者,允许同时在不同的位置安装一组文件。

这些功能理论上都不是很困难,但是由于可靠性和通用性这两个需求相互冲突,所以实现起来特别困难。我想让那些与系统管理无关的人用ASD。为了这个目的,我应该恰当地满足他们的需求,而且没有任何琐碎的限制。因此,我不想对文件名的长度、文件大小、一次运行所能传递的文件数目等问题作任何限制。而且一旦ASD里出现了bug,导致错误的软件版本被发布,那就是ASD的末日,我决不会再有第二次机会。1.3.1 可靠性与通用性

C没有内建的可变长数组:编译时修改数组大小的唯一方法就是动态分配内存。因此,我想避免任何限制,就不得不导致大量的动态内存分配和由此带来的复杂性,复杂性又让我担心可靠性。例如,下面给出ASD中的一个典型的代码段:/* 读取八进制文件 */param = getfield(tf);mode = cvlong(param,strlen(param),8);/* 读入用户号 */uid = numuid(getfield(tf));/* 读入小组号 */gid = numgid(getfield(tf));/* 读入文件名(路径) */path = transname(getfield(tf));/* 直到行尾 */geteol(tf);

这段代码读入文件中用tf标识的一行的连续字段。为了实现这一点,它反复调用了几次getfield,把结果传递到不同的会话程序中。

代码看上去简单直观,但是外表具有欺骗性:这个例子忽略了一个重要的细节。想知道吗?那就想想getfield的返回类型是什么。由于getfield的值表示的是输入行的一部分,所以显然应该返回一个字符串。但是C没有字符串;最接近的做法是使用字符指针。指针必须指到某个地方;应该什么时候用什么方法回收内存?

C里有一些解决这类问题的方法,但是都比较困难。一种办法就是让getfield每次都返回一个指针,这个指针指向调用它的新分配的内存,调用者负责释放内存。由于我们的程序先后4次调用了getfield,所以也需要先后4次在适当场合调用free。我可不愿意使用这种解决方法,写这么多的调用真是很讨厌,我肯定会漏掉一两个。

所以,我再一次想,假如我能承受漏写一两个调用的后果,也就能承受漏写所有调用的后果。所以另一种解决方法应该完全无需回收内存,每次调用时,让getfield分配内存,然后永远不释放。我也不能接受这种方法,因为它会导致内存的过量消耗,而实际上,通过仔细地设计完全可以避免内存不足的问题。

我选择的方法是让getfield所返回内存块的有效期保持到下次调用getfield为止。这样,总体来说,我不用老是记着要回收getfield传回的内存。作为代价,我必须记住,如果打算把getfield传回的结果保留下来,那么每次调用后就必须将结果复制一份(并且记住要回收用于存放复制值的那块内存)。当然,对于上述的程序片断来说,付出这个代价是值得的,事实上,对于整个ASD系统来说,也是合适的。但是跟完全无需回收内存的情况相比,使用这种策略显然还是使得编写程序的难度增大。结果,我为了使程序没有这种局限性所付出的努力,大部分都花在进行簿记工作的程序上,而不是解决实际问题的程序上。而且由于在簿记工作方面进行了大量的手工编码,我经常担心这方面的错误会使ASD不够可靠。1.3.2 为什么用C

此时,你可能会问自己:“他为什么要用C来做呢?”。毕竟我所描述的簿记工作用其他的语言来写会容易得多,譬如Smalltalk、Lisp或者Snobol,它们都有垃圾收集机制和可扩展的数据结构。

排除掉Smalltalk是很容易的:因为它不能在我们的机器上运行!Lisp和Snobol也有这个问题,只不过没那么严重:尽管我写ASD那会儿的机器能支持它们,但无法确保在以后的机器上也能用。实际上,在我们的环境中,C是唯一确定可移植的语言。

退一步,即使有其他的语言可用,我也需要一个高效的操作系统接口。ASD在文件系统上做了很多工作,而这些工作必须既快又稳定。人们会同时发送成百上千的文件,可能有数百万个字节,他们希望系统尽可能快,而且一次成功。1.3.3 应付快速增长

我开始开发ASD的时候,我们的网络还只是个原型:有时会失效,不能与每台机器都连通。所以我用uucp作传输工具——我别无选择。然而,一段时间后,网络第一次变得稳定,然后成为了不可或缺的部分。随着网络的改善,使用ASD的机器数目也在增加。到了大概25台机器的时候,uucp已经慢得不能轻松应付这样的负载了。是时候了,我们必须跨过uucp,开始直接使用网络。

对于使用网络进行软件发布,我有一个好主意:我可以写一个spooler来协调数台机器上的发布工作。这个spooler需要一个在磁盘上的数据结构来跟踪哪台机器成功地接收和安装了软件包,以便人们在操作失败时可以找到出错的地方。这个机制必须十分强健,可以在无人干预的情况下长时间运行。

然而,我迟疑了好一阵,ASD最初版本中那些曾经困扰过我的琐碎细节搞得我泄了气。我知道我希望解决的问题,但是想不出来在满足我的限制条件的前提下,应该如何用C来解决这些问题。一个成功的spooler必须:

·有与尽量多的操作系统工具的接口。

·避免没有道理的限制。

·速度上必须比旧版本有本质的提高。

·仍然极为可靠。

我可以解决所有这些问题,除了最后一个。写一个spooler本身就很难,写一个可靠的spooler就更难。一个spooler必须能够对付各种可能的奇异失败,而且始终让系统保持可以恢复的状态。

我在排除uucp中的bug上面花了数年的功夫,然而我仍然认为,对于我新的spooler来说,要想成功,就必须立刻做到真正的bugfree。1.4 进入C++

在那种情况下,我决定来看看能否用C++来解决我的问题。尽管我已经非常熟悉C++了,但还没有用它做过任何严肃的工作。不过Bjarne Stroustrup的办公室离我不远,在C++演化的过程中,我们曾经在一起讨论。

当时,我想C++有这么几个特点对我有帮助。

第一个就是抽象数据类型的观念。比如,我知道我需要将向每台计算机发送软件的申请状态存储起来。我得想法把这些状态用一种可读的文件保存起来,然后在必要的时候取出来,在与机器会话时应请求更新状态,并能最终改变标识状态的信息。所有这一切都要求能够灵活进行内存的分配:我要存储的机器状态信息中,有一部分是在机器上所执行的任何命令的输出,而这输出的长度是没有限定的。

另一个优势是Jonathan Shopiro最近写的一个组件包,用于处理字符串和链表。这个组件包使得我能够拥有真正的动态字符串,而不必在簿记操作的细节上战战兢兢。该组件包同时还支持可容纳用户对象的可变长链表。有了它,我一旦定义了一个抽象数据类型,比如说叫machine_status,就可以马上利用Shopiro的组件包定义另一个类型——由machine_status对象组成的链表。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载