Effective STL中文版:50条有效使用STL的经验.docx
《Effective STL中文版:50条有效使用STL的经验.docx》由会员分享,可在线阅读,更多相关《Effective STL中文版:50条有效使用STL的经验.docx(125页珍藏版)》请在课桌文档上搜索。
1、EffeCtiVeSTL中文版:50条有效使用STL的经验1. 1容器2. 2vector和SIring3. 3关联容器4. 4迭代器5. 5算法6. 6函数子、函数子类、函数及其他7. 7在程序中使用STL8. 参考书目9. 附录A地域性与忽略大小写的字符串比较10. 附录B对MiCrOSoft的STL平台的说明11-后折页12. 1容器没错,STL中有迭代器(iterator)s算法(algorithm)和函数对象(functionobject),但是对于大多数C+程序员来说,最值得注意的还是容器。容器比数组功能更强大、更灵活。它们可以动态增长(和缩减),可以自己管理内存,可以记住自己包含
2、了多少对象。它们限定了自己所支持的操作的复杂性。诸如此类的优点还有很多。不难理解它们为何如此受欢迎,因为相对于其竞争者,无论是来自其他库中的容器还是你自己编写的容器,其优越性是显而易见的。STL容器不是简单的好,而是确实很好。本章讲述适用于所有STL容器的准则。随后几章将就特定类型的容器展开论述。本章内容包括:如何就你所面临的具体制约条件选择适当的容器类型;避免一种错误认识,即为一种类型的容器而编写的代码换了其他容器也能工作;对于容器中的对象,复制操作的重要性;当指针或者auto_ptr被存放在容器中时会有什么样的困难;删除操作的细节;用定制的分配子能做什么及不能做什么;使程序获得最高效率的窍
3、门;以及在多线程环境中使用容器时日一些箸虑。涉及的方方面面很多。别着急,饭要一口一口地吃。这些问题将分为几条,逐条下来,你一定会形成一些想法,并将这些想法应用到你正在编写的代码中。第1条:慎重选择容器类型。C+提供了几种不同的容器供你选择,可是你有没有意识到它们的不同点在哪里?为了防止你在选择时有所疏忽,这里给出了简要回顾: 标准STL序列容器:vector、stringsdeque和list。 标准STL关联容器:set、multisetsm叩和multimap。 非标准序列容器SliSt和rope。SliSt是一个单向链表,rope本质上是一个“重型Stringo(“rope”是重型“si
4、ring”,明白了吗?)你可以在第50条中找到对这些非标准(但通常可以使用)的容器的一个简要介绍。 非标准的关联容器hash_set、hash-multisetshash_mapOhash_multimapo在第25条中,我分析了这些基于哈希裴的、标准关联容器的变体(它们通常是广泛可用的)。 VeCtOr作为String的替代。第13条讲述了在何种条件下这种替代是有意义的。 vector作为标准关联容器的替代。正如第23条中所阐明的,有时VeCtor在运行时间和空间上都要优于标准关联容器。 几种标准的非STL容器,包括数组、bitset.valarraysstack、queue和PriOrit
5、y_queue因为它们不是STL容器,所以在本书中很少提及,仅在第16条中提到了一种“数组优于STL容器”的情形,以及在第18条中解释了为什么bitset比VeCtOr要好。值得记住的是,数组也可以被用于STL算法,因为指针可以被用作数组的迭代器。可以做出的选择是很多的,选择的多样性意味着在做选择时要考虑多种因素。不幸的是,大多数关于STL的讨论对于容器的世界涉及很浅,在“如何选择最合适的容器”这一问题上忽略了很多因素。即便是C+标准也是如此。C+标准就“如何在vector、deque和IiSt中做出选择”提供了如下建议:VeCtOr、IiSt和deque为程序员提供了不同的复杂性,使用时要对
6、此做出权衡。VeCtor是默认应使用的序列类型;当需要频繁地在序列中间做插入和删除操作时,应使用list;当大多数插入和删除操作发生在序列的头部和尾部时,deque是应考虑的数据结构。如果算法复杂性是主要的考虑因素,我认为以上的建议是恰当的,但除此之外需要考虑的还有很多。稍后我们将讨论与算法复杂性相对应的有关容器的重要问题。但首先我要引入对STL容器的一种分类方法,该方法没有得到应有的重视。这就是对连续内存容器(contiguous.memorycontainer)和基于节点的容器(node-basedcontainer)的区分O连续内存容器(也称为基于数组的容器,array-basedcon
7、tainer)把它的元素存放在一块或多块(动态分配的)内存中,每块内存中存有多个元素。当有新元素插入或已有的元素被删除时,同一内存块中的其他元素要向前或向后移动,以便为新元素让出空间,或者填充被删除元素所留下的空隙。这种移动影响到效率(参见第5条、第14条)和异常安全性(莪们很快将会看至。这一点)。标港的连续血存容器有VeCu)r、String和deque。非标淄的rope也是一个连续内存容器。基于节点的容器在每一个(动态分配的)内存块中只存放一个元素。容器中元素的插入或删除只影响到指向节点的指针,而不影响节点本身的内容,所以当有插入或删除操作时,元素的值不需要移动。表示链表的容器,如IiSt
8、和slist,是基于节点的;所有标准的关联容器也是如此(通常的实现方式是平衡树)。非标准的哈希容器使用不同的基于节点的实现,在第25条将会看到这一点。有以上这些术语作为基础,我们将概括出选择容器时最重要的一些问题。在接下来的讨论中,我将不考虑非STL容器(如数组、bitset等),因为本书毕竟是一本关于STL的书。 你是否需要在容器的任意位置插入新元素?如果需要,就选择序列容器;关联容器是不行的。 你是否关心容器中的元素是排序的?如果不关心,则哈希容器是一个可行的选择方案;否则,你要避免哈希容器。 你选择的容器必须是标准C+的一部分吗?如果必须是,就排除了哈希容器、SHSt和rpeo 你需要哪
9、种类型的迭代器?如果它们必须是随机访问迭代器,则对容器的选择就被限定为VeCtor、deque和Stringo或许你也可以考虑rope(有关rope的资料,见第50条)。如果要求使用双向迭代器,那么你必须避免SliSt(见第50条)及哈希容器的一个常见实现(见第25条)。 当发生元素的插入或删除操作时,避免移动容器中原来的元素是否很重要?如果是,就要避免连续内存的容器(见第5条)。 容器中数据的布局是否需要和C兼容?如果需要兼容,就只能选择VeCtor(见第16条)。 元素的查找速度是否是关键的考虑因素?如果是,就要考虑哈希容器(见第25条)、排序的Veaor(见第23条)和标准关联容器一或许
10、这就是优先顺序。如果容器内部使用了引用计数技术(referencecounting),你是否介意?如果是,就要避免使用String,因为许多String而实现都使用了引用计数。rope也需要避免,因为权威的r。Pe实现是基于引用计数的(见第50条)。当然,你需要某种表示字符串的方法,这时你可以考虑VeetOr。对插入和删除操作,你需要事务语义(transactionalsemantics)吗?也就是说,在插入和删除操作失败时,你需要回滚的能力吗?如果需要,你就要使用基于节点的容器。如果对多个元素的插入操作(即针对一个区间的形式见第5条)需要事务语义,则你需要选择list,因为在标准容器中,只有
11、IiSt对多个元素的插入操作提供了事务语义。对那些希望编写异常安全(exceptionsafe)代码的程序员,事务语义显得尤为重要(使用连续内存的容器也可以获得事务语义,但是要付出性能上的代价,而且代码也显得不那么直截了当。更多细节,请参考SUtter的ExceptionQ/C+网中的第17条)。你需要使迭代器、指针和引用变为无效的次数最少吗?如果是这样,就要使用基于节点的容器,因为对这类容器的插入和删除操作从来不会使迭代器、指针和引用变为无效(除非它们指向了一个你正在删除的元素)。而针对连续内存容器的插入和删除操作一般会使指向该容器的迭代器、指针和引用变为无效。如果在容器上使用SWap,使得
12、迭代器、指针或引用变为无效了,你会在意吗?如果在意,那么你要避免使用String,因为String是STL中在SWaP过程中会导致迭代器、指针和引用变为无效的唯一容器。如果序列容器的迭代器是随机访问类型,而且只要没有删除操作发生,且插入操作只发生在容器的末尾,则指向数据的指针和引用就不会变为无效,这样的容器是否对你有帮助?这是非常特殊的情形,但如果你面对的情形正是如此,则deqUe是你所希望的容器(有意思的是,当插入操作仅在容器末尾发生时,deque的迭代器有可能会变为无效。deque是唯一的、迭代器可能会变为无效而指针和引用不会变为无效的STL标准容器)。这些问题并没有涵盖所有的情形。比如,
13、它们没有考虑不同容器类型所采取的不同的内存分配策略(第10条和第14条讨论了这些策略的某些方面)。但它们应该能使你明白,除非你不关心元素的排序情况、是否与标准相符、迭代器的能力、元素布局与C的兼容性、查找速度、因引用计数所引起的反常行为、是否便于实现事务语义,以及迭代器在何种条件下变为无效,否则,除了容器操作的算法复杂性,你还需要考虑更多的因素。算法复杂性是很重要,但它远不是问题的全部。对于容器,STL给了你多种选择。在STL以外,你还有更多的选择。在选择一个容器之前,请仔细考虑所有的选择。存在“默认的容器”吗?我可不这样认为。第2条:不要试图编写独立于容器类型的代码。STL是以泛化(gene
14、ralization)原则为基础的:数组被泛化为“以其包含的对象的类型为参数”的容器;函数被泛化为“以其使用的迭代器的类型为参数”的算法;指针被泛化为“以其指向的对象的类型为参数”的迭代器。这仅仅是开始。容器类型被泛化为序列和关联容器,类似的容器被赋予相似的功能。标准的连续内存容器(见第1条)提供了随机访问迭代器,而标准的基于节点的容器(也参见第1条)提供了双向迭代器。序列容器支持PUSh_from和/或PUShJack操作,而关联容器则不然。关联容器提供了对数时间的IoWerjXnmd、UPPerJXnmd和equal_range成员函数,但序列容器却没有提供。随着这样的泛化的不断进行,你自
15、然也想加入到这场运动中来。这种想法是值得赞赏的。当你编写自己的容器、迭代器和算法时,你当然想这么做。可是,很多程序员却以一种不同的方式做泛化。他们在自己的软件中不是针对某种具体的容器,而是想把容器的概念泛化,这样他们就能使用,比如VeCtOr,而仍保留以后将其换成CIeqUe或IiSt的选择但不必改变使用该容器的代码。也就是说,他们试图编写独立于容器的代码(containerindependentcode)o这类泛化,尽管出发点是好的,却几乎总是误入歧途。即便是最热心地倡导独立于容器类型的代码的人也会很快意识到,试图编写对序列容器和关联容器都适用的代码几乎是毫无意义的。很多成员函数仅当其容器为
16、某一种类型时才存在,例如,只有序列容器才支持PUSh_front或PUShJDack,只有关联容器才支持CoUm和lower_bound,等等。即使是insert和erase这样的基本操作,也会随容器类型的不同而表现出不同的原型和语义。比如,当你向序列容器中插入对象时,该对象位于被插入的位置处;而当你向关联容器中插入对象时,容器会按照其排序规则,将该对象移动到适当的位置上。又如,当带有一个迭代器参数的erase作用于序列容器时,会返回一个新的迭代器,但当它作用于关联容器时则没有返回值(第9条给出了一个例子,说明这将如何影响你的代码)。假设你想编写对于大多数通用的序列容器(即vector、deq
17、ue和IiSt)都适用的代码,那么很显然,你的程序只能使用它们的功能的交集,这意味着你不能使用reserve或capacity(见第14条),由山deque而IiSt中没有这样的操作。由手底旃存在,熹味着你也要放弃OPeratO山,而且你要把操作限制在双向迭代器的能力范围之内。进一步说,这意味着你要放弃那些要求随机访问迭代器的操作,包括Sort、Stable_sort、PartiaLSOrt和nth_element(见第31条)。另一方面,为了要支持VeCtor,你就不能使用PUSh_front和pop_front;但是对于VeCtor和deque而言,SPIiCe和成员函数形式的SOrt又是
18、被禁用的。结合以上这些限制条件,最后的这一限制意味着你的“泛化的序列容器”将没有任何形式的Sort可供使用。这是显而易见的。如果违背了上述任何限制,那么你的代码对某一种你想使用的容器将无法编译通过。而能够编译通过的代码则更加危险。这些限制的根源在于,对不同类型的序列容器,使迭代器、指针和引用无效(invalidate)的规则是不同的。要想使你的代码对VeCto、deque和IiSt都能工作,你必须要假定,对任何一种容器,使迭代器、指针和引用无效的任何操作将在你所使用的容器上使它们无效。所以,你必须假定每个insert调用都使所有迭代器、指针和引用无效,因为deque:insert使所有迭代器无
19、效。而且,由于不能调用CaPaCity,因此VeCtOr:insert必须保证使所有的指针和引用也无效(第1条说明了deque比较独特,它有时候可以使迭代器无效而不必使指针和引用也无效)。类似的推理可得出结论,对erase的每次调用都要假定使一切变为无效。还想听到更多吗?你不能把容器中的数据传递到C接口中,因为只有VeCtor支持这一点(见第16条)。你不能使用bool作为要存储的对象类型来实例化(instantiate)你的容器,因为,如第18条所示,VeCtor并不总是表现得像一个VeCtOl,它实际上并没有存储bool类型的对象。你不能假定IiSt的常数时间的插入和删除操作,因为VeCt
20、Or和deque进行此类操作耗费的是线性时间。当所有这些限制都被遵守之后,你的“泛化的序列容器”将不能使用reserve、capacity.OPerator口、PUSh_front、popjront.splice,以及任何要求随机访问迭代器的操作;每次对该容器执行insert和erase操作将耗费线性时间,并将所有的迭代器、指针和引用变为无效;该容器内部的布局将和C不兼容,不能存储bool类型的数据。这样的容器真的是你想在应用中使用的容器吗?我认为不会是这样的。如果你没有这么野心勃勃,决定不支持list,那么你依然不能使用reserve、CaPaCity、PUSlLfrOnt和pop_fron
21、t;依然要假定对insert和erase的每次调用都耗费线性时间并使一切(迭代器、指针和引用)变为无效。你依然失去了和C的布局兼容性;你依然不能存储bool类型的数据。如果你放弃序列容器,转而寻求对于不同的关联容器都能工作的代码,情况并不会好到哪里去。编写对于Set和map都适用的代码几乎是不可能的,因为Set存储单个对象而map存储“一对”对象。即便是编写对于Set和multiset域map和multimap)都适用的代码也未是一件容易的事情。以单个值为参数的insert成员函数对于set/m叩和与之对应的multi类型有不同的返回类型,同时你必须十分小心,不要对容器中同一个值有多少拷贝做任
22、何假定。如果使用m叩和multim叩,那么你要避免使用OPerato山,因为这个成员函数只对map存在。面对现实吧:这么做不值得。不同的容器是不同的,它们有非常明显的优缺点。它们并不是被设计来交换使用的,你无法掩盖这一点。如果试图这样做,那么只是在碰运气,而这种运气却是碰不得的。但是,依然可能会有这么一天,你意识到自己所选择的容器类型不是最佳的,所以你想使用另一种容器类型。你已经知道,当改变容器类型时,不仅需要改正编译器诊断出的问题,还要检查使用该容器的所有代码,以便发现按照新类型的性能特点和它使迭代器、指针及引用无效的规则,代码要做出何种改动。如果你想从VeCtor转到其他类型,要确保不再依
23、赖于VeaOr与C的布局兼容性;如果是从其他类型转到VeCtor,要确保没有用它来存储bool类型的数据。考虑到有时候不可避免地要从一种容器类型转到另一种,可以使用常规的方式来实现这种转变:使用封装(encapsulation)技术。最简单的方式是通过对容器类型和其迭代器类型使用类型定义(typedef)。因此,不要这样写:而要这样写:这样就使得改变容器类型要容易得多,尤其当这种改变仅仅是增加一个自定义的分配子时,就显得更为方便(这一改变不影响使迭代器/指针/引用无效的规则)。即使你没有意识到这些类型定义所带来的封装效果,你可能也会很欣赏它们所节省的工作。比如你有如下类型的对象而你想用COnS
24、LiteratOr来遍历此m叩,你真的想把敲入很多遍吗?当你使用STL有少许经验后,会发现类型定义可以帮你的忙。类型定义只不过是其他类型的别名,所以它带来的封装纯粹是词法(lexical)上的。类型定义并不能阻止一个客户去做(或依赖)它们原本无法做到(或依赖)的事情。如果你不想把自己选择的容器暴露给客户(client),就得多费点劲儿。你需要使用类(class)。要想减少在替换容器类型时所需要修改的代码,可以把容器隐藏到一个类中,并尽量减少那些通过类接口(而使外部)可见的、与容器相关的信息。比如,当你想创建一个顾客列表时,不要直接使用list。相反,创建一个CUStOmerLiSt类,并把Ii
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Effective STL中文版:50条有效使用STL的经验 STL 中文版 50 有效 使用 经验

链接地址:https://www.desk33.com/p-675843.html