首页 > 代码库 > (转)C++学习建议

(转)C++学习建议

原文:http://www.cnblogs.com/xilentz/archive/2010/05/01/1725460.html

博主传达了大量的去其糟粕的思想,所以,我只取了他对如何学习C++的建议,我还标记那些我觉得重要的话,方便以后提醒自己。

C++是一门强大的语言,我们没有任何理由不学习他,领略其中的风采。

建议1:有辨别力地阅读(包括那些被广泛称为“经典”的)C++书籍。

如果书中介绍的某块内容你认为在日常编程中基本不会用到(属于20%场景),那么也许最好的做法是非常大概的浏览一下,留个印象,而不是顺着这条线深究下去。关于在初学的时候应该读哪些书,后面还会提到。

实际上,除了语言无关的编程修养之外(需要阅读什么书后面会提到),对于C++这门特定的语言,要开始用它来编程,你只需知道一些基础但重要的语言知识(需要阅读哪些书后面会提到)以及“C++里面有许多缺陷和陷阱”的事实,并且——

建议2:养成随时查阅资料和文档的习惯。

“查文档”几乎可以说是作为一个程序员最重要的能力(是的,能力)了;它是如此重要,以至于在英文里面有一个专门的缩写——RTFM。为什么这个能力如此重要,原因很简单:编程领域的知识太鸡零狗碎了。不仅知识量巨大,而且知识的细节性简直是任何学科都无与伦比的(随便找一个框架类库看看它的API文档吧)。所以,把如此巨量的信息预先放在脑子里不仅不实际,而且简直是自作孽。你需要的是“元能力”,也就是查文档的能力——从你手头遇到的问题开始,进行正确合理的分析,预测问题的解决方案可能在什么地方,找到关于后者的资料,阅读理解,运用。

同样,在C++中也是如此,如果你从学习C++一开始就抱着这种态度的话,那么即便等到面试的时候被问到某个语言细节,你也可以胸有成竹的说你虽然并不知道这个细节,但在实际编码中遇到相应问题的时候肯定会找到合适的参考资料并很快解决问题(解决问题,才是最终目的)。当然,更大的可能性是,你在平常编码中已经接触过了最常见的那80%的陷阱和技巧了,由于你用的是实践指导性的学习方式,所以你遇到的需要去学习的陷阱和技巧几乎肯定都是常见场景下的,比没头苍蝇似的逮住一本C++“经典”就“细细研读”的办法要高效N倍,因为在没有实践经验的情况下,你很可能会认为其中的每个技巧,每个陷阱,都是同样概率发作的。

为什么市面上的C++书热衷于那些细节和技巧呢?

你用一个天生用来开啤酒瓶的工具开了啤酒瓶,不但啥成就感也没有,而且谁也不会觉得你牛13。然而,如果你发明了一种用两根筷子也能打开啤酒瓶的办法,或者你干脆生就一口好牙可以把瓶盖啃开,那也许就大不一样了。人家就会觉得你很好很强大。

事实8:每个人都喜欢戴着脚镣跳舞。

也就是说,如果你用一个天生为某个目的的工具来做他该做的事情,没有人会喝彩,你也不会觉得了不起。但如果你用两个本身不是为某个目的的工具组合出新功能的话,你就是“创新”者(尽管也许本来就有某个现成的工具可用)。

而C++则是这些“创新”的土壤,是的,我说的就是无穷无尽的workarounds和惯用法。但问题是,这些“创新”其实根本不是创新,你必须认识到的是,他们都只不过是在没有first-class解决方案的前提下不得已折腾出来的替补方案。是的,它们某种程度上的确可以叫创新,甚至研究可行的解决方案本身也是一件非常有意思的事情,但——

事实9:我知道它们很有趣,但实际上它们只是补丁方案。

是的,不要因为这些“创新”方案有趣就忍不住一头钻进去。你之所以觉得有趣是因为当你一定程度上熟悉了C++之后,C++的所有一切,包括缺陷,对你来说就成了一个“既定事实”,一个背景,一个习以为常的东西(人是有很强的适应性的)。因此,当你发现在这个习以为常的环境下居然出现了新的可能性时,你当然是会欢呼雀跃的(比如我当年读《Modern C++ Design》的时候就有一次从早读到晚,午饭都没吃),然而实际上呢?其它语言中也许早就有first-class的支持了,其它语言也许根本不需要这个惯用法,因为它们就没有这些缺陷。此外,从实践的角度来说,更重要的是,这些“解决方案”也许你平时编程根本就用不到。

