前言
我的blog以前很长一段时间关注的都是C++中的技术&细节,乃至于读者和应者都寥寥。然而5月份的时候写的一篇“你应当如何学习C++”,阅读量却达到了3万多,在blog上所有文章中却是最高的(且远远超过了第二位);评论数目也有一百多。为什么独独这篇能够激起这么多的回应,想必是国内的C++社群被C++压抑太久,或者,严格来说,是被C++的教育方式压抑太久。实际上,不管是在各大国内论坛上,还是在comp.lang.c++.moderated这样的国际C++论坛上,乃至于在douban上的小组内,有心者都会发现,对C++语言的细节的关注一直都没有停止过,同样,对C++语言的细节的抱怨也从来都没有停止过。一个例子就是comp.lang.c++.moderated上的一个技术牛人James Kanze说的,他说接触C++十年了,到现在还需要不时去翻C++标准。这就难怪Eric Raymond老大在《The Art of Unix Programming》中说“C++是反紧凑”的了。C++中的细节太多,就算都看过了,也不可能都记住。更关键的是,就算都记住了,也不能让你成为一个真正的好程序员。
绝大多数人都把细节太多(或者用贬义词来说就是“阴暗角落太多”)归结为C++的本质问题,认为一切邪恶由此而生。也正因此,大约9月份的时候,Linus在邮件列表上说“C++是一门有思想包袱的语言;仅仅是为了让程序员远离C++,我也要用C”。这句短短的话在国内引起了很大的反应,最初是刘江转了Linus的话,然后云风和孟岩都发表了自己的看法;我也写了一篇“Why C++”(后来发给Bjarne,Bjarne对这篇文章做了一个友情评注)。
然而,这一通浑水搅过之后,我相信引起的变化未必很大。大多数原先的反对者能从中找出反对的理由,于是更加反对;大多数原先的赞同者也能从中找到赞同的理由,于是更加赞同;而剩下来的原先没有明确意见的,看双方各有各的道理,可能还是没有头绪。
摆脱自我服务偏见——理性思考的前提
《决策与判断》上提到过一个有趣的真实故事:1980年的某一天,美国空战司令部的计算机突然发出警报——苏联的一枚核弹正在向美国本土飞来。司令部立即调兵遣将,迅速为一场核战做好了准备,然而3分钟之后,工程人员发现是计算机的一个小零部件故障造成的。然而,这场虚惊之后,大众的反应才是真正有意思的:原先支持核武装的,认为现在感觉更加安全了(因为“事实证明这类的故障是完全可克服的”);而原先反对核武装的则认为更不安全了(因为“这类错误信号可能导致苏联过度反应,引发真正的核战”)。类似的情况也发生在三里岛核泄露事件之后,同样的,反对者认为(“这表明管理部门没有办法安全管理核能”),支持者认为(“这正表明这样的危险没有想像得那么严重,是可克服的”)。社会心理学把诸如此类的现象总结为“自我服务偏见”。不幸的是,“真理越辩越明”其实只适用于理性思考者。
为什么啰嗦这么一大通呢?就是因为,一直以来泛滥于程序员社群的“语言之争”,背后真正的原因其实并不在于语言实质上的优劣,而在于观察者的眼睛。在观察者的眼睛里面,语言并非一门工具,而是自己花了N多时间(其中尤数C++为最)来“修炼”的技能,对于这样的技能,被否定无疑等同于自己被否定。所以,从心理学上讲,语言并不是工具(尽管一直有这么一种呼吁),而是信仰。这样的信仰在越是花得时间久的语言上越是激烈。有趣的是,几乎所有的“热闹”的社群都有这样的现象,Java、Python、Ruby…莫不如是;因为就算语言本身不复杂,程序员仍然还是要投入大量的精力去学习各种各样的框架类库(想想Java的那些框架?)。因此这些语言社区的信仰未必不比C++社群的强烈。
然而,一旦弄清我们为什么会把语言当成信仰,就非常有助于摆脱在看待语言时的“自我服务偏见”,从客观的角度去看待问题。——“当你看到的是支持某个意见的证据时,试着去想一想有哪些证据是不支持它的”。
那么为什么要摆脱自我服务偏见?说小了,是为了成为一个更优秀的程序员(谁也不希望因为偏见而去使用一门低效的语言乃至不妥当的语言)。说大了是节省生命(因为偏见可能导致越陷越深,浪费时间)。
所以,如果你能够理性的思考我们将要讨论的问题,避免自我服务偏见(就当你从来没有花时间在C++上一样)。那么我们便可以开始讨论真正的问题了。
前言2
现在,几乎每个学习C++的都知道C++的核心问题是其复杂性;甚至本身不在C++社群的,也知道这是事实。群众的眼睛是雪亮的,何况这还是个太显而易见的事实。
但看了无数篇阐述C++复杂性的文章,和争论C++复杂性的吐沫星子(包括我前段时间写的两篇关于C++的总结)。我始终都有一个感觉——没分析透,就跟盲人摸象一样。正如“Why C++”的一位读者批评的,我在文章里面没有写明到底哪些是C++的“非本质复杂性”。当然,我自己凭感觉就能知道,而接触C++一段时间的人大致也能知道,但新手乃至非新手则对我所谓的“非本质复杂性”根本没有一个具体的认识,这就使得那篇“Why C++”脱离了原本的意图——面向所有C++使用者和学习者。
同样的原因,在写了“你应当如何学习C++”一文之后,当孟岩先生邀请我给《程序员》写一个系列的文章,介绍一下我在接触C++的过程中的态度和认识转变时,我虽然非常高兴的答应了,但直到现在3个月过去了还是颗粒无收。为什么?因为我觉得真正本质的问题没有被清晰的触摸到;所以直到现在我都没有动笔,免得废话说了一大堆,除了能被当成小说读读之外,对真正考虑是否要学习乃至使用C++的人未必有什么实际用处。
然而,这么个念头一直都放在潜意识里面。前一阵子和Bjarne通信,谈到了关于C++复杂性的一些想法,在邮件里面总结了一下C++的复杂性来源,感觉思路清晰了许多。而这篇文章要达到的目的,正是传达对C++的复杂性的一个具体而明确的认识,有了这个认识作为支持,我们便可以推导出学习C++的最佳(实践者)的方法。
为什么要学习(并使用)C++
显然,如果找不出要学习C++的理由,那么谈什么“正确的学习方法”等于是废话。
首先重复一句Bjarne的话:“我们的系统已经是极度复杂的了,为了避开C++的复杂性而干脆不用C++(Linus的做法),无异于因噎废食。”在所有可用C和C++的领域,C++都是比C更好的语言。当我说“更好的”时候,我说的是C++拥有比C更安全的类型检查、更好的抽象机制、更优秀的库。当然,凡事都有例外,如果你做的项目1)不大。2)编码中用不到什么抽象机制,甚至ADT(抽象数据类型,例如std::complex这种不含多态和继承的)也用不到,RAII也用不到,异常也用不到。3)你连基础库(如,简化资源管理的智能指针、智能容器)都用不着。那么也许你用C的确没问题;所以如果你的情况如此,不用和我争论,因为我无法反驳你。我们这里说的领域大致是Bjarne在“C++应用列表”里面列出来的那些地方。
底线是:如果把C++中的诸多不必要的复杂性去掉,留下那些本质的,重要的语言特性,简化语言模型,消除历史包袱。即便是C++的反对者也许也很难找到理由说“我还是不用C++”。在我看来,一个真正从实践意义上理性反对使用C++的人只有一个理由:C++的复杂性带来的混乱抵消乃至超过了C++的抽象机制和库(在他的特定项目中)带来的好处。
值得注意的是,这里需要避免一个陷阱,就是一旦人们认定了“C++不好”,那么这个理由就会“长出自己的脚来”,即,就算我们拿掉C++的复杂性,他们可能也会坚持还是不用C++,并为之找一堆理由。我假定你不是这样的人。不过,也许最可能的是他会说:“问题是我们今天用的C++并非如此(简洁),你的假设不成立。”是的,我的假设不成立。但虽然我们无法消除复杂性,我们实际上是可以容易地避开复杂性,避短扬长的。这也是本文的要点,容我后面再详述。
当然,到现在你可能还是会说。我还是不用C++,因为我可以用D;或者如果你本来做的项目就不需要C++,你则可能会说,我用Python.首先,如果你的项目能用Java/Python乃至Ruby做,那么用C++是自讨苦吃。因为能用那些语言代表你的项目在效率上本身要求就不高,那么用一门效率上讨不到太大好处,复杂性上却绰绰有余的语言,有什么价值呢?其次,如果你的项目效率是很重要的,你可能会说可以用D.然而现实是D在工业界尤其是国内被运用得非常少,几乎没有。而C++却有大量的既有代码,已经使用C++去做他们的产品的公司,在很长一段时间之内几乎是不可能用别的语言重写代码的,正如Joel所说,决定重写一个非平凡的代码基==自杀。所以,我们至少要注意以下两个明显的事实:
事实1:C++在工业界仍有稳定的核心市场。
这个事实大概不需要多加阐述,很多大公司的核心技术还是要靠C++来支撑的(见Bjarne主页上的C++应用列表)。所谓事实,就是未必是大家最愿意承认的情况,但又不得不承认。C++积累了庞大的代码基,这个代码基不是一朝一夕能够推翻的。D从语言角度来说的确优于C++,但最关键的就是还没有深入工业界(也许根本原因是没有钱支持,但这不是我们讨论的重点)。而C呢,根据Bjarne本人的说法,他的观察是主流工业界的趋势一直是“从C到C++”的,而不是反过来,至少在欧美是如此。在国内我们则可以通过CSDN上的招聘情况得到一个大致类似的信息。
事实2:C++程序员往往能享受到有竞争力的薪酬。
是的,这不是一篇不食人间烟火的技术文章。这个事实基于的逻辑很简单:物以稀为贵。Andrei Alexandrescu这次来中国SD2.0大会的时候,在接受采访时也说过:“最赚钱的软件(如MS Office)是C++写的”。孟岩也在blog上提到这么个事实,我想他作为CSDN的技术总编,业界观察肯定比我清晰深刻。所以我这里就不多废话了。
当然,以上逻辑并不就意味着在怂恿你去学C++,一切还要看你的兴趣。所以如果你志不在C++身处的那些应用领域,那这篇文章并非为你而写。
“C++的复杂性是根本原因”——一个有漏洞的推理
一旦我们认识了C++在一些领域是有需求的(值得学习和掌握的)这个问题之后,就可以接下来讨论“怎样正确学习和掌握C++”这个核心问题了。
其实,对于这个问题,Bjarne已经宣传了十年。早在99年的时候Bjarne就写了“Learning C++ as A New Language”,并在好几篇技术访谈(这里,这里,这里,还有这里)里面提到如何正确对待和使用C++中支持的多种抽象机制的问题。Andrew Koenig也写了一本现代C++教程《Accelerated C++》(这本书后面还会提到)。然而这么多年来,C++社群的状况改善了吗?就我所知,就算有改善,也是很小的。学习者还是盲目钻语言细节,只见树木不见森林;网上还是弥漫着各种各样的“技术”文章和不靠谱的“学习C++的XX个建议”;一些业界的有身份的专家还是在一本接一本的出语言孔乙己的书(写一些普通程序员八辈子用不着的技巧和碰不着的角落);而业界真正使用C++的公司在面试的时候还总是问一些边边角角的细节问题,而不是考察编程的基本素养(不,掌握所有的语言细节也不能让你成为一个合格的程序员)。这个面试理念是错误的,估计其背后的推理应该是“如果这个家伙不知道这个细节,那么估计他对语言也熟悉不到哪儿去;而如果他知道,那么虽然他可能并不是好的程序员,但我们还是能够就后一个问题进一步测试的”,这个理念的问题在于,对语言熟悉到一定程度(什么程度后面会具体建议)就已经可以很好的编程了(剩下的只需查查文档);而很多公司在测试“对语言熟悉程度”的时候走得明显太远了(比如,问临时对象生命期和析构顺序当然是无可厚非的,但问如何避免一个类被拷贝或者如何避免其构建在堆上?);当然,有些语言知识是必须要提前掌握的,具体有哪些后面会提到,面试的时候并非不能问语言细节,关键是“问哪些”。
所以说:
事实3:C++的整个生态圈这么些年来在学习C++的哲学上,实在没有多少改善。
为什么?是因为Bjarne介绍的学习方法在技术上没有说到点子上?是Andrew Koenig的书写得不够好?说了谁也不会相信。因为实际上,这里的原因根本不是技术上的,而是非技术的。
众所周知的一个事实是,从最表层讲,C++的最严重问题是在语言学习阶段占用了学习者的太多时间。翻一翻你的C++书架或者电子书目录,绝大多数的C++“经典”都是在讲语言。在我们通常的意义上,要“入门”C++,在语言上需要耗的时间一般要两三年。而要“精通”C++,则搞不好需要耗上十年八年的。(这跟Peter Norvig说的“十年学习编程”其实不是一回事,人家那是说一般意义上的编程技能,不是叫你当语言律师。)
那为什么我说“C++的复杂性是根本原因”是个有漏洞的推理呢?因为,要让人们在使用一门语言去做事情之前耗上大量时间去学习语言中各种复杂性,除了语言本身的复杂性的事实之外,还有一个重要的事实,那就是学习者的态度和(更重要的)方法。而目前大多数C++学习者的态度和方法是什么呢?——在真正用C++之前看上一摞语言书(日常编程八辈子都未必用得到)。而为什么会存在这样的学习态度呢?这就是真正需要解释的问题。实际上,有两方面的原因:
事实4:市面上的绝大多数C++书籍(包括很多被人们广泛称为“必读经典”的)实际上都是反面教材。
也就是说,随便你拿起哪本C++书籍(包括很多被人们广泛称为“必读经典”的),那么有很大的可能这本书中的内容不是你应该学的,而是你不应该学的。我之所以这么说有两个原因,因为一,我曾经是受害者。二,也是更实质性的原因,这些所谓的必读经典,充斥的是介绍C++中的陷阱和对于C++的缺陷的各种workarounds(好听一点叫Idioms(惯用法)或techniques(技术));又因为C++中的这类陷阱和缺陷实在数不胜数,所以就拉出了一个“长尾”;这类书籍在所有语言中都存在(“C缺陷和陷阱”、“Effective Java”、“Effective C#”等等),然而在C++里面这个尾巴特别长,导致这类书数不胜数。三,这些书中列出来的缺陷和陷阱根本不区分常见程度,对于一个用本程序员来说,应该希望看到“从最常见的问题到最不常见的问题”这样的顺序来罗列内容,然而这些书里面要么全部混在一起,要么按照“资源管理、类设计、泛型”这样的技术分类来介绍内容,这根本毫无帮助(如果我看到一个章节的内容,我当然知道它讲的是类设计还是资源管理,还用废话么?),使得一个学习者无法辨别并将最重要的时间花在最常见的问题之上。
最最关键的是:这些书当中介绍的内容与成为一个好程序员根本毫无关系,它们顶多只能告诉你——嗨,小心跌入这个陷阱。或者告诉你——嗨,你知道当你(八辈子都不一定遇到)遇到这个需求的时候,可以通过这个技巧来得以解决吗?结果读了一本又一本之后,你脑袋里除了塞满了“禁止”、“警戒”、“灯泡”符号之外,真正的编程素质却是一无长进。又或者有这样一类书,热衷于解释语言实现背后的机制,然而语言特性本质上是干嘛用的?是用来在实际编码中进行抽象的(说得好听一点就是“设计”),不是用来告诉你这个特性是怎么支持的。比如我就见过以下的情景:面试官问:“你知道虚函数吗?”得到的回答是一堆关于虚函数表机制的解释。面试官又问:“那虚函数的好处是什么呢?”到底为什么要虚函数呢?得到的回答是:“恩…啊…就是…多态吧”(这时已经觉得回答不够深刻了)。再问:“那多态是干嘛的呢?”哑口无言。
事实5:就算记住一门语言的所有细节也不能让你成为一个合格的程序员。
事实6:了解语言实现固然有其实践意义(在极端场合的hack手法,以及出现底层bug的时候迅速定位问题),然而如果为了了解语言机制而去了解语言机制便脱离了学习语言的本意了。
在C++里面这样的情况很多见:知道了语言实现的底层机制,却不知道语言特性本身的意义在什么地方。本末倒置。为什么?书害的。二,这类书当中介绍的所有情景加起来其实只属于那20%(二八法则),甚至20%都不到的场景(究竟是哪些书,后面会介绍,我不便直接列出书名,打击面太大,但我会把我认为essential的书列出来)。这就是为什么我说“八辈子都用不着”的原因。
事实7:80%的C++书籍(包括一些“经典”)只涉及到20%(或者更少)的场景。
你可能会说,那难道这些书就根本不值得看了吗?
我的回答是,对。根本不值得看。——但是值得放在旁边作为必要的时候的参考(记住从索引或目录翻起,只看严格必要的部分),如果你是个严肃的程序员的话。因为不管承认与否,墨菲法则的强大力量是不可忽视的——如果有一个可能遇到的陷阱,那么总会遇到的。而同样,C++的那些奇技淫巧也并非空穴来风,总有时候会需要用到的。但是你不需要预先把C++的所有细节和技巧存在脑子里才能够去编程,即:
建议1:有辨别力地阅读(包括那些被广泛称为“经典”的)C++书籍。
如果书中介绍的某块内容你认为在日常编程中基本不会用到(属于20%场景),那么也许最好的做法是非常大概的浏览一下,留个印象,而不是顺着这条线深究下去。关于在初学的时候应该读哪些书,后面还会提到。
实际上,除了语言无关的编程修养之外(需要阅读什么书后面会提到),对于C++这门特定的语言,要开始用它来编程,你只需知道一些基础但重要的语言知识(需要阅读哪些书后面会提到)以及“C++里面有许多缺陷和陷阱”的事实,并且——
建议2:养成随时查阅资料和文档的习惯。
“查文档”几乎可以说是作为一个程序员最重要的能力(是的,能力)了;它是如此重要,以至于在英文里面有一个专门的缩写——RTFM.为什么这个能力如此重要,原因很简单:编程领域的知识太鸡零狗碎了。不仅知识量巨大,而且知识的细节性简直是任何学科都无与伦比的(随便找一个框架类库看看它的API文档吧)。所以,把如此巨量的信息预先放在脑子里不仅不实际,而且简直是自作孽。你需要的是“元能力”,也就是查文档的能力——从你手头遇到的问题开始,进行正确合理的分析,预测问题的解决方案可能在什么地方,找到关于后者的资料,阅读理解,运用。
同样,在C++中也是如此,如果你从学习C++一开始就抱着这种态度的话,那么即便等到面试的时候被问到某个语言细节,你也可以胸有成竹的说你虽然并不知道这个细节,但在实际编码中遇到相应问题的时候肯定会找到合适的参考资料并很快解决问题(解决问题,才是最终目的)。当然,更大的可能性是,你在平常编码中已经接触过了最常见的那80%的陷阱和技巧了,由于你用的是实践指导性的学习方式,所以你遇到的需要去学习的陷阱和技巧几乎肯定都是常见场景下的,比没头苍蝇似的逮住一本C++“经典”就“细细研读”的办法要高效N倍,因为在没有实践经验的情况下,你很可能会认为其中的每个技巧,每个陷阱,都是同样概率发作的。
为什么市面上的C++书热衷于那些细节和技巧呢?
你用一个天生用来开啤酒瓶的工具开了啤酒瓶,不但啥成就感也没有,而且谁也不会觉得你牛13.然而,如果你发明了一种用两根筷子也能打开啤酒瓶的办法,或者你干脆生就一口好牙可以把瓶盖啃开,那也许就大不一样了。人家就会觉得你很好很强大。
事实8:每个人都喜欢戴着脚镣跳舞。
也就是说,如果你用一个天生为某个目的的工具来做他该做的事情,没有人会喝彩,你也不会觉得了不起。但如果你用两个本身不是为某个目的的工具组合出新功能的话,你就是“创新”者(尽管也许本来就有某个现成的工具可用)。
而C++则是这些“创新”的土壤,是的,我说的就是无穷无尽的workarounds和惯用法。但问题是,这些“创新”其实根本不是创新,你必须认识到的是,他们都只不过是在没有first-class解决方案的前提下不得已折腾出来的替补方案。是的,它们某种程度上的确可以叫创新,甚至研究可行的解决方案本身也是一件非常有意思的事情,但——