现在的位置: 首页 > 综合 > 正文

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

2013年09月30日 ⁄ 综合 ⁄ 共 6173字 ⁄ 字号 评论关闭

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
:学习其它语言。

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

尾声

建议
8
(可选):重读本文。

注:这篇文章的目的是给国内的C++学习者(尤其是初学者)一个可操作的建
议。我打算不断修订并完善它;因为这是根据我个人的经验来写的,而基于我对C++的熟悉程度,可能会有地方并不能完完全全站到初学者的视角来看问题。我估
计会有这样的地方,所以,如果有任何建议,请发邮件给我:

pongba@gmail.com

【上篇】
【下篇】

抱歉!评论已关闭.