不,我当然不是说这些补丁方案不重要。正如前面所说,C++中繁杂的技巧并非空穴来风,总有实际问题在背后驱动的。但问题是,对于我们日常编程来说,这些“实际问题”简直是八杆子打不着的。犯不着先费上80%的劲儿把20%时候才用到的东西揣在脑子里,用的时候查文档或书就行了。

看到这里,塑造C++中特定的心态哲学的另一个原因想必你也已经知道了。实际上,这个原因才是真正根本的。前面说的一个原因是C++书籍市场(教育)造就的,然而为什么人们喜欢写这些书呢?进一步说,为什么人们喜欢读这些书呢?(我承认,我也曾经读得津津有味。)答案很简单:心理。每个人都喜欢戴着脚镣跳舞(事实8)。认识到这一点不是为了提倡它,而是只有当我们认识到自己为什么会津津有味地去钻研一堆补丁解决方案的时候,我们才真正能够摆脱它们的吸引。

总而言之,C++的复杂性只是一个必要条件,并非问题的根本症结。根本症结在于人的心理,每个人都喜欢戴着脚镣跳舞,并且以为是“创新”。意识到这一点之后可以帮我们避免被各种各样名目繁多的语言细节和技巧占去不必要的时间。

然而,C++的复杂性始终是一个不可回避的现实。C++中有大量的陷阱和缺陷,后者导致了数目惊人的惯用法和workarounds。不加选择的全盘预先学习,是非常糟糕的做法,不仅低效,而且根本没有必要,实在是浪费生命。爱因斯坦曾经说过,“我只想知道‘他’(宇宙)的设计理念,其它的都是细节”。然而,正如另一些读者指出的,如果对C++中的这些细节事先一点都没有概念的话,那么实际编码中一旦遇到恐怕就变成没头苍蝇了,也许到哪里去RTFM都不知道。这也是为什么那么多C++面试都会不厌其烦地问一些有代表性的语言细节的原因。

把细节全盘装在脑子里固然不好,但对细节一无所知同样也不是个办法。那么对于C++程序员来说,在学习中究竟应该以怎样的态度和学习方法来对付C++的复杂性呢?其实答案也非常简单,首先有一些很重要&必须的语言细节&特性是需要掌握的,然后我们只需知道在C++中大抵有哪些地方有复杂性(陷阱、缺陷),那么遇到问题的时候自然能够知道到哪儿去寻找答案了。具体的建议在后文。

C++的复杂性分类

本来这一节是打算做成一个C++复杂性索引的,然而一来C++的复杂性太多,二来网上其实已经有许多资料(比如Bjarne Stroustrup本人的C++ Technical FAQ就是一个很好的文档),加上市面上的大多数C++书里面也不停的讲语言细节;因此实际上我们不是缺乏资料,而是缺乏一种索引这些资料的办法,以及一种掌控这些复杂性的模块化思维方法。

由于以上原因,这里并不详细罗列C++的复杂性,而是提供一个分类标准。

C++的复杂性有两种分类办法,一是分为非本质复杂性和本质复杂性;其中非本质复杂性分为缺陷和陷阱两类。另一种分类办法是按照场景分类:库开发场景下的复杂性和日常编码的复杂性。从从事日常编码的实践者的角度来说,采用后一种分类可以让我们迅速掌握80%场景下的复杂性。

二八法则

以下通过列举一些常见的例子来解释这种分类标准:

80%场景下的复杂性:

1. 资源管理(C++日常复杂性的最主要来源):深拷贝&浅拷贝;类的四个特殊成员函数;使用STL;RAII惯用法;智能指针等等。

2. 对象生命期:局部&全局对象生存期;临时对象销毁;对象构造&析构顺序等等。

3. 多态

4. 重载决议

5. 异常(除非你不用异常):栈开解(stack-unwinding)的过程;什么时候抛出异常;在什么抽象层面上抛出异常等等。

