书名:C++沉思录-
ISBN:978-7-115-52126-2
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [美]安德鲁·凯尼格(Andrew Koenig)
[美]芭芭拉·摩尔(Barbara Moo)
译 黄晓春
审 校 孟 岩
责任编辑 傅道坤
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
Authorized translation from the English language edition, entitled Ruminations on C++: A Decade of Programming Insight and Experience, 9780201423396 by Andrew Koenig and Barbara Moo, published by Pearson Education, Inc, publishing as Addison-Wesley Professional, Copyright © 1996 Pearson Education, Inc.
All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.
CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD., and POSTS & TELECOMMUNICATIONS PRESS Copyright © 2020.
本书封面贴有Pearson Education (培生教育出版集团) 激光防伪标签。无标签者不得销售。
本书基于作者在知名技术杂志发表的技术文章、世界各地发表的演讲以及斯坦福大学的课程讲义整理、写作而成,融聚了作者10多年C++程序生涯的真知灼见。
本书分为6篇,共32章,分别对C++语言的历史和特点、类和继承、STL与泛型编程、库的设计等几大技术话题进行了详细而深入的讨论,细微之处几乎涵盖了 C++所有的设计思想和技术细节。本书通过精心挑选的实例,向读者传达先进的程序设计方法和理念。
本书适合有一定经验的C++程序员阅读学习,可以帮助他们提升技术能力,成为C++程序设计的高手。
AT&T大规模程序研发部(前贝尔实验室)成员。他从1986年开始从事C语言的研究,1977年加入贝尔实验室。他编写了一些早期的类库,并在1988年组织召开了第一个具有相当规模的C++会议。在ISO/ANSI C++委员会成立的1989年,他就加入了该委员会,并一直担任项目编辑。他已经发表了100多篇C++方面的论文,并在Addsion-Wesley出版了C Traps and Pitfalls 一书(中文版名为《C缺陷与陷阱》,由人民邮电出版社出版),还应邀到世界各地演讲。
AT&T网络体系结构部门负责人。在1983年加入贝尔实验室不久,她开始从事Fortran 77编译器的研究工作,这是第一个用C++编写的商业产品。她一直负责AT&T的C++编译器项目,直到AT&T卖掉它的软件业务。她还为SIG会议、Lund技术学院和斯坦福大学提供辅导课程。
Anderw Koenig和Barbara Moo不仅有着多年的C++开发、研究和教学经验,而且还亲身参与了C++的演化和变革,对C++的变化和发展产生了重要的影响。
这是一本关于C++程序设计的图书。说得具体些,它首先是一本关于程序设计的书,其次才是一本关于C++的书。从这个意义上讲,本书与市面上大部分C++图书不一样,那些书所关注的是语言本身,而不是如何运用这种语言。
识字最多的人一定是最好的作家吗?能演奏最多音符的人一定是最好的音乐家吗?最勤于挥舞画笔的人一定是最好的画家吗?显然不是——这些观点极其荒谬。然而,我们却经常认为,那些了解最多语言特性的人就是最好的程序员。这一看法同样是荒谬的:编程工作中最困难的部分并不是去学习语言细节,而是理解问题的解决之道。
本书对于语言本身并没有着墨太多,相反,本书谈了很多关于程序设计技术方面的话题。一个作家必须学习如何讲述故事,同样,一个程序员也必须学习如何分析问题。本书包含了大量的问题,以及针对这些问题的解决方案。认真地研习这些内容[1],将会有助于你成为更出色的程序员。
本书所展示的解决方案有一个共同的思想,那就是抽象——集中注意力,只关注问题中那些在当前背景下最为重要的部分。可以说,如果不以某种方式进行抽象,你就不可能编写任何计算机程序,只此一点已经足以使“抽象”成为程序设计中最重要的单个思想。C++支持好几种不同的抽象形式,其中最著名的有抽象数据类型(Abstract Data Type,ADT)、面向对象程序设计和泛型程序设计。
在本书出版时,泛型程序设计还没有得到广泛的认知。短短几年后,STL(Standard Template Library,标准模板库)成为了C++标准库的一部分,这一思想也已经非常流行。所有这些使得本书中的思想随着时间的推移而越来越重要。
希望你能运用这些思想去理解一堆拼凑的代码与一个抽象之间的差别——这种差别就好像一堆辞藻与一篇文章、一堆音符与一支歌曲、一纸涂鸦与一幅图画之间的差别一样。
Andrew Koenig
Barbara Moo
2002年10月
美国新泽西州吉列市
[1] 这里“研习”一词,原文是study。ACCU主席Francis Glassborow曾经说过,所谓study,就是“阅读,学习,再阅读,再学习,反复阅读和学习,直到彻底理解”。——译者注
This is a book about programming in C++. In particular, it is first a book about programming and then a book about C++. In that sense, it is very different from most books about C++, which concentrate on what the language is rather than on how to use it.
Is the best author the one who knows the most words? Is the best musician the one who can play the most notes? Is the best painting the one with the most brush strokes? Of course not—these ideas are very absurd. Yet too often, we think that the best programmers are the ones who know the most language features. This idea is equally absurd: The hard part of programming is not learning the details of language features—it is understanding how to solve problems.
This book doesn’t talk much about language features. Instead, it talks about programming techniques. Just as a writer must learn how to tell a story, a programmer must learn how to analyze a problem. Accordingly, this book is full of problems and their solutions. Studying them is one way to become a better programmer.
Most of the solutions in this book share the idea of abstraction—concentrating one’s attention on just the parts of a problem that are important in the current context. It is impossible to write computer programs without using abstraction in one way or another, a fact that makes abstraction the most important single idea in programming. C++ supports abstraction in several forms, the best known of which are abstract data types, object-oriented programming, and generic programming.
Generic programming was not a widely known idea when we published this book. The idea became popular only a few years later, when the STL (Standard Template Library), a library designed to support generic programming, became part of the C++ standard library. As a result, the ideas in this book have become more important with time.
We hope that you can use these ideas to understand the difference between a pile of code and an abstraction—a difference that is as important as the difference between a pile of words and a story, or a pile of notes and a song, or a pile of brush strokes and a painting.
Andrew Koenig
Barbara Moo
Gillette, New Jersey, USA
October, 2002
——读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的Fortran 77和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月
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期之后停刊了。Stanley Lippman也甜言蜜语地哄着我在C++ Report上开设专栏,当时这本杂志刚刚从一份简陋的通信时刊正式成为成熟的杂志。加上我在C++ Report上发表的29篇专栏文章,我一共发表了98篇文章。
在这些杂志刊物里,刊载着大量的材料。如果这些文章单独看来是有用的,那么集结起来应该会更有用。所以,Barbara[2]和我(主要是Barbara)重新回顾了所有的专栏,选择出其中最好的文章,并根据一致性和连续性的原则进行了增补和重写。
既然你已经知道了本书的由来,我就再讲讲为什么要选择这本书,而不是其他的C++图书。众所周知,有关C++方面的图书太多了,为什么要选这一本呢?
第一个原因是,我想你们会喜欢它。大部分C++图书都没有顾及这点:它们应该是基于科目教学式的。吸引人最多不过是次要目标。
杂志专栏则不同。我猜想肯定会有一些人站在书店里,手里拿着一本JOOP,扫一眼Koenig的专栏之后,便立刻决定购买整本杂志。但是,要是我自认为这种情况很多的话,就未免太狂妄自大了。绝大多数读者是在买了书之后才读我的专栏的,也就是说他们有绝对的自由来决定是否读我的专栏。所以,我得让我的每期专栏都货真价实。
本书不对那些晦涩生僻的细节进行琐碎烦人的长篇大论。初学者不应该指望只读本书就能学会C++。具备了一定基础的人,比如已经知道几种编程语言的人,以及已经体会到如何通过阅读代码就能推断出一门新语言的规则的人,将能够通过本书对C++有所了解。大部分从头开始学习的读者应该先读Bjarne Stroustrup的The C++ Programming Language或者Stanley Lippman的C++ Primer,然后再读本书,这样效果可能会更好。
这是一本关于思想和技术的书,不关乎具体细节。如果你试图了解怎样用虚基类实现向后翻腾两周半,就请到别处去找吧。这里所能找到的是许多等待你去阅读分析的代码。请试一试这些范例。根据我们的课堂经验,想办法使这些程序运行起来,然后加以改进,能够很好地巩固你的理解。
如果你已经对C++有所了解,那么本书不仅能让你过一把瘾,而且能对你有所启示。这也是你应该阅读本书的第二个原因。本书并不是教C++语言本身,而是想告诉你用C++编程时怎样进行思考,以及如何思考问题并用C++表述解决方案。知识可以通过系统学习获取,智慧则不能。
就专栏来说,我尽力使每期文章都独立成章,但我相信,对于结集来说,如果能根据概念进行编排,将更易于阅读,也更有趣味。因此,本书划分为6篇。
第一篇是对主题的扩展介绍,这些主题将遍布本书的其他章节中。本篇没有太多的代码,但是所展现的有关抽象和务实的基本思想将贯穿本书,更重要的是,这些思想在C++设计原则和应用策略中得以充分体现。
第二篇着眼于继承和面向对象编程,大多数人认为这些是C++ 中最重要的思想。你将知道继承的重要性,以及它能做什么。你还会知道为什么将继承对用户隐藏起来是有益的,以及什么时候要避免继承。
第三篇探索模板技术,我认为这才是C++ 中最重要的思想。之所以这样认为,是因为这些模板提供了一种特别强大的抽象机制。它们不仅可以构造对所包含的对象类型一无所知的容器,还可以建立远远超出类型范畴的泛型抽象。
继承和模板之所以重要的另一个原因是,它们能够扩展C++,而不必等待(或者雇佣)人去开发新的语言和编译器。进行扩展的方法之一就是通过类库。第四篇谈到了库——包括库的设计和使用。
对基础有了很好的理解以后,就可以学习第五篇中的一些特殊编程技术了。在这一篇,你可以知道如何把类紧密地组合在一起,或者把它们尽可能地分离开。
最后,在第六篇,我们将返回头来对本书所涉及的内容做一个回顾。
这些经年累月写出来的文章有一个缺陷,就是它们通常没有用到语言的现有特性。这就导致一个问题:我们是应该在C++标准尚未最终定稿的时候,假装ISO C++已经成熟了,然后重写这些专栏,还是维持古迹,保留老掉牙的过时风格呢[3]?
这样的问题还有许多,我们选择了折中。对那些原来的栏目中有错的地方——无论是由于后来语言规则的变化而导致的错误,还是由于看待事物的方式改变而导致的错误——我们都做了修正。一个很普遍的例子就是对const的使用。自从const加入到语言中以来,它的重要性就在我们的意识中日益加强。
另一方面,尽管标准委员会已经接受bool作为内建数据类型,这里大量的范例还是使用int来表示真或者假的值。这是因为这些专栏文章早在之前就完成了,使用int作为真假值还将继续有效,而且要使绝大多数编译器支持bool还需要一些年头。
除了在JOOP、C++ Report、C++ Journal中发表我们的观点外,我们还在许多地方通过发表讲演(和听取学生的意见)对它们进行了提炼。尤其值得感谢的是USENIX Association和SIG Publications举办的会议,以及JOOP和C++ Report的发行人。另外,在计算机科学西部研究院的赞助下,我们俩在斯坦福大学讲授过多次单周课程,我们还在贝尔实验室为声学研究实验室和网络服务研究实验室的成员讲过课。还有Dag Brück曾为我们在瑞典组织了一系列的课程和讲座。Dag Brück当时在朗德理工学院自动控制系任教,现在供职于Dynasim AB。
我们也非常感谢那些阅读过本书草稿和那些专栏并对它们发表意见的人:Dag Brück、Jim Coplien、Tony Hansen、Bill Hopkins、Brian Kernighan(他曾笔不离手地认真阅读了两遍)、Stanley 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 Koenig
Barbara Moo
新泽西州吉列市
1996年4月
[1] 就是C++创造者Bjarne Stroustrup,这里可能是丹麦文。——译者注
[2] 本书合作者Barbara Moo是Andrew Koenig的夫人,退休前是贝尔实验室高级项目管理人员,曾负责Fortran和CFront编译器的项目管理。——译者注
[3] 本书编写于1996年年底,当时C++标准已经发布了草案第二版,非常接近最终标准。次年(1997年),C++标准正式定稿。本书内容是完全符合C++ 97标准的。——译者注
本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线投稿(直接访问www.epubit.com/ selfpublish/submission即可)。
如果您所在的学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
有一次我遇到一个人,他曾经用各种语言写过程序,唯独没用过C和C++。他提了一个问题:“你能说服我去学习C++,而不是C吗?”,这个问题还真让我想了一会儿。我给许多人讲过C++,可是突然间我发现他们全都是C程序员出身。到底该如何向从没用过C的人解释C++呢?
于是,我首先问他使用过什么与C相近的语言。他曾用Ada1[1]编写过大量程序——但这对我毫无用处,我不了解Ada。还好他知道Pascal,我也知道。于是我打算在我们两个之间有限的共通点之上找一个例子。
下面看看我是如何向他解释什么事情是C++可以做好而C做不好的。
C++的核心概念就是类,所以我一开始就定义了一个类。我想写一个完整的类定义,它要尽量小,要足够说明问题,还要有用。另外,我还想在例子中展示数据隐藏(data hiding),因此希望它有公有数据(public data)和私有数据(private data)。经过几分钟的思索,我写下这样的代码:
# include <stdio.h>
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++如此强大而灵活。
例如,一旦我开始使用Trace类,就会发现,如果能够在必要时关闭跟踪输出(trace output),这将会是个有用的功能。小意思,只要改一下类的定义就行:
#include <stdio.h>
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对象的输出。
当我问自己“如果用户想要修改这样的类,将会如何”时,我获得了更深层的理解。
用户总是要求修改程序。通常,这些修改是一般性的。例如,“你能让它随时关闭吗?”或者,“你能让它打印到标准输出设备以外的东西上吗?”我刚才已经回答了第一个问题。接下来着手解决第二个问题。后来证明这个问题在C++里可以轻而易举地解决,而在C里却得大动干戈。
我当然可以通过继承来创建一种新的Trace类。但是,我还是决定尽量让示例简单,避免介绍新的概念。所以,我修改了Trace类,用一个私有数据来存储输出文件的标识,并提供了构造函数,让用户指定输出文件:
#include <stdio.h>
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++类的特殊方式,使得对程序的改进变得轻而易举,而且不会影响使用这些类的代码。
此时,我又开始想,对于这个问题,典型的C解决方案会是怎样的。它可能会从一个类似于函数trace()(而不是类)的东西开始:
#include <stdio.h>
void trace(char *s)
{
printf("%s\n", s);
}
它还可能允许我以如下形式控制输出:
#include <stdio.h>
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向另一个文件输出,则何时切换输出的开关状态呢?显然,要想使结果正确需要花费相当大的精力。
为什么在C方案中进行扩展会如此困难呢?难就难在没有一个合适的位置来存储辅助的状态信息——在本例中是文件名和“noisy”标记。在这里,这个问题尤其让人恼火,因为在原来的情况下根本就不需要状态信息,只是到后来才知道需要存储状态。
向原本没有考虑存储状态信息的设计中添加这项能力是很难的。在C中,最常见的做法就是找个地方把它藏起来,就像我这里采用“noisy”标记一样。但是这种技术也只能做到这样;如果同时出现多个输出文件来搅局,就很难有效控制了。C++版本则更简单,因为C++鼓励采用类来表示类似于输出流的事物,而类就提供了一个理想的位置来放置状态信息。
结果是,C倾向于不存储状态信息,除非事先已经规划妥当。因此,C程序员趋向于假设有这样一个“环境”:存在一个位置集合,他们可以在其中找到系统的当前状态。如果只有一个环境和一个系统,这样考虑毫无问题。但是,系统在不断增长的过程中往往需要引入某些独一无二的东西,并且创建更多这类东西。
我的客人认为这个例子很有说服力。他走后,我意识到刚刚所揭示的内容跟我认识的另一个人在一个非常大的项目里得到的经验非常相似。
他们开发交互式事务处理系统:屏幕上显示着纸样表单的电子版本,一群人围坐在跟前。人们填写表单,表单的内容用于更新数据库;等等。在项目接近尾声的时候,客户要求做些改动:划分屏幕以同时显示两个无关的表单。
这样的改动是很恐怖的。这种程序通常充满了各种库函数调用,并假设都知道“屏幕”在哪里和如何更新。这种改变通常要求查找出每一条用到了“屏幕”的代码,并要把它们替换为表示“屏幕的当前部分”的代码。
当然,这些概念就是我们在前面的例子中看到的隐藏状态(hidden state)的一种。因此,如果说在C++版本中修改这类应用程序比在C版本中容易,就不足为奇了。所需要做的事就是改变屏幕显示程序本身。相关的状态信息已经包含在类中,这样在类的多个对象中复制它们只是小事一桩。
是什么使得对系统的改变如此容易呢?关键在于,一项计算的状态作为对象的一部分应当是显式可用的,而不是某些隐藏在幕后的东西。实际上,将一项计算的状态显式化,这个理念对于整个面向对象编程思想来说,都是一个基础[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年之前,Ada是美国国防部唯一准许的军用软件开发语言,至今仍然是最重要的军用系统软件开发语言。——译者注
[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++允许使用的各种抽象机制。
本章介绍一些个人经历:我会谈到那些使我第一次对使用C++产生兴趣的事情和学习过程中的心得体会。因此,我不会去说哪些东西是C++最重要的部分,相反会讲讲我是如何在特定情况下发现了C++的优点。
这些情形很有意思,因为它们是真实的历史。我的问题不属于类似于图形、交互式用户界面等“典型面向对象的问题”,而是属于一类复杂问题;人们最初用汇编语言来解决这些问题,后来多用C来解决。系统必须能在许多不同的机器上高效地运行,要与一大堆已有的系统软件实现交互,还要足够可靠,以满足用户群的苛刻要求。
我想做的事情是,使程序员能更简单地把自己的工作发布到不断增加的机器中。解决方案必须可移植,还要使用一些操作系统提供的机制。当时还没有C++,所以对于那些特定的机器来说,C基本上就是唯一的选择。我的第一个方案效果不错,但实现之困难令人咋舌,主要是因为要在程序中避免武断的限制。
机器的数目迅速增加,终于超过负荷,到了必须对程序进行大幅度修改的时候了。但是程序已经够复杂了,既要保证可靠性,又要保证正确性,如果让我用C语言来扩展这个程序,我真担心搞不定。
于是我决定尝试用C++进行改进工作。结果是成功的:重写后的版本较之老版本在效率上有了极大的提高,同时可靠性丝毫不打折扣。尽管C++程序天生不如相应的C程序快,但是C++使我能在自己的智力所及的范围内使用一些高超的技术,而对我来说,用C来实现这些技术太困难了。
我被C++吸引住,很大程度上是由于数据抽象,而不是面向对象编程。C++允许我定义数据结构的属性,还允许我在用到这些数据结构时,把它们当作“黑匣子”使用。这些特性用C实现起来将困难许多。而且,其他的语言不能把我所需的效率和可靠性结合起来,同时还允许我应付已有的系统(和用户)。
1980年,当时我还是AT&T贝尔实验室计算科学研究中心的一名成员。早期的局域网原型刚刚作为试验运行,管理方希望能鼓励人们更多地利用这种新技术。为了达到这个目的,我们打算增加5台机器,这超过了我们现有机器数目的两倍。此外,根据硬件行情的趋势来看,我们最终还会拥有多得多的机器(实际上,他们承诺使中心的网络拥有50台左右的机器)。这样一来,我们将不得不应对由此引发的软件系统维护问题。
维护问题肯定比你想象的还要困难得多。另外,类似于编译器这样的关键程序总在不断变化。这些程序需要仔细安装;磁盘空间不够或者安装时遇到硬件故障,都可能导致整台机器报废。而且我们不具备计算中心站的优越条件:所有的机器都由使用的人共同合作负责维护。因此,一个新程序要想运行到另一台机器上,唯一的方法就是有人自愿负责把它放到上面。当然,程序的设计者通常是不愿意做这件事的。所以,我们需要一个全局性的方法来解决维护问题。
Mike Lesk多年前就意识到了这个问题,并用一个名叫uucp的程序“部分地”加以解决。这个程序此后很有名气。我说“部分地”,是因为Mike故意忽略了安全性问题。另外,uucp一次只允许传递一个文件,而且发送者无法确定传输是否成功。
我决定扛着Mike的大旗继续往下走。我采用uucp作为传输工具,通过编写一个名叫ASD(Automatic Software Distribution,自动软件发布)的软件包来为程序员提供一个安全的方法,使他们能够把自己的作品移植到其他机器上。我预料这些机器的数量很快会变得非常巨大。我决定采用两种方式来增强uucp:更新完成后通知发送者;允许同时在不同的位置安装一组文件。
这些功能在理论上都不是很困难,但是由于可靠性和通用性这两个需求相互冲突,所以实现起来特别困难。我想让那些与系统管理无关的人用ASD。为了达到这个目的,我应该恰当地满足他们的需求,而且没有任何琐碎的限制。因此,我不想对文件名的长度、文件大小、一次运行所能传递的文件数目等问题做任何限制。而且一旦ASD里出现了bug,导致错误的软件版本被发布,那就是ASD的末日。我绝不会再有第二次机会。
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不够可靠。
此时,你可能会问自己:“他为什么要用C来做呢?”毕竟我所描述的簿记工作用其他的语言来写会容易得多,比如Smalltalk、Lisp或者Snobol,它们都有垃圾收集机制和可扩展的数据结构。
排除掉Smalltalk是很容易的:因为它不能在我们的机器上运行!Lisp和Snobol也有这个问题,只不过没那么严重:尽管我写ASD那会儿的机器能支持它们,但无法确保在以后的机器上也能用。实际上,在我们的环境中,C是唯一确定可移植的语言。
退一步讲,即使有其他的语言可用,我也需要一个高效的操作系统接口。ASD在文件系统上做了很多工作,这些工作必须既快又稳定。人们会同时发送成百上千个文件,这些文件可能有数百万个字节,他们希望系统尽可能快,而且一次成功。
在开始开发ASD的时候,我们的网络还只是个原型:有时会失效,不能与每台机器都连通。所以我用uucp作传输工具——我别无选择。然而,使用一段时间后,网络第一次变得稳定,然后成为了不可或缺的部分。随着网络的改善,使用ASD的机器数目也在增加。到了大概25台机器的时候,uucp已经慢得不能轻松应付这样的负载了。是时候了,我们必须跨过uucp,开始直接使用网络。
对于使用网络进行软件发布,我有一个好主意:我可以写一个spooler来协调数台机器上的发布工作。这个spooler需要一个在磁盘上的数据结构来跟踪哪台机器成功地接收和安装了软件包,以便人们在操作失败时可以找到出错的地方。这个机制必须十分强健,可以在无人干预的情况下长时间运行。
然而,我迟疑了好一阵,ASD最初版本中那些曾经困扰过我的琐碎细节搞得我泄了气。我知道我希望解决的问题,但是想不出在满足我的限制条件的前提下,应该如何用C来解决这些问题。一个成功的spooler必须具备以下特性。
我可以解决所有这些问题,除了最后一个。写一个spooler本身就很难,写一个可靠的spooler就更难了。一个spooler必须能够应对各种可能的失败,而且始终让系统保持可以恢复的状态。
我在排除uucp中的bug上面花了数年的工夫,然而我仍然认为,对于我这个新开发的spooler来说,要想成功,就必须立刻做到真正的无bug。
在那种情况下,我决定来看看能否用C++来解决我的问题。尽管我已经非常熟悉C++了,但还没有用它做过任何严肃的工作。好在Bjarne Stroustrup的办公室离我不远,在C++演化的过程中,我们曾经在一起讨论。
当时,我想C++有这么几个特点对我有帮助。
第一个就是抽象数据类型的观念。比如,我知道需要将向每台计算机发送软件的申请状态存储起来。我得想法把这些状态用一种可读的文件保存起来,然后在必要的时候取出来,且在与机器会话时应请求更新状态,并能最终改变标识状态的信息。所有这一切都要求能够灵活进行内存的分配:我要存储的机器状态信息中,有一部分是在机器上所执行的任何命令的输出,而这输出的长度是没有限定的。
另一个优势是Jonathan Shopiro最近写的一个用于处理字符串和链表的组件包。这个组件包使得我能够拥有真正的动态字符串,而不必在簿记操作的细节上战战兢兢。该组件包同时还支持可容纳用户对象的可变长链表。有了它,我一旦定义了一个抽象数据类型,比如说叫machine_status,就可以马上利用Shopiro的组件包定义另一个类型——由machine_status对象组成的链表。
为了把设计说得更具体一些,下面列出一些从C++版的ASD spooler中选出来的代码片断。这里变量m的类型是machine_status[1]:
struct machine_status {
String p; // 机器名
List<String> q; // 存放可能的输出
String s; // 错误信息,如果成功则为空
}
//…
m.s = domach(m.p, dfile, m.q); // 发送文件
if (m.s.length() == 0) { // 工作是否正常
sendfile = 1; // 成功——别忘了,我们是在发送一个文件
if (m.q.length() == 0) // 是否有输出
mli.remove(); // 没有,这台机器的事情已经搞定
else
mli.replace(m); // 有,保存输出
} else {
keepfile = 1; // 失败,提起注意,稍后再试
deadmach += m.p; // 加到失败机器链表中
mli.replace(m); // 将其状态放回链表
}
这个代码片断对于我们传送文件的每台目标机器都执行一遍。结构体m将发送文件尝试的执行结果保存在自己的3个域中:p是一个String,保存机器的名字;q是一个String链表,保存执行时可能的输出;s是一个String,尝试成功时为空,失败时标明原因。
函数domach试图将数据发送到另一台机器上。它返回两个值:一个是显式的;另一个是隐式的,通过修改第三个参数返回。我们调用domach之后,m.s反映了发送尝试是否成功的信息,而m.q则包含了可能的输出。
然后,我们通过将m.s.length()与0比较来检查m.s是否为空。如果m.s确实为空,那么将sendfile置1,表示我们至少成功地把文件发送到了一台机器上,然后来看看是否有什么输出。如果没有,那么可以把这台机器从需要处理的机器链表中删除。如果有输出,则将状态存储在List中。变量mli就是一个指向该List内部元素的指针(mli代表machine list iterator[机器链表迭代器])。
如果尝试失败,未能有效地与远程机器对话,那么将keepfile置为1,提醒我们必须保留该数据文件,以便下次再试,然后将当前状态存到List中。
这个程序片断中没什么高深的东西。这里的每一行代码都直接针对其试图解决的问题。跟相应的C代码不同,这里没有什么隐藏的簿记工作。这就是问题所在。所有的簿记工作都可以在库里被单独考虑,调试一次,然后彻底忘记。程序的其余部分可以集中精力解决实际问题。
这个解决方案是成功的,ASD每年要在50台机器上进行4000次软件更新。典型的例子包括更新编译器的版本,甚至是操作系统内核本身。较之C,C++使我得以在程序中从根本上更精确地表达我的意图。
我们已经看到了一个C代码片断的例子,它展示了一些隐秘的细枝末节。现在,我们来研究一下为什么C必须考虑这些细枝末节,然后再来看一看C++程序员怎样才可能避免它们。
尽管C有字符串文本量,但它实际上没有真正的字符串概念。字符串常量实际上是未命名的字符数组的简写(由编译器在尾部插入空字符来标识串尾),程序员负责决定如何处理这些字符。因此,尽管下面的语句是合法的;
char hello[] = "hello";
但是这样就不对了:
char hello[5];
hello = "hello";
因为C没有复制数组的内建方法。第一个例子中用6个元素声明了一个字符数组,元素的初值分别是‘h’、‘e’、‘l’、‘l’、‘o’和‘\0’(一个空字符)。第二个例子是不合法的,因为C没有数组的赋值。最接近的方法是:
char *hello;
hello = "hello";
这里的变量hello是一个指针,而不是数组:它指向包含了字符串常量“hello”的内存。
假设我们定义并初始化了两个字符“串”:
char hello[] = "hello";
char world[] = " world";
并且希望把它们连接起来。我们希望库可以提供一个concatenate函数,这样就可以写成这样:
char helloworld[]; //错误
concatenate(helloworld, hello, world);
可惜的是,这样并不奏效,因为我们不知道helloworld数组应该占用多大内存。通过写成
char helloworld[12]; //危险
concatenate(helloworld, hello, world);
可以将它们连接起来,但是我们在连接字符串时并不想去数字符的个数。当然,通过下面的语句,我们可以分配绝对够用的内存:
char helloworld[1000]; //浪费而且仍然危险
concatenate(helloworld, hello, world);
但是到底多少才够用?只要我们必须预先指定字符数组的大小为常量,就要接受猜错许多次的事实。
避免猜错的唯一办法就是动态决定字符串的大小。因此,我们希望可以这样写:
char *helloworld;
helloworld = concatenate(hello, world); //有陷阱
让concatenate函数负责判断包含变量hello和world的连接所需内存的大小、分配这样大小的内存、形成连接以及返回一个指向该内存的指针等所有这些工作。实际上,这正是我在ASD最初的C版本中所做的事情:我采用了一个约定,即所有字符串和类似字符串的值的大小都是动态决定的,相应的内存也是动态分配的。然而什么时候释放内存呢?
对于C的串库来说无法得知程序员何时不再使用字符串了。因此,库必须要让程序员负责决定何时释放内存。一旦这样做了,我们就会有很多方法来用C实现动态串。
对于ASD,我采用了3个约定。前两个在C程序中是很普遍的,第三个则不是。
1.字符串由一个指向它的首字符的指针来表示。
2.字符串的结尾用一个空字符标识。
3.生成字符串的函数不遵循用于这些串的生命期的约定。例如,有些函数返回指向静态缓冲区的指针,这些静态缓冲区要保持到这些函数的下一次调用;而其他函数则返回指向调用者要释放的内存的指针。这些字符串的使用者需要考虑这些各不相同的生命周期,要在必要的时候使用free来释放不再需要的字符串,还要注意不要释放那些将在其他地方自动释放的字符串。
类似“hello”的字符串常量的生命周期是没有限制的,因此,写成
char *hello;
hello = "hello";
后不必释放变量hello。前面的concatenate函数也返回一个无限存在的值,但是由于这个值保存在自动分配的内存区,所以使用完后应该将它释放。
最后,有些类似getfield的函数返回一个生存期经过精心定义但是有限的值。甚至不应该释放getfield的值,但是,如果想要将它返回的值保存一段很长的时间,我就必须记得将它复制到时间稍长的存储区中。
为什么要处理3种不同的存储期?我无法选择字符串常量:它们的语义是C的一部分,我不能改变。但是我可以使所有其他的字符串函数都返回一个指向刚分配的内存的指针。那么就不必决定是否要释放这样的内存了:使用完后就释放内存通常都是对的。
不让所有这些字符串函数都在每次调用时分配新内存的主要原因是,这样做会使程序十分巨大。例如,我将不得不像下面这样重写C程序代码段(见1.3.1节):
/* 读取八进制文件 */
param = getfield(tf);
mode = cvlong(param, strlen (param), 8);
free(param);
/* 读入用户号 */
s = getfield(tf);
uid = numuid(s);
free(s);
/* 读入小组号 */
s = getfield(tf);
gid = numgid(s);
free(s);
/* 读入文件名(路径) */
s = getfield(tf);
path = transname(s);
free(s);
/* 直到行尾*/
geteol(tf);
看来我还应该有一些其他的可选工具来减小我所写的程序。
使用C++修改ASD与用C修改相比较,前者得到的程序更简短,而所依赖的常规更少。作为例子,让我们回顾C++ ASD程序。该程序的第一句是为m.s赋值:
m.s = domach(m.p, dfile, m.q);
当然,m.s是结构体m的一个元素,m.s也可以是更大的结构体的组成部分;等等。如果我必须自己记住要释放m.s的位置,就必然对两件事情有充分的心理准备。第一,我不会一次正确得到所有的位置;要清除所有bug肯定要经过多次尝试。第二,每次明显地改变某个东西时肯定会产生新的bug。
我发现使用C++就不必再担心所有这些细节。实际上,我在写C++ ASD时,没有找到任何一个与内存分配有关的错误。
尽管ASD的C版本中有许多用来处理字符串的函数,但我却从没有想过要把它们封装成通用的包。向人们解释使用这些函数要遵循哪些规则实在是太麻烦了。而且,根据多年和计算机用户打交道的经验,我知道了一件事,那就是:在使用你的程序时,如果因为不遵守规则而导致工作失败,大部分人不会反躬自省,反而会怪罪到你头上。C可以做好很多事情,但不能处理灵活多变的字符串。
C++版本的ASD spooler也使用字符/字符串函数,已经有人写过这些函数,所以我不用写了。和我当初发布C字符串规则比起来,编写这些函数的人更愿意让其他人来使用这些C++字符串例程,因为他不需要用户记住那些隐匿的规定。同样,我使用串库作为例程的基础来实现分析文件名所需的指定的模式匹配,而这些例程又可抽取出来用于别的工作。
此后我用C++编程时,还有过几次类似的经历。我考虑问题的本质是什么,再定义一个类来抓住这个本质,并确保这个类能独立地工作。然后在遇到符合这个本质的问题时就使用这个类。令人惊讶的是,解决方法通常只用编译一次就能工作了。
我的C++程序之所以可靠,是因为我在定义C++类时运用的思想比用C做任何事情时都多得多。只要类定义正确,我就只能按照我编写它的初衷那样去用它。因此,我认为C++有助于直接表达我的思想并实现我的目的。
本章内容基于一篇专栏文章,从我写那篇文章到现在已经过去很多年了。在这段时间里,我很欣慰地看到一整套C++类库逐渐形成了。C库到处都是,但是,可以肯定至少我所见过的C库都有一定的问题。而C++则相反,它能实现真正的针对通用目的的库,编写这些库的程序员甚至根本不必了解他们的库会用于何处。
这正是抽象的优点。
[1] 细心的读者可能会发现我把数据成员设为public,并为此惊讶。我是故意这样做的:machine_status是一个简单的类,其结构就是其接口。对于如此简单的类来说,把成员设为private没有任何好处(从这个小小的脚注可以看到作者的实用主义态度,相对于后来很多人所奉行的教条主义,确实有很大的差别。——译者注)。