闳约深美C++

时间:2022-10-29 07:30:30

大约在1993年的时候,我开始学习C++。C++是我的“初恋情人”。虽然之前也学过Basic和Fortran,但只是作为学校里的一门课程来学的;而C++则是伴我度过青涩成长岁月的编程语言。那时候国内C++的书籍还非常少,对我影响最大的是《Borland C++ 3.0程序员指南》。近年来一直在用Java,算算时间,没有关心C++也有五六年了,期间C++出了新标准,我也是知之不详。前些日子朋友送来一本《C++编程思想》(第2卷),正好趁此机会重学一遍。

这本书的第一部分主题是构建稳定的系统。初级程序员往往只考虑功能需求,而高级程序员将考虑更多的东西,包括健壮性、性能、可伸缩性、可维护性、可扩展性等。构建稳定的系统就是功能之外首先值得考虑的东西。

作为一名Java程序员,我已经能熟练使用,深知在软件开发中,对发生概率较小的异常情况的处理可能会占掉相当多的思考时间。而这反映在健壮软件的代码中,就是有相当多的异常处理代码。只是当年学习使用C++时,对软件开发的理解还比较浅,对异常的处理接触得相对也比较少。在C++中,基本数据类型(如int)也可以抛出,不像Java,只能抛出实现了Throwable接口的对象。在C++的try…catch语句中,特别要注意资源释放的问题,Java亦是如此。

auto_ptr是为简化C++资源(特别是内存)管理问题而提供的一种机制,书中通过一个简化的版本说明了auto_ptr的原理。这里用到了模板。模板实现了另一种抽象层面的复用,有时候它的作用类似于宏。具体到“资源获得式初始化(RAII)”,如果拥有资源的对象在超出其作用域时都需要释放其资源,那么何不把这种行为集中实现?函数模板和通用算法也是如此。这样做的结果是消除了重复,减少了代码量。这就是auto_ptr的设计动机。

不像Java,C++没有垃圾回收机制,所以C++程序员必须投入更多的精力来思考资源分配/释放问题。如果某件事(如释放内存)总是被忘记,那就设计一种机制来防止再次忘记。总结起来,在C++中使用对象或分配内存,有以下两个原则:(1)尽量在栈中分配对象;(2)如果要使用堆,就使用auto_ptr模板类。

要理解auto_ptr,需要程序员理解栈和堆、动态内存分配、栈反解、构造函数和析构函数的调、模板等概念。虽然也可以死记住这种用法,但说不准哪天就会带来大麻烦。例如Java虽然有GC,但OutOFMemory的异常还是屡见不鲜。这就是知其然而不知其所以然的问题。C++对程序员提出了较高的要求,对PC结构和内存管理,结构化编程和面向对象编程都要有相当的理解。所以网上有人说:“真正的程序员使用C++。”

书中提到,虽然可以在方法声明中声明该方法可能会抛出的异常,但是标准C++库中的函数却都没有这样做。原因是当模板与具体类绑定时,异常类型可能是未知的。作者通过这一点告诉我们,虽然语言提供了某种机制,用还是不用,完全取决于使用者。

要构建健壮的系统,就不能不提防御式编程。这个概念与契约式编程有关,强调程序单元有清晰的规格说明,并在错误发生的第一地点发现它,避免因为“垃圾进,垃圾出”而引发不可知的严重后果。Meyer的“面向对象软件构造”对此有深入论述,这是严谨的OO程序员必须学习和理解的内容。有人把契约式编程理解为一种责任划定,出了问题的时候可以确定到底是哪个程序员的责任,所以可以假定别人都实现了契约,而不必再写代码进行检查。笔者认为这样的理解是有偏差的。难道与人签订了合同之后就不用检查合同履行的情况?契约式编程的目的是为了得到更健壮的系统,所以直接导致了防御式编程。

单元测试用于检查一个程序单元对它的契约(规格说明)执行的情况。大致可以分为两部分工作:(1)如果调用者不能满足前置条件,程序是否能正确反应;(2)在调用者满足前置条件的情况下,程序是否确保了后置条件的成立,并给出了预期的结果。让设计变成可测试的规格说明,让测试可以自动进行。书中第2章还介绍了一个极为精简的测试框架,只包含两个类。用最少的代码来实现你的意图,体现了简约之美。少即是多。如果读者对单元测试想了解更多,可以去看Kent Beck的《测试驱动开发》和其他一些书籍。熟悉JUnit的读者则可以体会一下,用不同的语言来表述同样的思想时的差异。

