再呓C++
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
再呓C++
“C++是一个足够复杂,但因为其复杂而变得足够有趣的语言”
----------云风
之所以写这篇文章,很大程度上要归咎于一年前曾经许诺过要写一篇关于STL方面的学习心得。
我想我还算一个遵守承诺的人,尽管曾经许下的很多承诺都已随风逝去,不过还好,我还有坚持下去的勇气。
经过一年来断断续续的学习,从模板知识到STL标准库,再到STL源码剖析,还算看了几本大作,七窍里面也通了六窍,虽说越看越觉得没谱,但也有必要对已学过知识进行一些总结整理,当然这主要是个人在学习过程中的一个感悟和在模板中较为关键部分的概括,而不是一篇任何意义上的教程,算是温故而知新吧。
“好记性不如烂笔头”,工作后总是很怀念这句话,也许是大学期间听了太多次这句话吧。
最后,当我开始提笔写这篇文章的时候,我期望能够赶在11月底前完成,然后等待某个时刻的到来。
虽然如今的他多少让人觉得有点江郎才尽的感觉,虽然最近的一次感动也已经是两年前的《不能说的秘密》,但周董的新专辑终究是这乏味生活中的一丝期待。
此刻应该有很多人抓狂般的将鼠标移到了叉号上吧?手下留情,我们马上开始这篇关于C++的不能不说的秘密,人类历史上最伟大的财富(之一):C++ Template &Standard Template Library。
History
"Andy, are you crazy?"
“Well, yes I am crazy, but why not try it?”
这是Alexander Stepanov在收到Andy Koenig邮件时的简单对话。
时间也回溯到1994年1月6日,当时的Koenig在给Stepanov的电子邮件中表示如果Stepanov愿意将STL的说明文件撰写齐全,在1月25日前提出,便可能成为标准C++的一部份。
于是在Alexander和Lee的拼命赶工中,STL也开始了自己疯狂的C++标准化之旅。
而在这部STL纪传体史书中,我们至少应该记住如下五个人物。
David R. Musser:泛型程序设计观念的奠基人和提倡者。
早在1971年便开始提倡泛型编程(GP),只是当时没有任何编程语言支援泛型程序设计,直到1987年与Alex在Ada语言(支持泛型概念)下开发出一套相关的Ada library。
Bjarne Stroustrup:被誉为C++之父。
早在C++原始设计之初,出于对描述参数化容器的愿望,就已经考虑模板概念的引入。
但由于相关设计和实现问题的不明确,以及害怕会增加C++本身的复杂度,特别是担心增加编译和链接时间,这件事情就被推迟。
后来在C++广泛应用的过程中,Bjarne发现使用C++最大的问题就是缺乏一个扩充的标准库,而根本原因就是缺少一种定义容器类的一般机制,于1989年在Cfront 中引入模板机制,这为STL在C++中的实现奠定了基础。
此后在C++ STL标准化工作中也强力支持,甚至不惜拖延C++标准化决议时间,最终促成了STL的标准化。
【C++语言的设计和演化·第15章】Andy Koenig:在整个C++ STL标准化起到穿针引线的作用。
当Andy听完Stepanov对STL的介绍以及看到在惠普实验室的研究成果后异常兴奋,清楚的意识到在C++标准中引入STL的必要性和创新意义。
他将STL推广到C++世界,也给了Stepanov一个实现C++ STL的平台和支持。
Alexander Stepanov:被誉为STL之父。
早在C++诞生的年代,Stepanov便开始考虑在保证效率的前提下,将算法从诸多具体应用之中抽象出来的可能性,并同Musser等人一起在Ada中对STL进行实践。
在这期间,Stepanov意识到,在当时的面向对象程序设计思想中所存在的一些问题,比如抽象数据类型概念所存在的缺陷。
希望通过对软件领域中各组成部分的分类,逐渐形成一种软件设计的概念性框架,在这些驱动下逐步完善了STL中的数据结构和架构框架。
随后注意到C++语言在实现其泛型思想方面所具有的潜在
优势,在HP实验室工作期间,和Meng Lee的共同努力下,通过模板机制实现了一个包含有大量数据结构和算法部件的庞大运行库,也就是STL的雏形。
在标准化的进程中也进行了大量调整,加入了封装内存模式信息的抽象模块,从而独立于具体平台,甚至连比较成熟的stream、string等都以template形式重新写过,最终实现了STL的标准化工作。
整个标准库到处都是template!
Meng Lee:早期从事C++编译器工作,对模板机制非常熟悉,很多STL的程序都是出自她手。
关于她了解不是很多,但最关键的一点还是要告诉大家,她可是一位亚洲MM哦o(∩_∩)o
Component
在这一章中内容比较多,我也意识到仅凭现在自身的水平、仅用三言两语和很短的时间内不可能面面俱到的描述模板和STL这一庞大体系,这里仅根据个人的理解而言。
如果真的达到尽善尽美,可能需要一生来不断的修正和扩充,C++的学习不可能一蹴而就,在学习和使用过程中会有不同的感悟。
这里仅仅迈出第一步。
本节分为三个层次分别介绍模板实现机制、语法概念以及STL的组件构成。
Template mechanism
模板的实现机制在标准中没有过多规定,也不像面向对象机制中有着比较成熟的解析规则和标准,只是在最终实现角度定义了语法和规则。
很多实现机制都可以由编译器设计者自行提供解决方案。
在Bjarne 对模板的目标来看,主要是在保证移植性、合理编译和链接效率的前提下实现语法的方便性、运行时效率和类型安全。
坦白说,由于在安全性、易用性、灵活性以及执行文件大小和效率之间的侧重点不同,以及实现复杂度的巨大,各个编译器之间都会有各自的取舍与平衡点,实现一个十全十美的编译器几乎是一件不可能的事情。
由于C++模板机制的首要竞争对手就是宏,所以在灵活性和效率上要求非常严格,因此模板机制几乎都是在编译和链接时的机制。
总体来讲,C++模板本身就是一套复杂的宏语言和虚函数的组合,大部分实现都放在编译期间处理(类似宏的处理手段),对于动态数据则采用虚函数列表的机制达到运行时的支持。
具体的模板在编译器的解析机制,相关资料也不多,即使连《Inside C++ Model》也只有不多的几页来描述,主要过程如下【Inside The C++ Object Model】:
●在程序编译初期,对于任何template相关的信息都不会具现,而是存储在于object files中
●当object files被链接在一起时,会有一个prelinker程序执行,用来检测object files,寻找template
实体的相互使用和对应的定义
●对于使用到的template实体而该实体还没有定义的情况,prelinker会将该文件收集标记,这样
便可以将必要的具现操作指定给特定的文件,同时记录在prelinker产生的.ii文件中
●prelinker重新执行编译器,重新编译每一个.ii曾被改变过的文件。
整个过程不断重复,知道所有
必要的具现操作都完成
●链接成可可执行文件
通过对模板实现过程的理解,我们不难看到,在编译器初期看到template的时候并没有做任何事,然后通过对程序的整体解析,对各种必要的具现加以定义,最后通过虚函数表实现必要的动态绑定机制。
其中最为关键和复杂的任务集中在对具现的处理上。
简单想,如果把具现出来的类的所有成员函数都定义出来,肯定能够保证模板的正常使用,但这是以增加文件大小和执行效率作为代价;而如果程序非常巨大,对于每一个具现(及可能是基本类型也可能是自定义类型)只定义一次,虚函数表的布局合理又是一个极端复杂的解析过程,自然会增加过多的编译时间和难度。
实际上,多个定义可能会产生,而链接器在尽量优化和避免的前提下也放任这种情况而取其一。
很幸运,编译器为我们处理了大多数细节。
Template concept
语法
模板的使用很简单,在Bjarne的著作中可以看到为此可谓大煞苦心。
一个template作为标示(对人也对编译器),一个<>指定参数(圆括号过度滥用),足矣!然而这种简单中却隐藏着无法预知的不简单。
一方面为了语法简单,编译器为我们做了很多事情,同时也隐藏了很多内幕;另一方面在应用过程中各种复杂的应用与设计,估计也超出了当初Bjarne的想象。
在这里主要针对模板、引数推导、实例化三个必不可少的步骤简单说明,对于(偏)特化、重载、继承等暂不涉及。
●模板
C++引入模板的最初目的是设计容器,因此类模板得到最先支持,随后很多算法函数的移植以及
概念上的统一,也引入了函数模板的概念。
对于模板参数,C++采用宽松的语法规则,既可以指
类型,也可以指定非类型参数(常数等);既可以是泛型参数,也可以局部特化。
类模板支持继
承机制,函数模板也支持重载。
此外,由于虚函数的动态绑定机制,不支持虚函数的模板。
●参数推导
如上所述,模板支持C++面向对象的大多数语法。
当我们写出一个类模板运行后,感叹“that’s good”
时,我们有没有想过,编译器为此付出了多少?可以说,如果没有名称查询机制,类不会被编
译器了解,如果没有引数推导,参数类型则不得不显示指定。
名称查询,分为(非)受限名称,(非)依赖名称。
C++引入命名空间,各种运算符的混杂当编
译器遇到一个名称,需要寻找该名称引用的对象,这并不是一件轻松的事情。
在模板中,如果
普通查找没有找到该名称,则对该名称相关的类和命名空间组成集合,在第二次查找中再进行
查找(会忽略using),为此C++定义了一些规则来描述“相关”,称之为ADL,尽可能找到名称所
对应的类型【C++ Template第九章】
引数推导:在STL中占有很重要的地位,针对模板(成员)函数,不适用于模板类。
编译器会按
照一套规则就隐式寻找最佳参数类型匹配,这就免去了用户指定类型的繁杂操作。
相对普通函
数的推导,模板中采用的是精确匹配【C++ Template第二、十一章】
●实例化
当我们在代码角度实现模板功能,当编译器能够找到正确的名称和参数类型后,我们的编译器
便迎来的最后一步:实例化,将模板解析成真正的C++代码。
C++实例化即代码具现,主要分为两步,非依赖型在第一步就已经具现,针对依赖性则在第二步
通过ADL和引数推导进行具现(POI),生成最后的普通C++代码。
【C++ Template第十章】
设计
模板作为全新的语言机制,也带来了很多新的设计理念,确实也产生了意想不到的威力。
也是目前C++世界最为热门的讨论内容。
这里仅列出两个尝尝鲜。
trait与policy:在STL内存管理特别是优化代码中有比较重要的价值。
定义一系列属性,在代码交互中形成“标准协议”。
其中trait注重类型,而policy注重行为。
从设计的角度讲,这其实非常简单,说是设计我觉得有点牵强,不过在作用角度,特别对复杂情况和几乎无从处理的问题,这或许是一个简单可行的好方式。
个人觉得trait更应该是对类设计人员知识水平和使用习惯的新要求。
【C++ Template第十五章】metaprogram:利用模板编译期间解析的特点,通过模板递归的方式,可以使用模板在编译期进行计算,在性能要求较高的程序中会带来不错的好处。
不过个人觉得用法限制很大,在大部分性能没有变态要求的情况下采用此方式多少有点得不偿失。
【C++ Template第十七章】
应用
智能指针:也算是高级应用吧,通过模板绑定(独占、共享),让模板类来负责内存的管理,而在使用中和原生质真没有区别。
成本很低,价值很大。
在复杂逻辑情况下,内存很难管理的应用中采用智能指针应该是一个不错的原则。
不过如果能够自己创建一个具备垃圾回收的new函数,这种内存池机制在C++中已经没有太多难度,算法、框架以及效率上都比较成熟,我想更有价值【C++ Template第二十章】仿函数:……【C++ Template第二十二章】
STL component
STL的本质思想是将算法和数据结构之间独立,可以说是对数据结构的完美实践的精品。
各种数据结构的布局、应用;常用算法的支持;主流应用的封装;坦白说相对模板的复杂多变,STL的知识则非常的少,可谓是小而精干,背后拥有先进的技术和深厚的理论。
下图是STL六大组件的相互关系。
STL框架关系【截取自STL源码剖析】
从图中可以看到,在整个STL框架中,一切都以容器为中心,为了更有效的数据布局,抽离出Allocator 模块;为了将行为与数据结构独立,则产生了迭代器的概念;而算法模块匹配迭代器,提供了一种全局函数,特定的算法往往对应特定的数据结构,因此了解不同的算法针对行的优势和缺陷,对我们解决问题有着至关重要的价值;为了使用的方便,STL也为我们提供了一些工具包和各种适配器。
STL Annotated
《STL源码剖析》,一言以蔽之,STL的《Inside C++ Object Model》。
很难得的一本好书,读完让人有一种很舒畅的感觉,虽然SGI和标准有一定出入,但是在思想上基本一致,建议每一个学习STL的人都要居家常备。
相对Template在语法设计方面的复杂性,STL本身在这些层面上并则是小巫见大巫了,不过STL 在数据管理和产品化结构框架的思想和技巧则非常的有水准,值得大家所了解。
在我阅读这本书的时候,个人觉得最精彩的部分是Allocator和Containers这几个章节,也对比了STLPort、MFC、VS2009中相关部分
的实现,下面很粗略的过一下我觉得最精彩的部分,我也有苦衷,都让侯捷写完了☹。
●Allocator模块
配置器作用很小,因为多数程序员都适用默认参数;配置器又至关重要,因为大多数操作本质
上都属于内存的操作,这与程序的效率和稳定性息息相关。
在配置器中,通过trait设计和特化,
多很多常用和可能的性能提升做了很多处理。
一级配置器:STL中默认的配置器其实在内存处理中比较简单,主要是通过全局函数来方便类型
的创建和释放。
在STL中对效率的考虑非常极致,比如对常用类型的特化,将内存分配和类型的
构造、析构过程分离,然后通过trait机制来尽量避免无意义的构造和析构。
二级配置器:SGI特有,虽不是标准,也很难说有多大的使用场景,但其中内存池的引入,以及
数据结构的组织和管理,则显得极其优雅。
在二级配置器中主要负责小内存(超过128字节则
自动采用一级配置器),这往往是程序中最繁琐,最消耗的部分。
数据管理主要采用数组+链表
的方式来管理,数组用来管理不同大小的内存块,链表则管理相同大小的内存块,从而形成一
个二维内存管理,其中对使用内存块释放,增加,异常等进行了精致处理和优化,在书中有很
详细的介绍,有兴趣的可以多看一下。
内存管理函数:对内存的使用,STL提炼出一些常用的全局函数,这在容器的管理中使用率非常
频繁。
主要是用于数据构造的不同情况,以及数据本身的特点(POD)进行的优化,同样采用trait
方式。
在配置器中对内存的控制很精致,对很多使用场景和类型进行了丰富的处理,这也为STL在数据
管理以及算法应用打下了坚实的地基!
●Containers模块
很多人对STL的第一印象就是容器,或许也是唯一印象。
一门语言能有自己的容器,确实是值得
庆祝的事情,而C++的容器可以说得上是为数据结构量身定做的产品,非常可靠耐用。
每一种数据结构都有自己的优势,自然也继承了先天的劣势,STL容器的数量比较丰富,简单而
言可以分为两大类,一类是以vector和list为代表,另一类则以set和map为代表。
vector:在小数据的情况下具有极佳的效果,由于内存连续,用法非常方便。
在空间分布中一般
采用倍数增长的机制。
在插入和删除等先天劣势的功能中也做了很多优化工作,比如插入中会
参考插入数据量与剩余空间、可用空间的逻辑关系,尽可能采用批处理的方式来改进效率。
list:vector和list可以说的上是最为常用的容器类型,list在功能角度可以说和vector基本相同,
但在应用场景上和vector则有不同的侧重。
相对vector 的内存连续,list在内存上离散分别,因
此在大数据下具有一定优势,功能上比较灵活,没有太多优化的空间,当然因为数据结构本身
导致迭代器的性能相对vector有一定损失,要视具体数据来分析。
rb_tree:平衡二叉树的一种改进,set、map容器的数据结构。
在功能角度将算得上是vector和
list的折中,即避免了vector的连续内存的要求,有可以改善list在数据查询等方面的差强人意。
在实现角度,rb_tree则与前者有着巨大的不同(自动排序),也有着先天的劣势(插入),结构
较为复杂,为了在数据插入中排序和平衡会有较为繁琐的调整过程,而这也是二叉树最为核心
的部分。
当然,迭代器的实现相对list也更复杂一些。
●不同STL简单对比
在学习过程中,相比MFC、STLPort和VS2009做了一些研究,赞叹这些神来之笔之外也有一些大
跌眼镜之处,下面结合vector列出几点不同:
创建对象:在MFC的容器中,所有内存以char*为基本大小,并没有使用stl,在构造函数中只
是简单的初始化工作,基本上花费时间很少;而在STLPort中,通过初始化列表的优化,但由于
继承导致的“子函数”的因素,在效率上有一些损失;在VS中,本身编译器会有很多优化,但
在安全机制上的if判断,还有子函数的调用,以及没有采用初始化列表的原因,性能几乎无法
忍受。
内存增长机制:mfc采用线型的动态增长方式(默认),而stl中采用每次增长50% 方式,在多
数情况下减少申请内存的次数,要优于mfc。
另外,vc9采用memcpy而STLPort采用memmove
总结:通过容器的对比,大可以感觉出三者之间细微处的不同,也增加了我们使用容器的经验。
在vector中设置自动增长值非常有必要,而且在不必要情况下不要随便创建空的vector对象,
在创建时尽可能设置空间大小,即使大点也可以接受。
在其他如插入删除获取等相关函数在处
理思路上和stl的都基本一致,MFC与STLPort在这些功能上的效率基本相当。
最让人意外的在
VS中的STL加入了过多的安全处理和逻辑判断,代码上写的多少有点“大巧不工”,多少让人有
所失望,在最常用的构造、Add等函数中效率有不小的下降。
对于一款轻巧强大的STL来说,犹
如灌铅的羽毛。
Future
C++的标准化工作长达10年之久,我想这让它失去了一个最为关键的时期,而STL库过于精简,并没有提供类似Java那样丰富的库,这让C++失去了不少使用者。
诚然,这些问题的客观因素都可以归结于C++的复杂和易学难用的特点。
我也不清楚C++未来的方向会在哪,无论是丰富的标准库,还是泛型编程与面向对象的演绎,或者是高效可移植语言的推广,引入成熟的内存管理、回收机制,甚至是Beyond the C++ Standard Library,C++都有无限的可能。
而且随着现代编译器的优化,我完全相信C++在效率问题上已经不输于C。
此外,C++是灵活优雅的,在理论体系上相当的完美,当你深入理解后你会被它的优雅所折服。
因此,C++可以说得上是绩优潜力股,丰富的弹性和强大的功能在各个方面都有很多空间,我也永远看好C++的未来。
当然,作为技术人员,技术和语言没有好坏之分,往往都有此一时彼一时的起伏,希望所有的人都能够耐心学习几门语言,大有益处,或许还会影响你分析事物的角度。
由于篇幅限制,在很多地方上做了不少缩水,和当初设想的内容还是有一定出入,不少内容只是写了一些个人认为最有必要留意的部分,我想今后会在此基础上继续添加新的感悟。
当然如果希望对上述内容有一个详细的理解,确实还是如云风所说,还是去书店看看那一排排的大部头吧,知识永远不可能被如此压缩的一篇文章所取代。
不过写完这篇学习总结,多少有些释然,也算圆了自己的一个承诺,只是没想到会拖了一年之久,想想一年来的变化和不变化,多少有点感慨。
突然觉得自己有些老了,少了一点激情,多了一丝无奈。
不管怎样,如果能看到这里,我都很感谢你,哪怕只能引起你一步的留恋或一秒的回忆,我也会觉得很满足。
PS:之所以限制篇幅,最重要的原因是本着笔筒以稀为贵的原则,都是限量版哦,呵呵。
参考书籍
C++语言的设计和演化
深入探索C++模型
C++模板
C++标准程序库
STL源码剖析。