STL三十条
+ -
管理员 VIP用户 编辑
2023-10-16 9 0

条款1:仔细选择你的STL容器

你知道C++中有很多你可以支配的容器,但是你意识到有多少吗?要确定你没有忽略你的选项,这里有一个快速回顾。

  • 标准STL序列容器:vector、string、deque和list。
  • 标准STL关联容器:set、multiset、map和multimap。
  • 非标准序列容器slist和rope。slist是一个单向链表,rope本质上是一个重型字符串。(“绳子(rope)”是重型的“线(string)”。明白了吗?)你可以找到一个关于这些非标准(但常见的)容器的概览在条款50。
  • 非标准关联容器hash_set、hash_multiset、hash_map和hash_multimap。我在条款25检验了这些可以广泛获得的基于散列表的容器和标准关联容器的不同点。
  • vector<char>可以作为string的替代品。条款13描述了这个替代品可能会有意义的情况。
  • vector作为标准关联容器的替代品。就像条款23所说的,有时候vector可以在时间和空间上都表现得比标准关联容器好。
  • 几种标准非STL容器,包括数组、bitset、valarray、stack、queue和priority_queue。因为它们是非STL容器,所以在本书中关于它们我说得很少,虽然条款16提到了数组比STL容器更有优势的一种情况,而条款18揭示了为什么bitset可能比vector<bool>要好。值得注意的是,数组可以和STL算法配合,因为指针可以当作数组的迭代器使用。

这是所有的选项,而且可以考虑的范围和可以在它们之间的选择一样丰富。不走运的是,STL的大多数讨论只限于容器世界的一个很窄的视野,忽略了很多关于选择适当容器的问题。就连标准都介入了这个行动,提供了以下的在vector、deque和list之间作选择的指导方案:

vector、list和deque提供给程序员不同的复杂度,因此应该这么用:vector是一种可以默认使用的序列类型,当很频繁地对序列中部进行插入和删除时应该用list,当大部分插入和删除发生在序列的头或尾时可以选择deque这种数据结构。

如果你主要关心的是算法复杂度,我想这个方案是有理由的建议,但需要关心更多东西。

现在,我们要检查一些可以补充算法复杂度的重要的容器相关问题,但首先我需要介绍一种STL容器的分类方法,它被讨论的次数并不像它应该的那样多。那是连续内存容器和基于节点的容器的区别。

连续内存容器(也叫做基于数组的容器)在一个或多个(动态分配)的内存块中保存它们的元素。如果一个新元素被查入或者已存元素被删除,其他在同一个内存块的元素就必须向上或者向下移动来为新元素提供空间或者填充原来被删除的元素所占的空间。这种移动影响了效率(参见条款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)和标准的关联容器——大概是这个顺序。
  • 你介意如果容器的底层使用了引用计数吗?如果是,你就得避开string,因为很多string的实现是用引用计数(参见条款13)。你也不能用rope,因为权威的rope实现是基于引用计数的(参见条款50)。于是你得重新审核你的string,你可以考虑使用vector<char>。
  • 你需要插入和删除的事务性语义吗?也就是说,你需要有可靠地回退插入和删除的能力吗?如果是,你就需要使用基于节点的容器。如果你需要多元素插入(比如,以范围的方式——参见条款5)的事务性语义,你就应该选择list,因为list是唯一提供多元素插入事务性语义的标准容器。事务性语义对于有兴趣写异常安全代码的程序员来说非常重要。(事务性语义也可以在连续内存容器上实现,但会有一个性能开销,而且代码不那么直观。要了解这方面的知识,请参考Sutter的《Exceptional C++》的条款17[8]。)
  • 你要把迭代器、指针和引用的失效次数减到最少吗?如果是,你就应该使用基于节点的容器,因为在这些容器上进行插入和删除不会使迭代器、指针和引用失效(除非它们指向你删除的元素)。一般来说,在连续内存容器上插入和删除会使所有指向容器的迭代器、指针和引用失效。
  • 你需要具有有以下特性的序列容器吗:1)可以使用随机访问迭代器;2)只要没有删除而且插入只发生在容器结尾,指针和引用的数据就不会失效?这个一个非常特殊的情况,但如果你遇到这种情况,deque就是你梦想的容器。(有趣的是,当插入只在容器结尾时,deque的迭代器也可能会失效,deque是唯一一个“在迭代器失效时不会使它的指针和引用失效”的标准STL容器。)

这些问题几乎不是事情的完结。比如,它们没有关注不同的容器类型使用不同的内存配置策略(条款10和14讨论了这些策略的一些方面)。但是,它们已经足够是你信服了,除非你对元素顺序、标准的一致性、迭代器能力、内存布局和C的兼容性、查找速度、因为引用计数造成的行为不规则、事务性语义的轻松实现和迭代器失效的条件没兴趣,你得在容器操作的算法复杂度上花更多的考虑时间。当然这样的复杂度是重要的,但这离整个故事很远。