书中利用宏实现了调试时的代码跟踪。宏的运用对于C程序员应该不陌生,Java程序员则需要多花一些时间来体会。

调试和内存泄漏检查,都是C++程序员不能回避的话题。书中简单介绍了两个基本方法,虽然详细讨论这两个问题需要更大的篇幅,而且在实际工作中可能会采用BoundsChecker或Purify这样的工具,但这种简单的方法比较有利于初次接触这个领域的人理解概念,也容易进行尝试。

第二部分的主题是标准C++库。面向对象系统通过复用来提高开发效率,标准库则是实现复用的一个重要方面。作为一个Java程序员,笔者对这一点深有体会。

以前用Borland C++时,笔者也用过String类,但那不属于C++标准,是厂商扩展。学过C的人都知道,使用C的char*或char[]需要专门的练习。C++则利用对象封装的力量,减轻了程序员的负担。Java有String和StringBuffer两个类,它们在实现原理上不同,适用的场合也不同,《Effective Java》一书中有详细讨论。C++只有string类,关于它的对象创建和内存管理可以仔细研究一下,这涉及效率。首先考虑效率问题,是C/C++文化的“商标”。要考虑效率,就需要对计算机的结构和工作原理有充分的了解。一个深受C/C++文化影响的人,会不由自主地考虑每条语句后面计算机所做的事,并考虑这样做是否有效率,而许多只学过Java的程序员则很难表现出这一特点。不过遗憾的是书中没有附带介绍正则表达式的使用,以笔者的经验,正则表达式是字符串操作的有力工具。

抽象出流的概念是面向对象设计的又一经典范例,向我们展现了一个好的对象系统设计是多么易于理解。Java也继承了这一概念,虽然实现上有差异。通过从istream和ostream多继承得到iostream,这和十多年前笔者学到的一样,只是现在这些类都已模板化,并成为了C++标准的一部分。对国际化和本地化的支持也是现代编程语言不可缺少的特征,今天的C++对宽字符也有了良好支持,处理汉字时不再像当年那么麻烦了。

模板的威力令人印象深刻,C++标准中的许多东西都已构架在模板的基础上了,如string、auto_ptr、IO流和bitset等。C++社区对模板有着特殊浓厚的兴趣。从C++开始引入模板至今,大家想出了各式各样的精巧用法,甚至有“模板元编程”这样的奇特用法。Java也在1.5版本中引入了模板泛型编程。

模板的使用也产生了些许问题。模板对编译器带来了相当的负担,尤其恼人的是在编译时难以提供准确的出错信息。另外,大量使用模板的程序也向阅读者提出了更高的要求(请尝试读一下STL的源代码)。一些专家建议慎用模板,例如在《UML参考手册》(第2版)的Template词条中指出:“模板应该慎用。在许多时候(如在C++中),它们完成的功能可以通过多态和泛化更好地实现,使用模板只是基于一种追求不必要的效率的错误的热情。因为它们是生成器,它们的结果并不总是显而易见的。”

模板为开发者提供了这样一种场景,即我定义了一个通用算法,欢迎您来使用它;我定义了一个参数化类,欢迎您来特化它。往深了说,这涉及面向对象哲学:有没有纯粹的算法?有没有纯粹的数据?世界仅仅是由对象和行为组成,还是存在超越对象而独立存在的法则,这些法则对许多类对象都有效?C++标准的设计者认为这种法则是存在的,所以在设计中实现了通用算法。而在Java中,即使存在这种法则,也要放在某个Util类或静态方法里去,写成God.newtonRuleOne()这种样子。这种做法让不少Java程序员在理解Singleton或service oriented programming时存在困难,在使用Spring这样的框架时也感到别扭。理解C++标准库时,也需要了解一点“C++哲学”。

容器类非常重要,所以人们一遍又一遍地实现它。《Borland C++ 3.0程序员指南》中就提到它提供了两个容器类,其中一个是基于模板技术实现的。Java在提供泛型支持后,又重写了容器类的代码,而在此前,容器类已经由著名的Joshua Bloch重写了一遍。C++的容器类实现完整,有dequeue、stack、bitset这样的类。流迭代器也是Java中没有的概念。笔者感觉,C++中的迭代器概念更像“封装过的指针”。

