不客气地说,CSDN论坛有一股不太好的风气,那就是喜欢空对空。常常就常识性的问题争个不可开交,而真正有价值的帖子却鲜有人问津。这样就很难留住真正有技术积累的且乐于贡献于社区的工程师。而且,由于基本的问题被一遍又一遍地问,总给人一种在低水平徘徊的感觉。道理上讲,还有一种可能是有太多的新人不断加入到C++之中,从而是C++的平均水平被稀释。我情愿是这样。同时,我真心希望,这一代C++工程师不是通过CSDN的C++版来学习C++的,至少不仅仅是。
书归正传。今天,偶然看到了了这个帖子《从号称自己C++好的人说自己C语言很差说开去》[url=http://topic.csdn.net/u/20091011/05/492fe17d-63b8-4050-a677-3b536908d8a7.html][/url],不出所料,其热闹程度非同一般。本来类似的问题也容易引起大家的共鸣和争论,我们可以从中抽提重一点有价值的东西。但是通篇看下来,有价值的评论没有反响,明显错误的却引来热捧。今天,索性花点时间,不揣冒昧,就其中谬种流传给出一个剖析。这种工作其实是吃力不讨好的,在专业工程师眼中是等而下之甚至不入流。那我为什么做呢?
1. 我对自己有一个承诺,有贡献于社区,因为我从社区获取良多。所以不敢偷懒
2. 专家一般不会在这种问题上浪费时间。我不是专家,尽管时间还是宝贵,但是如果它能有用,我就觉得值得
3. 期望能引起思考,进而早日上层次(没有写错!)
1. C++是用来解决什么问题的?
支持或者维护过稍微大一点的项目就会明白,理解现有的代码是最困难的事情。这个困难不是在于你熟悉不熟悉某个库(专用库往往是最容易理解的),而是理解其业务逻辑。说白了,就是类似反汇编当初编码人员的想法。这里,程序或者代码本身的组织带来的复杂性可能已经超过了业务逻辑的复杂性。而且,这种复杂性不是线性增长的!复杂性的来源是一个有趣也是开放的话题,这里仅仅列出一部分,并且看看C++是怎么进步来控制这些复杂性的:
a. 单一作用域内过多可见的名字。过多的名字不仅仅导致名字冲突的几率急剧增加,而且使得该作用域内任何的操作可能波及到范围指数变大。这是我们一直强调尽量不使用全局名字的原因。C提供了结构这样的操作,把多个关联的名字绑定到一个名字;C++除了通过使用访问控制来强化名字使用之外,还提供了名字空间的构造,可以把不是直接关联,但是逻辑相关的名字隐藏起来。这也是为什么C++允许就近声明。
b. 细节完全可见。C++使用访问控制还有一个好处就是隐藏细节。人们往往难以经受走捷径的诱惑而太多暴露是细节很容易让一个程序员在不完全理解的情况下使用了他不应该使用的东西。不要指望通过完善文档来避免这样的事情,因为一来程序员写的文档从来不够更新,二来程序员更相信自己的直觉。这就是我们强调“自文档”的含义,尽量使你的代码通俗到不用写注释也可以完整表达你的含义。这就要求我们使用合适的名字,这也是为什么熟悉const关键字成为C++入门的必须课,也可以解释为什么还要引入专门的mutable关键字。一个常识是代码是写给程序员的,而不是编译器;
c. 相同功能的不同非标准的实现。要想理解非标准的实现,就要深入到其中细节。这对于理解代码简直就是梦魇。更糟糕的是,许多以自己实现字符串类,容器类为骄傲的程序员的实现根本达不到标准的高度,更不要说超出。这里的常识是(如果你不是轮胎厂商就)不要重新发明轮子。这里可以解释为什么C++在语言核心的改进非常谨慎,而在库的支持上不断拓展而精益求精。其他现代语言以丰富的库支持而自豪,自称为C++程序员的人却抱残守缺,以不用任何标准库为荣,真是怪事哪都有,这里特别多。较新C++教学资料,如C++ Primer, Accelerate C++以及Programming Principles and Practice Using C++都完整的OO以及C++标准库为基础。忘记谭浩强先生的C++入门书吧。
d. 资源管理。资源是使用之前需要申请,使用后必须释放的东西。资源由于其稀缺,所以就显得非常重要。最简单的资源就是内存。C程序以及大部分不好的C++程序中,内存问题一直都是占据主要。C++提供了自动调用构造函数,析构函数以及基于值语义的可重载的对象复制机制,进而使得资源管理问题前所未有的简单。这个技术简称RAII,是学习C++登堂入室的关键。熟练使用这个看似简单的技术,就再也不会遭遇资源泄漏,资源重复释放,资源释放后使用等等一系列问题。
e. 错误处理。如果这个问题没有引起你的注意,那么你就没有真正写过工业强度的代码,但就是要求可以7*24运行,可以用于诸如生命保障系统控制的代码,尽管巨大部分的代码都不需要这样的强度。错误处理之所以复杂在于我们在出错的时候要尽可能回复到先前的状态,比如一个多线程服务程序,当一个新的请求因为资源有限而不能启动线程提供服务时,其他的正在运行的服务线程不应该收到任何的影响。C用来克服这些问题的手段有goto这样的局部跳转以及long jump这样的全局跳转。经验证明这些构造都不好使用(副作用大,难以组合等)。大家都知道,C++提供了异常。配合上述的资源管理,二者真实双剑合壁,谁与争锋。那些提起异常就考虑性能的可能一直都不明白,异常构造究竟是用来干什么的!
f. 风格和组织。C和C++都是自由风格代码,所有差别不大。但是我们有合适的编码标准。使用一致的编码标准,可以强化代码阅读人员的阅读习惯,减少歧义,提高精神欢愉程度而提升工作效率。这个C和C++打个平手。
g. 问题分解。复杂的问题必须首先分解之,然后各个击破。这也是团队合作的基础和前提。做这个目前所知的最好的工具就是OO。C++提供了对OO的完整支持,这就使得C++这个领域得心应手,左右逢源。GoF在Design Pattern中说,选择支持面向对象的语言的原因之一就是不想论述诸如继承,运行时多态这样的模式,尽管这样的模式在C的实现中非常普遍。C++把C中太常用的模式标准化了。
h. 静态类型检查。强类型之所以有用,是因为我们需要编译器帮助我们检查可能在实现引入的bug。C也是强类型的,但是支持的不够(想象全能的void*)。C++通过引入类型安全的库支持以及细化了类型查遍而强化了强类型检查,同时,是用有效的泛型手段,在加强了静态类型检查的同时还不需要额外的代码。
i. 有效的封装可以大大降低问题的复杂性。人人都知道,Windows平台上CreateFile是一个重要的函数,但是谁能在不查看文档的情绪下写好这个API的调用?那么istream呢?这个例子形象地说明了封装的极其重要而且C++善于与此。从系统API和标准库流差距十万八千里,其中你可以发挥余地的地方很多,比如创建NT sparse file就必须使用系统API,但是如果你每次都需要查文档,那么那还不是合格的程序员,更不要说是合格的C++程序员了。所以这里就显示出了封装的重要性。我评价一个C++程序是不是成熟的标准之一就是有没有自己的C++封装库。封装不仅仅积累类的知识,还有效减少了名字的数量。一个系统API往往需要多个参数,使用C++的缺省参数机制以及设计专用类,可以有效降低程序员的记忆负担。而且,除非重构了代码,你都不需要做重复的测试!
这样的条目还可以列出一些。基本上,任何一个C++相对C的新特性,都可以找到对应的工业最佳实践。
2。C++和性能问题。
C++程序员在更新的系统语言之前有时候不够自信,原因是C++太复杂。但是他们还有一根救命稻草,那就是性能。可惜的是,C++从来不是靠着宣称自己性能如何而获得目前的地位的。下面的叙述属于常识。对性能问题有一般理解的都可以直接跳过不读。这是为C++新手准备的,只要是为了打破性能迷思,重归学习C++的康庄大道。
a. 常识之一:性能是设计出来的,不是实现出来的。这句话的意思不是说糟糕的实现也能实现优良的性能,而是说,再好的实现,包括语言的选择,导致的性能改善都不如设计时对性能的考虑。现实的情况是,真正性能攸关的部分,可能早就在设计之初做过评估并且有了原型实现。实现时选择语言的机会非常有限。所以最后的结论就是C++在项目中被选中的绝大部分原因并不是C++的性能有多好。
b. 常识之二:未成熟的优化是罪恶之源。这句话的意思表述的和上一个有点相似,但是是从另一个侧面。那就是,除非做过性能瓶颈的测试(profiling),永远不要直观猜测性能的瓶颈,否则你可能花大量时间优化了不需要优化的地方。真正的性能瓶颈一般只存在与非常有限的几个分散地方,程序员对此估计的不准确性是人所共知的。
c. 常识之三:对于真正的性能问题,数据结构和算法的选择是关键。这需要多性能场景做数据流分析才能确定使用什么算法或者动态调整算法。C++非常关注性能问题,所以有基于树的map,也有机遇散列的map;有支持前端后端快速插入的容器,也有可以再中间快速插入的容器,还有可以根据迭代器类型自动选择二分查找还是顺序查找的自适应的标准库。这些,保证了一般C++代码的性能不会低于同等质量的任何其他代码。
d. 常识之四:在性能成为为题之前,它不是问题。这个有点绕。其基本含义是,对于绝大部分的C++实现者,也就是我们一般所说的C++程序员或者工程师,性能可以是最后一个考虑的因子。在此之前,很多更重要的事情需要关注,如编码风格,函数封装,基本类的设计以及C++基本构造的有效和合理使用。
e. 常识之五:C++不靠性能取胜,学习使用C++也并不复杂!这可以让C++工程师在C#和java程序员前挺直胸膛。C++解决的问题是偏向于底层的,复杂的系统及的问题,它的成功是靠着有效的复杂性控制机制以及与系统平台的天然兼容。而且,它正变得越来越易用!
3. C++和C
C++和C血脉相连,不可分割。二者不是竞争关系,而是相辅相成的,这种C和C++标准的相互借鉴就可以看出来。C++不是唯一的从C派生出来的现代多模式语言,但是C++是唯一成功的。下面就所谓的C和C++的复杂性做一简单讨论。希望可以给那些正在学些这些的朋友们一点帮助。
从C++的角度而言,C没有什么复杂性,一切都简单透明。C的复杂性来源于其基本特性的组合,比如数组,指针,函数指针的组合。在稍微复杂一点的应用中,它们的组合不可避免。另一方面,由于C处理的问题大部分在系统编程领域,系统编程有好多特性的约定,也导致C奇怪的使用。这应该属于域知识,并不是C的一部分。好多自动化问题的解决方案都是以C作为中间代码,依笔者有限的经验,这个可以说是相当的容易。
C中最复杂的毫无疑问是指针。但是复杂的并不是指针本身,而是指针可以做的工作太多太多,以及C语言中对指针操作的任意支持。一般来说,《C专家编程》是任何C背景程序员的必读数目。另一方面,阅读以下C规范,理解什么是标准明确定义的行为,这样就不至于局限于编译器的实现。当然使用多编译器是最好的,可以理解C的实现之差别。关注C语言构造背后的东西(rationale)而不是语言细节,并且熟悉C标准库。
C++,给人的第一影响就是复杂,复杂,复杂。但是,对于满汉全席,您会嫌盘子多吗?C++之所以又复杂的语言构造部分原因是它要解决的问题域极其宽广,部分原因是历史造成了。好消息是C++正在变的易于使用而且C++入门书籍也愈来愈好;不幸的是C++还没有完全达到人因工程要求的好的程度(易于写正确的代码,而不易于犯错)。我们学习C++的目的是要使用它,就像买个电视是为了看CCTV,而不是研究电视的工作原理一样。
4. 总结。
够长的了。上文中提到并且批评了一些错误的理解和思想,希望那个不至于误会为针对个人。作为一个使用C++的程序员,我的大部分日常代码都是使用perl写的,包括生成C++代码的时候。当有人责难为什么C++0x要引入regex的时候,我为此欢呼,因为它确实有用而,实现良好,而且与perl全兼容:)
本文的主要目的不是抛砖引玉,而是作为期望作为一个可以代言那部分没有发言的C++程序员,提供可以让C++新生力量少走弯路的一点指南。其中除了错误,其他所有都来于这个虚拟的社区。