C++提供了几种不同的容器供你选择,可是你有没有意识到它们的不同点在哪里?为了防止你在选择时有所疏忽,这里给出了简要回顾:
? 标准STL 序列容器:vector、string、deque 和list。
? 标准STL 关联容器:set、multiset、map 和multimap。
? 非标准序列容器slist 和rope。slist 是一个单向链表,rope 本质上是一个“重型”string。
(“rope”是重型“string”,明白了吗?)你可以在第50 条中找到对这些非标准(但通常可以使用)的容器的一个简要介绍。
? 非标准的关联容器hash_set、hash_multiset、hash_map 和hash_multimap。在第25 条中,
我分析了这些基于哈希表的、标准关联容器的变体(它们通常是广泛可用的)。
? vector作为string 的替代。第13 条讲述了在何种条件下这种替代是有意义的。
? vector 作为标准关联容器的替代。正如第23 条中所阐明的,有时vector 在运行时间和空
间上都要优于标准关联容器。
? 几种标准的非STL 容器,包括数组、bitset、valarray、stack、queue 和priority_queue。因为它们不是STL 容器,所以在本书中很少提及,仅在第16 条中提到了一种“数组优于STL 容器”的情形,以及在第18 条中解释了为什么bitset 比vector>要好。值得记住的是,数组也可以被用于STL 算法,因为指针可以被用作数组的迭代器。
可以做出的选择是很多的,选择的多样性意味着在做选择时要考虑多种因素。不幸的是,大多数关于STL 的讨论对于容器的世界涉及很浅,在“如何选择最合适的容器”这一问题上忽略了很多因素。即便是C++标准也是如此。C++标准就“如何在vector、deque 和list 中做出选择”提供了如下建议:
vector、list 和deque 为程序员提供了不同的复杂性,使用时要对此做出权衡。vector是默认应使用的序列类型;当需要频繁地在序列中间做插入和删除操作时,应使用list;当大多数插入和删除操作发生在序列的头部和尾部时,deque 是应考虑的数据结构。
如果算法复杂性是主要的考虑因素,我认为以上的建议是恰当的,但除此之外需要考虑的还有很多。
稍后我们将讨论与算法复杂性相对应的有关容器的重要问题。但首先我要引入对STL 容器的一种分类方法,该方法没有得到应有的重视。这就是对连续内存容器(contiguous-memorycontainer)和基于节点的容器(node-based container)的区分。
连续内存容器(也称为基于数组的容器,array-based container)把它的元素存放在一块或多块(动态分配的)内存中,每块内存中存有多个元素。当有新元素插入或已有的元素被删除时,同一内存块中的其他元素要向前或向后移动,以便为新元素让出空间,或者填充被删除元素所留下的空隙。这种移动影响到效率(参见第5 条、第14 条)和异常安全性(我们很快将会看到这一点)。标准的连续内存容器有vector、string 和deque。非标准的rope 也是一个连续内存容器。
基于节点的容器在每一个(动态分配的)内存块中只存放一个元素。容器中元素的插入或删除只影响到指向节点的指针,而不影响节点本身的内容,所以当有插入或删除操作时,元素的值不需要移动。表示链表的容器,如list 和slist,是基于节点的;所有标准的关联容器也是如此(通常的实现方式是平衡树)。非标准的哈希容器使用不同的基于节点的实现,在第25 条将会看到这一点。
有以上这些术语作为基础,我们将概括出选择容器时最重要的一些问题。在接下来的讨论中,我将不考虑非STL 容器(如数组、bitset 等),因为本书毕竟是一本关于STL 的书。
你是否需要在容器的任意位置插入新元素?如果需要,就选择序列容器;关联容器是不行的。
? 你是否关心容器中的元素是排序的?如果不关心,则哈希容器是一个可行的选择方案;否则,你要避免哈希容器。
? 你选择的容器必须是标准C++的一部分吗?如果必须是,就排除了哈希容器、slist 和rope。
? 你需要哪种类型的迭代器?如果它们必须是随机访问迭代器,则对容器的选择就被限定为vector、deque 和string。或许你也可以考虑rope(有关rope 的资料,见第50 条)。如果要求使用双向迭代器,那么你必须避免slist(见第50 条)及哈希容器的一个常见实现(见第25 条)。
? 当发生元素的插入或删除操作时,避免移动容器中原来的元素是否很重要?如果是,就要避免连续内存的容器(见第5 条)。
? 容器中数据的布局是否需要和C 兼容?如果需要兼容,就只能选择vector (见第16 条)。
? 元素的查找速度是否是关键的考虑因素?如果是,就要考虑哈希容器(见第25 条)、排序的vector(见第23 条)和标准关联容器——或许这就是优先顺序。
? 如果容器内部使用了引用计数技术(reference counting),你是否介意?如果是,就要避免使用string,因为许多string 的实现都使用了引用计数。rope 也需要避免,因为权威的rope 实现是基于引用计数的(见第50 条)。当然,你需要某种表示字符串的方法,这时你可以考虑vector。
? 对插入和删除操作,你需要事务语义(transactional semantics)吗?也就是说,在插入和删除操作失败时,你需要回滚的能力吗?如果需要,你就要使用基于节点的容器。如果对多个元素的插入操作(即针对一个区间的形式——见第5 条)需要事务语义,则你需要选择list,因为在标准容器中,只有list 对多个元素的插入操作提供了事务语义。对那些希望编写异常安全(exception-safe)代码的程序员,事务语义显得尤为重要(使用连续内存的容器也可以获得事务语义,但是要付出性能上的代价,而且代码也显得不那么直截了当。更多细节,请参考Sutter 的Exceptional C++[8]中的第17 条)。
? 你需要使迭代器、指针和引用变为无效的次数最少吗?如果是这样,就要使用基于节点的容器,因为对这类容器的插入和删除操作从来不会使迭代器、指针和引用变为无效(除非它们指向了一个你正在删除的元素)。而针对连续内存容器的插入和删除操作一般会使指向该容器的迭代器、指针和引用变为无效。
? 如果在容器上使用swap,使得迭代器、指针或引用变为无效了,你会在意吗?如果在意,那么你要避免使用string,因为string 是STL 中在swap 过程中会导致迭代器、指针和引用变为无效的唯一容器。
? 如果序列容器的迭代器是随机访问类型,而且只要没有删除操作发生,且插入操作只发生在容器的末尾,则指向数据的指针和引用就不会变为无效,这样的容器是否对你有帮助?这是非常特殊的情形,但如果你面对的情形正是如此,则deque 是你所希望的容器(有意思的是,当插入操作仅在容器末尾发生时,deque 的迭代器有可能会变为无效。deque 是唯一的、迭代器可能会变为无效而指针和引用不会变为无效的STL 标准容器)。
这些问题并没有涵盖所有的情形。比如,它们没有考虑不同容器类型所采取的不同的内存分配策略(第10 条和第14 条讨论了这些策略的某些方面)。但它们应该能使你明白,除非你不关心元素的排序情况、是否与标准相符、迭代器的能力、元素布局与C 的兼容性、查找速度、因引用计数所引起的反常行为、是否便于实现事务语义,以及迭代器在何种条件下变为无效,否则,除了容器操作的算法复杂性,你还需要考虑更多的因素。算法复杂性是很重要,但它远不是问题的全部。
对于容器,STL 给了你多种选择。在STL 以外,你还有更多的选择。在选择一个容器之前,请仔细考虑所有的选择。存在“默认的容器”吗?我可不这样认为。
本文节选自《Effective STL中文版:50条有效使用STL的经验(双色)》
【美】梅耶 (Meyers,S.) 著
潘爱民 陈铭 邹开红 译.
电子工业出版社出版
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/13164110/viewspace-764304/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/13164110/viewspace-764304/