6. undefined&unspecified&implementation defined三种行为的区别:i++ + ++i是undefined behavior(未定义行为——即“有问题的,坏的行为,理论上什么事情都可能发生”);参数的求值顺序是unspecified(未指定的——即“你不能依赖某个特定顺序,但其行为是良好定义的”);当一个double转换至一个float时,如果double变量的值不能精确表达在一个float中,那么选取下一个接近的离散值还是上一个接近的离散值是implementation defined(实现定义的——即“你可以在实现商的编译器文档中找到说明”)。这些问题会影响到你编写可移植的代码。

(注:以上只是一个不完全列表,用于演示该分类标准的意义——实际上,如果我们只考虑“80%场景下的复杂性”,记忆和学习的负担便会大大减小。)

20%场景下的复杂性:

1. 对象内存布局

2. 模板:偏特化;非类型模板参数;模板参数推导规则;实例化;二段式名字查找;元编程等等。

3. 名字查找&绑定规则

4. 各种缺陷以及缺陷衍生的workarounds(C++书中把这些叫做“技术”):不支持concepts(boost.concept_check库);类型透明的typedef(true-typedef惯用法);弱类型的枚举(强枚举惯用法);隐式bool转换(safe-bool惯用法);自定义类型不支持初始化列表(boost.assign库);孱弱的元编程支持(type-traits惯用法;tag-dispatch惯用法;boost.enable_if库;boost.static_assert库);右值缺陷(loki.mojo库);不支持可变数目的模板参数列表(type-list惯用法);不支持native的alignment指定。

(注:以上只是一个不完全列表。你会发现,这些细节或技术在日常编程中极少用到,尤其是各种语言缺陷衍生出来的workarounds,构成了一个巨大的长尾,在无论是C++的书还是文献中都占有了很大的比重,作者们称它们为技术,然而实际上这些“技术”绝大多数只在库开发当中需要用到。)

非本质复杂性&本质复杂性

此外,考虑另一种分类办法也是有帮助的,即分为非本质复杂性和本质复杂性。

非本质复杂性(不完全列表)

1. 缺陷(指能够克服的问题,但解决方案很笨拙;C++的书里面把克服缺陷的workarounds称作技术,我觉得非常误导):例子在前面已经列了一堆了。

2. 陷阱(指无法克服的问题,只能小心绕过;如果跌进去,那就意味着你不知道这个陷阱,那么很大可能性你也不知道从哪去解决这个问题):一般来说,作为一个合格的程序员(不管是不是C++程序员),80%场景下的语言陷阱是需要记住才行的。比如深拷贝&浅拷贝;基类的析构函数应当为虚;缺省生成的类成员函数;求值顺序&序列点;类成员初始化顺序&声明顺序;导致不可移植代码的实现相关问题等。

本质复杂性(不完全列表)

1. 内存管理

2. 对象生命期

3. 重载决议

4. 名字查找

5. 模板参数推导规则

6. 异常

7. OO(动态)和GP(静态)两种范式的应用场景和交互

总而言之,这一节的目的是要告诉你从一个较高的层次去把握C++中的复杂性。其中最重要的一个指导思想就是在学习的过程中注意你正学习的技术或细节到底是80%场景下的还是20%场景下的(一般来说,读完两本书——后面会提到——之后你就能够很容易的对此进行判断了),如果是20%场景下的(有大量这类复杂性,其中尤数各种各样的workarounds为巨),那么也许最好的做法是只记住一个大概,不去作任何深究。此外,一般来说,不管使用哪门语言,认识语言陷阱对于编程来说都是一个必要的条件,语言陷阱的特点是如果你掉进去了,那么很大可能意味着你本来就不知道这有个陷阱,后者很大可能意味着你不知道如何解决。

学习C++:实践者的方法

在上面写了那么多之后,如何学习C++这个问题的答案其实已经很明显了。我们所欠缺的是一个书单。

第一本

如果你是一个C++程序员,那么很大的可能性你会需要用到底层知识(硬件平台架构、缓存、指令流水线、硬件优化、内存、整数&浮点数运算等);这是因为两个主要原因:一,了解底层知识有助于写出高效的代码。二,C++这样的接近硬件的语言为了降低语言抽象的效率惩罚,在语言设计上作了很多折衷,比如内建的有限精度整型和浮点型,比如指针。这就意味着,用这类语言编程容易掉进Joel所谓的“抽象漏洞”,需要你在语言提供的抽象层面之下去思考并解决遇到的问题,此时的底层知识便能帮上大忙。因此,一本从程序员(而不是电子工程师)的角度去介绍底层知识的书会非常有帮助——这就是推荐《Computer Systems:A Programmers Perspective》(以下简称CSAPP)(中译本《深入理解计算机系统》)的原因。