与Java SDK中提供的类和方法相比,C++的标准库还是比较小的。但网上有不少C++的库可用,只是没有像Java那样形成标准。BOOST是一个“半官方”性质的C++类库,试想C++标准的制定者们一定也在考虑,有哪些东西值得放进C++标准库中。

书中第三部分讨论了一些扩展主题,在实际工作中碰到的机率也比较大。

RTTI是“反OO”的。在JUnit框架中,通过使用反射机制,提供了一种“非OO”的扩展方式,即TestCase子类中所有以“test”开头的方法都会被框架当作测试执行。Xstream通过反射,可以访问传入参数中的私有属性。在一个方法中,可以利用RTTI对传入参数进行类型检查,然后用switch语句对具体类型进行分别处理,从而导致没有扩展性的设计,即牺牲了OO的多态性。然而,为了达到特殊的目的,您可以考虑这种牺牲。另外,使用RTTI也可能影响性能。设计即折衷。

多重继承属于那种听起来很美的概念,要在自已的设计中使用多继承,一不小心就会带来许多麻烦。所以Java中没有多继承。在C++中,笔者也会考虑使用纯虚类来实现类似Java接口的概念,这样能避免多继承的诸多问题。

设计模式已经成为一个职业程序员必须掌握的知识。书中提供了关于设计模式的一些扩展讨论。读这部分内容之前,最好已经研读过那本经典的《设计模式》。

有人断言,并发是自OO以来程序员应该掌握的重要概念。因为CPU主频的增长已经遇到了“天花板”,CPU厂商纷纷推出多处理器系统或多内核系统来提高CPU的能力。为什么要在程序中使用并发?因为:(1)CPU要等待IO,特别是网络IO;(2)我们要充分利用SMP、Dual Core和Quad的能力。第一点还能通过异步IO来解决,第二点就能只能靠并发了。在Java中,一开始就提供了多线程支持,在1.5版本中更是增加了最初由Doug Lea设计的并发包。在头脑中建立起并发程序设计的概念是不容易的,所以线程安全和synchronized关键字一直是检验Java程序员水平的试金石。Bruce Eckel他们也认识到了并发程序设计的重要性,只是在C++标准中还没有对并发的支持。书中通过一个开源的Zthread项目,介绍了并发编程的一些内容。

学习一门语言时,掌握其语法只是一部分工作,更值得关注的是熟练使用这种语言的人们如何运用语言的元素来表达他们的思想。与语法相比,一些习语和典故才是语言中更具魅力的部分。这本书在有限的篇幅内,向我们展示了C++社区的人们是如何使用C++的。

C++可能是最难学的编程语言之一,要学好它,需要讲究方法。笔者曾经认真思考过语言学习的奥秘,最后得到的秘诀是八个字:听说领先,读写跟上。对于自然语言的学习和编程语言的学习都是如此。听、读就是学习已经掌握这门语言的人如何使用它,说、写就是自己实践。“吾尝终日而思矣,不如须臾之所学也。”Scott Ambler曾在他的著作《过程模式》中介绍过学习一门语言的方法:效率最高的方法是找到一个有资质的老师,参加他的课程;其次是找到一本好的教材,自己系统地学习;效率最低的方法是不看书,自己拿一个试验项目开始折腾。

这本书的中文版翻译质量比较好,但也存在一些瑕疵,如在讲解派生类的契约时将“Require no more;promise no less”译成了“不要只索取不付出”,笔者认为译成“要求不能多,承诺不能少”更合适。又如第10章中“”的译法,值得商榷。译文总体上意思准确,语句通顺,不会影响阅读。

总之,这本书既适合作为初学者学习C++的教材,也适合像笔者这样想了解C++新规范的人。在内容处理上符合本书的定位:一本C++培训教材。如果说“在任何领域,读5本书入门,读50本书成为专家”,那么这本书可以列入5本书的范围之内,当然也能成为50本书之一。

C++兼容并蓄,C++力求简约,C++博大精深,C++有特殊的美,所以说:闳约深美C++。笔者看到C++从Java的发展中吸收了经验,在下一个标准版本中,肯定会带来更多的东西。笔者也希望有一天能有机会再用C++写东西。

上一篇:最严重系统漏洞大曝光 下一篇:诺森德,零锁寒冰