插入辅文

条款1:仔细选择你的STL容器
你知道C++中有很多你可以支配的容器,但是你意识到有多少吗?要确定你没有忽略你的选项,这里有一个快速回顾。标准STL序列容器:vector、string、deque和list。 标准STL关联容器:set、multiset、map和multimap。 非标准序列容器slist和rope。slis......
条款2:小心对“容器无关代码”的幻想
STL是建立在泛化之上的。数组泛化为容器,参数化了所包含的对象的类型。函数泛化为算法,参数化了所用的迭代器的类型。指针泛化为迭代器,参数化了所指向的对象的类型。这只是个开始。独立的容器类型泛化为序列或关联容器,而且类似的容器拥有类似的功能。标准的内存相邻容器(参见条款1)都提供随机访问迭代器,标准......
条款3:使容器里对象的拷贝操作轻量而正确
容器容纳了对象,但不是你给它们的那个对象。此外,当你从容器中获取一个对象时,你所得到的对象不是容器里的那个对象。取而代之的是,当你向容器中添加一个对象(比如通过insert或push_back等),进入容器的是你指定的对象的拷贝。拷进去,拷出来。这就是STL的方式。一旦一个对象进入一个容器,以后对......
条款7:当使用new得指针的容器时,记得在销毁容器前delete那些指针
STL中的容器非常优秀。它们提供了前向和逆向遍历的迭代器(通过begin、end、rbegin等);它们能告诉你所容纳的对象类型(通过value_type的typedef);在插入和删除中,它们负责任何需要的内存管理;它们报告容纳了多少对象和最多可能容纳的数量(分别通过size和max_size);......
条款8:永不建立auto_ptr的容器
坦白地说,本条款不需要出现在《Effective STL》里。auto_ptr的容器(COAPs)是禁止的。试图使用它们的代码都不能编译。C++ 标准委员会花费了无数努力来安排这种情况[1]。我本来不需要说有关COAPs的任何东西,因为你的编译器对这样的容器应该有很多抱怨,而且所有那些都是不能编译的......
条款9:在删除选项中仔细选择
假定你有一个标准STL容器,c,容纳int,Container c;而你想把c中所有值为1963的对象都去掉。令人吃惊的是,完成这项任务的方法因不同的容器类型而不同:没有一种方法是通用的。如果你有一个连续内存容器(vector、deque或string——参见条款1),最......
条款12:对STL容器线程安全性的期待现实一些
标准C++的世界是相当保守和陈旧的。在这个纯洁的世界,所有可执行文件都是静态链接的。不存在内存映射文件和共享内存。没有窗口系统,没有网络,没有数据库,没有其他进程。在这种情况下,当发现标准没有提到任何关于线程的东西时你不该感到惊讶。你对STL的线程安全有的第一个想法应该是它将因实现而不同。当然,多......
条款20:为指针的关联容器指定比较类型
假定你有一个string*指针的set,你把一些动物的名字插入进set:set ssp; // ssp = “set of string ptrs”ssp.insert(new string("Anteater......
条款23:考虑用有序vector代替关联容器
当需要一个提供快速查找的数据结构时,很多STL程序员立刻会想到标准关联容器:set、multiset、map和multimap。直到现在这很好,但不是永远都好。如果查找速度真得很重要,的确也值得考虑使用非标准的散列容器(参见条款25)。如果使用了合适的散列函数,则可以认为散列容器提供了常数时间的查找......
条款25:熟悉非标准散列容器
STL程序员一般用不了多久就开始惊讶,“vector、list、map,很好,但是散列(hash)表在哪里”?唉,在标准C++库里没有任何散列表。 每个人都同意这是个不幸,但是标准委员会觉得需要加给他们的工作可能会过渡地推迟标准的完成。可以肯定标准的下一个版本将包含散列表,但是目前,STL没有散列的......
条款30:确保目标区间足够大
STL容器在被添加时(通过insert、push_front、push_back等)自动扩展它们自己来容纳新对象。这工作的很好,有些程序员因为这个信仰而被麻痹,认为他们不必担心要为容器中的对象腾出空间,因为容器自己可以照顾好这些。如果是那样就好了!当程序员想向容器中插入对象但并没有告诉STL他们所想......
条款33:提防在指针的容器上使用类似remove的算法
在管理一堆动态分配的Widgets,每一个都可能通过检验,你把结果指针保存在一个vector中:class Widget{public: ... bool isCertified() const; // 这个Widget是否通过检验 ...};vector&l......
取消
感谢您的支持,我会继续努力的!
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

您的支持,是我们前进的动力!