第三本(是的,第三本)

另一方面,C++不同于C的一个关键地方就在于,C++在完全保留有C的高效的基础上,增添了抽象机制。而所谓的“现代C++风格”便是倡导正确利用C++的抽象机制和这些机制构建出来的现代C++库(以STL为代表)的,Bjarne也很早就倡导将C++当作一门不同于C的新语言来学习(就拿内存管理来说,使用现代C++的内存管理技术,几乎可以完全避免new和delete),因此,一本从这个思路来介绍C++的入门书籍是非常必要的——这就是推荐《Accelerated C++》的原因(以下简称AC++)。《Accelerated C++》的作者Andrew Koenig是C++标准化过程中的核心人物之一。

第二本

C++是在C语言大行其道的历史背景下发展起来的,在一开始以及后来的相当长一段时间内,C++是C的超集,所有C的特性在C++里面都有,因此导致了大量后来的C++入门书籍都从C讲起,实际上,这是一个误导,因为C++虽然是C的超集,然而用抽象机制扩展C语言的重大意义就在于用抽象去覆盖C当中裸露的种种语言特性,让程序员能够在一个更自然的抽象层面上编程,比如你不是用int*加一个数组大小n来表示一个数组,而是用可自动增长的vector;比如你不是用malloc/free,而是用智能指针和RAII技术来管理资源;比如你不是用一个只包含数据的结构体加上一组函数来做一个暴露的类,而是使用真正的ADT。比如你不是使用second-class的返回值来表达错误,而是利用first-class的语言级异常机制等等。然而,C毕竟是C++的源头,剥开C++的抽象外衣,底层仍然还是C;而且,更关键的是,在实际编码当中,有时候还的确要“C”一把,比如在模块级的二进制接口封装上。Bjarne也说过,OO/GP这些抽象机制只有用在合适的地方才是合适的。当人们手头有的是锤子的时候,很容易把所有的目标都当成钉子,有时候C的确能够提供简洁高效的解决方案,比如C标准库里面的printf和fopen(此例受云风的启发)的使用界面就是典型的例子。简而言之,理解C语言的精神不仅有助于更好地理解C++,更理性地使用C++,而且也有其实践意义——这就是推荐《The C Programming Language》(以下简称TCPL)的原因。此外,建议在阅读《Accelerated C++》之前先阅读《The C Programming Language》。因为,一,《The C Programming Language》非常薄。二,如果你带着比较的眼光去看问题,看完《The C Programming Language》再看《Accelerated C++》,你便会更深刻的理解C++语言引入抽象机制的意义和实际作用。

第四本

《Accelerated C++》固然写得非常漂亮,但正如所有漂亮的入门书一样,它的优点和弱点都在于它的轻薄短小。短短3百页,对现代C++的运用精神作了极好的概述。然而要熟练运用C++,我们还需要更多的讲解,这个时候一本全面但又不钻语言牛角尖,从“语言是如何支持抽象设计”的角度而不是“为了讲语言特性而讲语言特性”的角度来介绍一门语言的书便至关重要,在C++里面,我还没有见到比C++之父本人的《The C++ Programming Language》(以下简称TC++PL)做得更好的,C++之父本人既有大规模C++运用的经验又有语言设计思想的最本质把握,因此TC++PL才能做到高屋建瓴,不为细节所累;同时又能做到实践导向,不落于为介绍语言而介绍语言的巢臼。最后有一个需要提醒的地方,TC++PL其实没有它看起来那么厚,因为真正介绍语言的内容只有区区500页(第一部分:基础;第二部分:抽象机制;以及第四部分:用C++设计),剩下的是介绍标准库的,可以当作Manual(参考手册)。

建议3:CSAPP &TCPL& AC++&TC++PL。

是的,在C++方面登堂入室并不需要阅读多得恐怖的所谓“经典”,至于为什么这些“经典”无需阅读,前面已经讲的很详细了。其实你只需要这四本书,就可以奠定一个深厚的基础,以及对C++的成熟理性的现代运用理念。其余的书都可以当成参考资料,用到的时候再去翻阅,即:

