mmxingye

导航

04 | 容器汇编 I:比较简单的若干容器

string


string 一般并不被认为是一个 C++ 的容器。但鉴于其和容器有很多共同点,我们先拿 string 类来开说。
string 是模板 basic_string 对于 char 类型的特化,可以认为是一个只存放字符 char 类型数据的容器。“真正”的容器类与 string 的最大不同点是里面可以存放任意类型的对象。
跟其他大部分容器一样, string 具有下列成员函数:

  • begin 可以得到对象起始点
  • end 可以得到对象的结束点
  • empty 可以得到容器是否为空
  • size 可以得到容器的大小
  • swap 可以和另外一个容器交换其内容

(对于不那么熟悉容器的人,需要知道 C++ 的 begin 和 end 是半开半闭区间:在容器非空时,begin 指向第一个元素,而 end 指向最后一个元素后面的位置;在容器为空时,begin 等于 end。在 string 的情况下,由于考虑到和 C 字符串的兼容,end 指向代表字符串结尾的 \0 字符。)

string 当然是为了存放字符串。和简单的 C 字符串不同

  • string 负责自动维护字符串的生命周期
  • string 支持字符串的拼接操作(如之前说过的 + 和 +=)
  • string 支持字符串的查找操作(如 find 和 rfind)
  • string 支持从 istream 安全地读入字符串(使用 getline)
  • string 支持给期待 const char* 的接口传递字符串内容(使用 c_str)
  • string 支持到数字的互转(stoi 系列函数和 to_string)

如果实现较为复杂、希望使用 string 的成员函数的话,那就应该考虑下面的策略

  • 如果不修改字符串的内容,使用 const string& 或 C++17 的 string_view 作为参数类型。后者是最理想的情况,因为即使在只有 C 字符串的情况,也不会引发不必要的内存复制。
  • 如果需要在函数内修改字符串内容、但不影响调用者的该字符串,使用 string 作为参数类型(自动拷贝)。
  • 如果需要改变调用者的字符串内容,使用 string& 作为参数类型(通常不推荐)。

string name;
cout << "What's your name? ";
getline(cin, name);
cout << "Nice to meet you, " << name
     << "!\n";

vector


除了容器类的共同点,vector 允许下面的操作(不完全列表)

  • 可以使用中括号的下标来访问其成员(同 string)
  • 可以使用 data 来获得指向其内容的裸指针(同 string)
  • 可以使用 capacity 来获得当前分配的存储空间的大小,以元素数量计(同 string)
  • 可以使用 reserve 来改变所需的存储空间的大小,成功后 capacity() 会改变(同 string)
  • 可以使用 resize 来改变其大小,成功后 size() 会改变(同 string)
  • 可以使用 pop_back 来删除最后一个元素(同 string)
  • 可以使用 push_back 在尾部插入一个元素(同 string)
  • 可以使用 insert 在指定位置前插入一个元素(同 string)
  • 可以使用 erase 在指定位置删除一个元素(同 string)
  • 可以使用 emplace 在指定位置构造一个元素
  • 可以使用 emplace_back 在尾部新构造一个元素

当 push_back、insert、reserve、resize 等函数导致内存重分配时,或当 insert、erase 导致元素位置移动时,vector 会试图把元素“移动”到新的内存区域。vector 通常保证强异常安全性,如果元素类型没有提供一个保证不抛异常的移动构造函数,vector 通常会使用拷贝构造函数。因此,对于拷贝代价较高的自定义元素类型,我们应当定义移动构造函数,并标其为 noexcept,或只在容器中放置对象的智能指针。这就是为什么我之前需要在 smart_ptr 的实现中标上 noexcept 的原因。

C++11 开始提供的 emplace… 系列函数是为了提升容器的性能而设计的。

deque


deque 不提供 data、capacity 和 reserve 成员函数。

如果你需要一个经常在头尾增删元素的容器,那 deque 会是个合适的选择。

list



需要指出的是,虽然 list 提供了任意位置插入新元素的灵活性,但由于每个元素的内存空间都是单独分配、不连续,它的遍历性能比 vector 和 deque 都要低。这在很大程度上抵消了它在插入和删除操作时不需要移动元素的理论性能优势。如果你不太需要遍历容器、又需要在中间频繁插入或删除元素,可以考虑使用 list。

另外一个需要注意的地方是,因为某些标准算法在 list 上会导致问题,list 提供了成员函数作为替代,包括下面几个:

  • merge
  • remove
  • remove_if
  • reverse
  • sort
  • unique

forward_list


大部分 C++ 容器都支持 insert 成员函数,语义是从指定的位置之前插入一个元素。对于 forward_list,这不是一件容易做到的事情(想一想,为什么?)。标准库提供了一个 insert_after 作为替代。此外,它跟 list 相比还缺了下面这些成员函数:

为什么会需要这么一个阉割版的 list 呢?原因是,在元素大小较小的情况下,forward_list 能节约的内存是非常可观的;在列表不长的情况下,不能反向查找也不是个大问题。提高内存利用率,往往就能提高程序性能,更不用说在内存可能不足时的情况了。

queue


依赖于某个现有的容器,因而被称为容器适配器(container adaptor)。
它的实际内存布局当然是随底层的容器而定的。从概念上讲,它的结构可如下所示:

stack


类似地,栈 stack 是后进先出(LIFO)的数据结构。

  • 用 emplace 替代了 emplace_back,用 push 替代了 push_back,用 pop 替代了 pop_back;没有其他的 push_…、pop_…、emplace…、insert、erase 函数

posted on 2022-04-07 22:26  独立树  阅读(65)  评论(0编辑  收藏  举报