从《C++ Primer 第四版》入手学习 C++
为什么要学习C++?
2009 年本书作者 Stan Lippman 先生来华参加上海祝成科技举办的C++技术大会,他表示人们现在还用C++的惟一理由是其性能。相比之下,Java/C#/Python等语言更加易学易用并且开发工具丰富,它们的开发效率都高于C++。但C++目前仍然是运行最快的语言[1],如果你的应用领域确实在乎这个性能,那么 C++ 是不二之选。
这里略举几个例子[2]。对于手持设备而言,提高运行效率意味着完成相同的任务需要更少的电能,从而延长设备的操作时间,增强用户体验。对于嵌入式[3]设备而言,提高运行效率意味着:实现相同的功能可以选用较低档的处理器和较少的存储器,降低单个设备的成本;如果设备销量大到一定的规模,可以弥补C++开发的成本。对于分布式系统而言,提高10%的性能就意味着节约10%的机器和能源。如果系统大到一定的规模(数千台服务器),值得用程序员的时间去换取机器的时间和数量,可以降低总体成本。另外,对于某些延迟敏感的应用(游戏[4],金融交易),通常不能容忍垃圾收集(GC)带来的不确定延时,而C++可以自动并精确地控制对象销毁和内存释放时机[5]。我曾经不止一次见到,出于性能原因,用C++重写现有的Java或C#程序。
C++之父Bjarne Stroustrup把C++定位于偏重系统编程(system programming) [6]的通用程序设计语言,开发信息基础架构(infrastructure)是C++的重要用途之一[7]。Herb Sutter总结道[8],C++注重运行效率(efficiency)、灵活性(flexibility)[9]和抽象能力(abstraction),并为此付出了生产力(productivity)方面的代价[10]。用本书作者的话来说,C++ is about efficient programming with abstractions。C++的核心价值在于能写出“运行效率不打折扣的抽象[11]”。
要想发挥C++的性能优势,程序员需要对语言本身及各种操作的代价有深入的了解[12],特别要避免不必要的对象创建[13]。例如下面这个函数如果漏写了&,功能还是正确的,但性能将会大打折扣。编译器和单元测试都无法帮我们查出此类错误,程序员自己在编码时须得小心在意。
inline int find_longest(const std::vector<std::string>& words)
{
// std::max_element(words.begin(), words.end(), LengthCompare());
}
在现代CPU体系结构下,C++ 的性能优势很大程度上得益于对内存布局(memory layout )的精确控制,从而优化内存访问的局部性[14](locality of reference)并充分利用内存阶层(memory hierarchy)提速[15],这一点优势在近期内不会被基于GC的语言赶上[16]。
C++的协作性不如C、Java、Python,开源项目也比这几个语言少得多,因此在TIOBE语言流行榜中节节下滑。但是据我所知,很多企业内部使用C++来构建自己的分布式系统基础架构,并且有替换Java开源实现的趋势。
学习C++只需要读一本大部头
C++不是特性(features)最丰富的语言,却是最复杂的语言,诸多语言特性相互干扰,使其复杂度成倍增加。鉴于其学习难度和知识点之间的关联性,恐怕不能用“粗粗看看语法,就撸起袖子开干,边查Google边学习[17]”这种方式来学习C++,那样很容易掉到陷阱里或养成坏的编程习惯。如果想成为专业C++开发者,全面而深入地了解这门复杂语言及其标准库,你需要一本系统而权威的书,这样的书必定会是一本八九百页的大部头[18]。
兼具系统性和权威性[19]的C++教材有两本,C++之父Bjarne Stroustrup的代表作《The C++ Programming Language》和Stan Lippman的这本《C++ Primer》。侯捷先生评价道:“泰山北斗已现,又何必案牍劳形于墨瀚书海之中!这两本书都从C++盘古开天以来,一路改版,斩将擎旗,追奔逐北,成就一生荣光[20]。”
从实用的角度,这两本书读一本即可,因为它们覆盖的C++知识点相差无几。就我个人的阅读体验而言,Primer更易读一些,我十年前深入学习C++正是用的《C++ Primer第三版》。这次借评注的机会仔细阅读了《C++ Primer第四版》,感觉像在读一本完全不同的新书。第四版内容组织及文字表达比第三版进步很多[21],第三版可谓“事无巨细、面面俱到”,第四版重点突出详略得当,甚至篇幅也缩短了,这多半归功于新加盟的作者Barbara Moo。
《C++ Primer 第四版》讲什么?适合谁读?
这是一本C++语言的教程,不是编程教程。本书不讲八皇后问题、Huffman编码、汉诺塔、约瑟夫环、大整数运算等等经典编程例题,本书的例子和习题往往都跟C++本身直接相关。本书的主要内容是精解C++语法(syntax)与语意(semantics),并介绍C++标准库的大部分内容(含STL)。“这本书在全世界C++教学领域的突出和重要,已经无须我再赘言[22]。”
本书适合C++语言的初学者,但不适合编程初学者。换言之,这本书可以是你的第一本C++ 书,但恐怕不能作为第一本编程书。如果你不知道什么是变量、赋值、分支、条件、循环、函数,你需要一本更加初级的书[23],本书第1章可用作自测题。
如果你已经学过一门编程语言,并且打算成为专业C++开发者,从《C++ Primer 第四版》入手不会让你走弯路。值得特别说明的是,学习本书不需要事先具备C语言知识。相反,这本书教你编写真正的C++程序,而不是披着C++ 外衣的C程序。
《C++ Primer 第四版》的定位是语言教材,不是语言规格书,它并没有面面俱到地谈到C++的每一个角落,而是重点讲解C++程序员日常工作中真正有用的、必须掌握的语言设施和标准库[24]。本书的作者一点也不炫耀自己的知识和技巧,虽然他们有十足的资本[25]。这本书用语非常严谨(没有那些似是而非的比喻),用词平和,讲解细致,读起来并不枯燥。特别是如果你已经有一定的编程经验,在阅读时不妨思考如何用C++来更好地完成以往的编程任务。
尽管本书篇幅近900页,其内容还是十分紧凑,很多地方读一个句子就值得写一小段代码去验证。为了节省篇幅,本书经常修改前文代码中的一两行,来说明新的知识点,值得把每一行代码敲到机器中去验证。习题当然也不能轻易放过。
《C++ Primer 第四版》体现了现代C++教学与编程理念:在现成的高质量类库上构建自己的程序,而不是什么都从头自己写。这本书在第三章介绍了string和vector这两个常用的类,立刻就能写出很多有用的程序。但作者不是一次性把string的上百个成员函数一一列举,而是有选择地讲解了最常用的那几个函数。
《C++ Primer 第四版》的代码示例质量很高,不是那种随手写的玩具代码。第10.4.2节实现了带禁用词的单词计数,第10.6利用标准库容器简洁地实现了基于倒排索引思路的文本检索,第15.9节又用面向对象方法扩充了文本检索的功能,支持布尔查询。值得一提的是,这本书讲解继承和多态时举的例子符合Liskov替换原则,是正宗的面向对象。相反,某些教材以复用基类代码为目的,常以“人、学生、老师、教授”或“雇员、经理、销售、合同工”为例,这是误用了面向对象的“复用”。
《C++ Primer 第四版》出版于2005年,遵循2003年的C++语言标准[26]。C++新标准已于2011年定案(称为C++11),本书不涉及TR1[27]和C++11,这并不意味着这本书过时了[28]。相反,这本书里沉淀的都是当前广泛使用的C++编程实践,学习它可谓正当时。评注版也不会越俎代庖地介绍这些新内容,但是会指出哪些语言设施已在新标准中废弃,避免读者浪费精力。
《C++ Primer 第四版》是平台中立的,并不针对特定的编译器或操作系统。目前最主流的C++编译器有两个, GNU G++和微软Visual C++。实际上,这两个编译器阵营基本上“模塑[29]”了C++语言的行为。理论上讲, C++语言的行为是由C++标准规定的。但是 C++不像其他很多语言有“官方参考实现[30]”,因此C++的行为实际上是由语言标准、几大主流编译器、现有不计其数的C++产品代码共同确定的,三者相互制约。C++编译器不光要尽可能符合标准,同时也要遵循目标平台的成文或不成文规范和约定,例如高效地利用硬件资源、兼容操作系统提供的C语言接口等等。在C++标准没有明文规定的地方,C++编译器也不能随心所欲自由发挥。学习C++的要点之一是明白哪些行为是由标准保证的,哪些是由实现(软硬件平台和编译器)保证的[31],哪些是编译器自由实现,没有保证的;换言之,明白哪些程序行为是可依赖的。从学习的角度,我建议如果有条件不妨两个编译器都用[32],相互比照,避免把编译器和平台特定的行为误解为C++语言规定的行为。尽管不是每个人都需要写跨平台的代码,但也大可不必自我限定在编译器的某个特定版本,毕竟编译器是会升级的。
本着“练从难处练,用从易处用”的精神,我建议在命令行下编译运行本书的示例代码,并尽量少用调试器。另外,值得了解C++的编译链接模型[33],这样才能不被实际开发中遇到的编译错误或链接错误绊住手脚。(C++不像现代语言那样有完善的模块(module)和包(package)设施,它从C语言继承了头文件、源文件、库文件等古老的模块化机制,这套机制相对较为脆弱,需要花一定时间学习规范的做法,避免误用。)
就学习C++语言本身而言,我认为有几个练习非常值得一做。这不是“重复发明轮子”,而是必要的编程练习,帮助你熟悉掌握这门语言。一是写一个复数类或者大整数类[34],实现基本的运算,熟悉封装与数据抽象。二是写一个字符串类,熟悉内存管理与拷贝控制。三是写一个简化的vector<T>类模板,熟悉基本的模板编程,你的这个vector应该能放入int和string等元素类型。四是写一个表达式计算器,实现一个节点类的继承体系(右图),体会面向对象编程。前三个练习是写独立的值语义的类,第四个练习是对象语义,同时要考虑类与类之间的关系。
表达式计算器能把四则运算式3+2*4解析为左图的表达式树[35],对根节点调用calculate()虚函数就能算出表达式的值。做完之后还可以再扩充功能,比如支持三角函数和变量。
在写完面向对象版的表达式树之后,还可以略微尝试泛型编程。比如把类的继承体系简化为下图,然后用BinaryNode<std::plus<double> >和BinaryNode<std:: multiplies<double> >来具现化BinaryNode<T>类模板,通过控制模板参数的类型来实现不同的运算。
在表达式树这个例子中,节点对象是动态创建的,值得思考:如何才能安全地、不重不漏地释放内存。本书第15.8节的Handle可供参考。(C++的面向对象基础设施相对于现代的语言而言显得很简陋,现在C++也不再以“支持面向对象”为卖点了。)
C++难学吗?“能够靠读书看文章读代码做练习学会的东西没什么门槛,智力正常的人只要愿意花功夫,都不难达到(不错)的程度。[36]” C++好书很多,不过优秀的C++开源代码很少,而且风格迥异[37]。我这里按个人口味和经验列几个供读者参考阅读:Google的protobuf、leveldb、PCRE的C++ 封装,我自己写的muduo网络库。这些代码都不长,功能明确,阅读难度不大。如果有时间,还可以读一读Chromium中的基础库源码。在读Google开源的C++代码时要连注释一起细读。我不建议一开始就读STL或Boost的源码,因为编写通用C++模板库和编写C++应用程序的知识体系相差很大。 另外可以考虑读一些优秀的C或Java开源项目,并思考是否可以用C++更好地实现或封装之(特别是资源管理方面能否避免手动清理)。
继续前进
我能够随手列出十几本C++好书,但是从实用角度出发,这里只举两三本必读的书。读过《C++ Primer》和这几本书之后,想必读者已能自行识别C++图书的优劣,可以根据项目需要加以钻研。
第一本是《Effective C++ 第三版》[38]。学习语法是一回事,高效地运用这门语言是另一回事。C++是一个遍布陷阱的语言,吸取专家经验尤为重要,既能快速提高眼界,又能避免重蹈覆辙。《C++ Primer》加上这本书包含的C++知识足以应付日常应用程序开发。
我假定读者一定会阅读这本书,因此在评注中不引用《Effective C++ 第三版》的任何章节。
《Effective C++ 第三版》的内容也反映了C++用法的进步。第二版建议“总是让基类拥有虚析构函数”,第三版改为“为多态基类声明虚析构函数”。因为在C++中,“继承”不光只有面向对象这一种用途,即C++的继承不一定是为了覆写(override)基类的虚函数。第二版花了很多笔墨介绍浅拷贝与深拷贝,以及对指针成员变量的处理[39]。第三版则提议,对于多数class而言,要么直接禁用拷贝构造函数和赋值操作符,要么通过选用合适的成员变量类型[40],使得编译器默认生成的这两个成员函数就能正常工作。
什么是C++编程中最重要的编程技法(idiom)?我认为是“用对象来管理资源”,即RAII。资源包括动态分配的内存[41],也包括打开的文件、TCP网络连接、数据库连接、互斥锁等等。借助RAII,我们可以把资源管理和对象生命期管理等同起来,而对象生命期管理在现代C++里根本不是困难(见注5),只需要花几天时间熟悉几个智能指针[42]的基本用法即可。学会了这三招两式,现代的C++程序中可以完全不写delete,也不必为指针或内存错误操心。现代C++程序里出现资源和内存泄漏的惟一可能是循环引用,一旦发现,也很容易修正设计和代码。这方面的详细内容请参考《Effective C++ 第三版》第3章资源管理。
C++是目前惟一能实现自动化资源管理的语言,C语言完全靠手工释放资源,而其他基于垃圾收集的语言只能自动清理内存,而不能自动清理其他资源[43](网络连接,数据库连接等等)。
除了智能指针,TR1中的bind/function也十分值得投入精力去学一学[44]。让你从一个崭新的视角,重新审视类与类之间的关系。Stephan T. Lavavej有一套PPT介绍TR1的这几个主要部件[45]。
第二本书,如果读者还是在校学生,已经学过数据结构课程[46],可以考虑读一读《泛型编程与STL》[47];如果已经工作,学完《C++ Primer》立刻就要参加C++项目开发,那么我推荐阅读《C++编程规范》[48]。
泛型编程有一套自己的术语,如concept、model、refinement等等,理解这套术语才能阅读泛型程序库的文档。即便不掌握泛型编程作为一种程序设计方法,也要掌握C++中以泛型思维设计出来的标准容器库和算法库(STL)。坊间面向对象的书琳琅满目,学习机会也很多,而泛型编程只有这么一本,读之可以开拓视野,并且加深对STL的理解(特别是迭代器[49])和应用。
C++模板是一种强大的抽象手段,我不赞同每个人都把精力花在钻研艰深的模板语法和技巧。从实用角度,能在应用程序中写写简单的函数模板和类模板即可(以type traits为限),不是每个人都要去写公用的模板库。
由于C++语言过于庞大复杂,我见过的开发团队都对其剪裁使用[50]。往往团队越大,项目成立时间越早,剪裁得越厉害,也越接近C。制定一份好的编程规范相当不容易。规范定得太紧(比如定为团队成员知识能力的交集),程序员束手束脚,限制了生产力,对程序员个人发展也不利[51]。规范定得太松(定为团队成员知识能力的并集),项目内代码风格迥异,学习交流协作成本上升,恐怕对生产力也不利。由两位顶级专家合写的《C++编程规范》一书可谓是现代C++编程规范的范本。
《C++编程规范》同时也是专家经验一类的书,这本书篇幅比《Effective C++ 第三版》短小,条款数目却多了近一倍,可谓言简意赅。有的条款看了就明白,照做即可:
· 第1条,以高警告级别编译代码,确保编译器无警告。
· 第31条,避免写出依赖于函数实参求值顺序的代码。C++操作符的优先级、结合性与表达式的求值顺序是无关的。裘宗燕老师写的《C/C++ 语言中表达式的求值》[52]一文对此有明确的说明。
· 第35条,避免继承“并非设计作为基类使用”的class。
· 第43条,明智地使用pimpl。这是编写C++动态链接库的必备手法,可以最大限度地提高二进制兼容性。
· 第56条,尽量提供不会失败的swap()函数。有了swap()函数,我们在自定义赋值操作符时就不必检查自赋值了。
· 第59条,不要在头文件中或#include之前写using。
· 第73条,以by value方式抛出异常,以by reference方式捕捉异常。
· 第76条,优先考虑vector,其次再选择适当的容器。
· 第79条,容器内只可存放value和smart pointer。
有的条款则需要相当的设计与编码经验才能解其中三昧:
· 第5条,为每个物体(entity)分配一个内聚任务。
· 第6条,正确性、简单性、清晰性居首。
· 第8、9条,不要过早优化;不要过早劣化。
· 第22条,将依赖关系最小化。避免循环依赖。
· 第32条,搞清楚你写的是哪一种class。明白value class、base class、trait class、policy class、exception class各有其作用,写法也不尽相同。
· 第33条,尽可能写小型class,避免写出大怪兽。
· 第37条,public继承意味着可替换性。继承非为复用,乃为被复用。
· 第57条,将class类型及其非成员函数接口放入同一个namespace。
值得一提的是,《C++编程规范》是出发点,但不是一份终极规范。例如Google的C++编程规范[53]和LLVM编程规范[54]都明确禁用异常,这跟这本书的推荐做法正好相反。
评注版使用说明
评注版采用大开本印刷,在保留原书板式的前提下,对原书进行了重新分页,评注的文字与正文左右分栏并列排版。本书已依据原书2010年第11次印刷的版本进行了全面修订。为了节省篇幅,原书每章末尾的小结和术语表还有书末的索引都没有印在评注版中,而是做成PDF供读者下载,这也方便读者检索。评注的目的是帮助初次学习C++的读者快速深入掌握这门语言的核心知识,澄清一些概念、比较与其他语言的不同、补充实践中的注意事项等等。评注的内容约占全书篇幅的15%,大致比例是三分评、七分注,并有一些补白的内容[55]。如果读者拿不定主意是否购买,可以先翻一翻第5章。我在评注中不谈C++11[56],但会略微涉及TR1,因为TR1已经投入实用。
为了不打断读者阅读的思路,评注中不会给URL链接,评注中偶尔会引用《C++编程规范》的条款,以[CCS]标明,这些条款的标题已在前文列出。另外评注中出现的soXXXXXX表示http://stackoverflow.com/questions/XXXXXX 网址。
网上资源
代码下载:http://www.informit.com/store/product.aspx?isbn=0201721481
豆瓣页面:http://book.douban.com/subject/10944985/
术语表与索引PDF下载:http://chenshuo.com/cp4/
本文电子版发布于https://github.com/chenshuo/documents/downloads/LearnCpp.pdf,方便读者访问脚注中的网站。
我的联系方式:giantchen_AT_gmail.com http://weibo.com/giantchen
陈硕
2012年3月
中国·香港
评注者简介 :
陈硕,北京师范大学硕士,擅长 C++ 多线程网络编程和实时分布式系统架构。现任职于香港某跨国金融公司 IT 部门,从事实时外汇交易系统开发。编写了开源 C++ 网络库 muduo; 参与翻译了《代码大全(第二版)》和《C++ 编程规范(繁体版)》;2009 年在上海 C++ 技术大会做技术演讲《当析构函数遇到多线程》,同时担任 Stanley Lippman 先生的口译员;2010 年在珠三角技术沙龙做技术演讲《分布式系统的工程化开发方法》;2012年在“我们的开源项目”深圳站做《Muduo 网络库:现代非阻塞C++网络编程》演讲。
[1] 见编程语言性能对比网站 http://shootout.alioth.debian.org/ 和Google 员工写的语言性能对比论文
https://days2011.scala-lang.org/sites/days2011/files/ws3-1-Hundt.pdf
[4] Milo Yip在《C++强大背后》提到大部分游戏引擎(如Unreal/Source)及中间件(如Havok/FMOD)是C++实现的。http://www.cnblogs.com/miloyip/archive/2010/09/17/behind_cplusplus.html
[5] 孟岩《垃圾收集机制批判》:C++利用智能指针达成的效果是,一旦某对象不再被引用,系统刻不容缓,立刻回收内存。这通常发生在关键任务完成后的清理(clean up)时期,不会影响关键任务的实时性,同时,内存里所有的对象都是有用的,绝对没有垃圾空占内存。http://blog.csdn.net/myan/article/details/1906
[8] Herb Sutter在C++ and Beyond 2011会议上的开场演讲《Why C++?》
http://channel9.msdn.com/posts/C-and-Beyond-2011-Herb-Sutter-Why-C
[10] 我曾向Stan Lippman介绍目前我在Linux下的工作环境(编辑器、编译器、调试器),他表示这跟他在1970年代的工作环境相差无几,可见C++在开发工具方面的落后。另外C++的编译运行调试周期也比现代的语言长,这多少影响了工作效率。
[11] 可参考Ulrich Drepper在《Stop Underutilizing Your Computer》中举的SIMD例子。
http://www.redhat.com/f/pdf/summit/udrepper_945_stop_underutilizing.pdf
[13] 可参考Scott Meyers的《Effective C++ in an Embedded Environment》
http://www.artima.com/shop/effective_cpp_in_an_embedded_environment
[14] 我们知道std::list的任一位置插入是O(1)操作,而std::vector的任一位置插入是O(N)操作,但由于std::vector的元素布局更加紧凑(compact),很多时候std::vector的随机插入性能甚至会高于std::list。见http://ecn.channel9.msdn.com/events/GoingNative12/GN12Cpp11Style.pdf 这也佐证std::vector是首选容器。
[15] 可参考Scott Meyers的技术报告《CPU Caches and Why You Care》和任何一本现代的计算机体系结构教材 http://aristeia.com/TalkNotes/ACCU2011_CPUCaches.pdf
[16] Bjarne Stroustrup有一篇论文《Abstraction and the C++ machine model》对比了C++和Java的对象内存布局。 http://www2.research.att.com/~bs/abstraction-and-machine.pdf
[21] Bjarne Stroustrup在《Programming--- Principles and Practice Using C++》的参考文献中引用了本书,并特别注明 use only the 4th edition.
[23] 如果没有时间精读注21中提到的那本大部头,短小精干的《Accelerated C++》亦是上佳之选。另外如果想从C语言入手,我推荐裘宗燕老师的《从问题到程序:程序设计与C语言引论(第2版)》
[25] Stan Lippman曾说:Virtual base class support wanders off into the Byzantine... The material is simply too esoteric to warrant discussion...
[29] G++统治了Linux平台,并且能用在很多Unix平台上;Visual C++统治了Windows平台。其他C++编译器的行为通常要向它们靠拢,例如Intel C++在Linux上要兼容G++,而在Windows上要兼容Visual C++。
[31] 包括C++标准有规定,但编译器拒绝遵循的。http://stackoverflow.com/questions/3931312/value-initialization-and-non-pod-types
[32] G++ 是免费的,可使用较新的4.x版,最好32-bit和64-bit一起用,因为服务端已经普及64位。微软也有免费的编译器,可考虑Visual C++ 2010 Express,建议不要用老掉牙的Visual C++ 6.0作为学习平台。
[35] “解析”可以用数据结构课程介绍的逆波兰表达式方法,也可以用编译原理中介绍的递归下降法,还可以用专门的Packrat算法。可参考http://www.relisoft.com/book/lang/poly/3tree.html
[39] Andrew Koenig的《Teaching C++ Badly: Introduce Constructors and Destructors at the Same Time》
http://drdobbs.com/blogs/cpp/229500116