建议4:实践驱动地学习。

实践驱动当然不代表什么基础都不打,直接捋起袖管就上。不管运用哪种工具,首先都需要知道关于它的一定程度的基本知识(包括应该怎么用,和不应该怎么用)。知道应该怎么用可以帮你发挥出它的正确和最大效用,知道不应该怎么用则可以帮你避免用的过程中伤及自身的危险。这就是为什么我建议你看四本书,以及建议你要了解C++中的陷阱(大部分来自C,因此你可以阅读《C缺陷和陷阱》)的原因。

实践驱动代表着一旦一个扎实的基础具备了之后获得延伸知识的方式。出于环境和心理的原因,C++学习者们在这条路上走错的几率非常大,许多人乃至以上来就拿Effective C++&More Effective C++、Inside C++ Object Model这类书去读(是的,我也是,所以我才会在这里写下这篇文章),结果读了一本又一本,出现知道虚函数实现机制的每个细节却不知道虚函数作用的情况。

实践驱动其实很简单:实践+查文档。知识便在这样一个简单的循环中积累起来。实践驱动的最大好处就是你学到的都是实践当中真正需要的,属于那“80%”最有用的。而查文档的重要性前面已经说过了,但对于C++实践者来说,哪些“文档”是非常重要的呢?

第二本

《C++ Coding Standard》。无需多作介绍,这是一本浓缩了C++社群多年来宝贵的经验结晶的书,贴近实践,处处以80%场景为主导,不钻语言旮旯,用本为主…总之,非常值得放在手边时时参阅。因为书很薄,所以也不妨先往脑袋里面装一遍。书中的101条建议的介绍都很简略,并且指出了详细介绍的延伸阅读,在延伸阅读的时候还是要注意不要陷入无关的细节和不必要的技巧中,时时抬头看一看你需要解决的问题。在C++编码标准方面,Bjarne也有一些建议。

第一本

《The Pragmatic Programmer》,用本程序员的杰作;虽然不是一本C++的书,但其介绍的实践理念却是所有程序员都需要的。

第三本

《Code Complete, 2nd Edition》,这是一本非常卓越的参考资料,涉及开发过程的全景,有大量宝贵的经验。你未必要一口气读完,但你至少应该知道它里面都写了哪些内容,以便可以回头参阅。

其它

所有优秀的技术书籍都是资料来源。一旦养成了查文档的习惯,所有的电子书、纸书、网络上的资源实际上都是你的财富。不过,查文档的前提是你要从手边的问题分析出应该到什么地方去查资料,这里,分析问题的能力很重要,因此:

建议5:思考。

这个建议就把我们带到了第四本书:

第四本:

《你的灯亮着吗?》。不作介绍,自己阅读,这本书只有一百多页,但精彩非常,妙趣横生。

最后,要想理性地运用一门语言,不仅需要看到这门语言的特点,还要能够从另一个角度去看这门语言——即看到它的缺点,因为从心理上——

事实10:一旦我们熟悉了一门语言之后,就容易不知不觉地在其框架下思考,受到语言特性的细节的影响,作出second-class的设计。

对于像C++这样的在抽象机制上作了折衷的语言,尤其如此,思考容易受到语言机制本身细节的影响,往往在心里头还没想好怎么抽象,就已经确定了使用什么语言机制乃至技巧;更有甚者是为了使用某个特性而去使用某个特性。然而,实际上,我们应该——

建议6:脱离语言思考,使用语言实现。

关于设计的一般理念,Eric Raymond在《The Art of Unix Programming》的第二部分有非常精彩的阐述。

此外,除了脱离语言的具体抽象机制来思考设计之外,学习其它语言对同类抽象机制的支持也是非常有益的,正如老话所说,“兼听则明”。前一阵子reddit上也常出现“How Learning XXX help me become a Better YYY programmer”(其中XXX和YYY指代编程语言)的帖子,正是这个道理,这就把我们带到了最后一个建议:学习其它语言。

建议7:学习其它语言。

如果你是一个系统程序员,你可能会觉得没有必要学习其它语言,然而未必如此,你未必需要精通其它语言,而是可以去试着了解其它语言的设计理念,是如何支持日常编程中的设计的。这一招非常有利于在使用你自己的语言编程时心理上脱离语言机制细节的影响,作出更好的抽象设计。

尾声