C++ 标准模板库(STL) 之 顺序容器

引论

Standard Template Library (STL) 主要由两种组件构成: 一是容器 (container);二是操作这些容器的所谓泛型算法(generic algorithm)

vector 和 list 这两种容器属于顺序性容器(sequential container)。顺序性容器会依次维护第一个、第二个······直到最后一个元素。map 和 set 两种容器属于关联容器(associative container)。关联容器可以让我们快速查找容器中的元素值。

泛型算法提供了许多可作用于容器类及数组类型上的操作。这些算法之所以被称为泛型(generic),是因为它们和它们想要操作的元素类型无关(不论是 int、double、还是 string)。它们同样也和容器类彼此独立(不论是 vector、list 还是 array)。

泛型算法系通过 function template 技术,到达“与操作对象的类型相互独立”的目的。而实现“与容器无关”的诀窍,就是不要直接在容器身上进行操作。而是借一对 iterator (first 和 last),表示我们要进行迭代的元素范围。

指针的算术运算及迭代器的引入

通过以下例子来简单说明引入迭代器的好处。

问题简述

给定容器 vector 或者 arrary 数组,以及一个元素值,如果元素存在于该容器中,则返回指向该元素的指针,否则返回 0,要求考虑到程序的扩展性

解决思路

要容纳不同的容器类型,可以将问题切分为较小、较简单的子问题,避免直接传 vector 或者 arrary 数组等容器给问题解决的函数 find(),而只是传递其元素

实现

  1. 最直接的是传指针
    1. 如何指定限定容器的界:
      • 通过指定数组的大小
      • 给定标兵指针
    2. 如何通过指针访问到元素
      • 下标运算
      • 起始地址加上索引值
    3. 上述解决方案的指针运算存在的问题
      • vector 和 array 相同,都是以一块连续内存存储其所有元素,才可以通过指针加索引值直接访问到元素
      • 这种解决方法不能支持 list 容器(扩展性差
  2. 使用泛型指针 Iterator
    1. Iterator 应该支持指针的运算: ++, *, ==, !=
    2. 如何定义 iterator:
      • iterator 应该提供的信息:

        • 迭代对象(某个容器)的类型,可用来决定如何访问下一个元素
        • iterator 所指的元素类型,可决定 iterator 提领操作的返回值
      • 定义方法

        如, vector<string>::iterator iter,或者 vector<string>::const_iterator iter

  3. 扩展性考虑,需要考虑容器是否提供了 equality(相等) 运算符
    • 使用函数指针,取代原本固定使用的 equality 运算符
    • 使用 function object (一种特殊的 class),取代固定的 equality 运算符

顺序容器概述

容器类型概述

  • vector 和 string: 元素保存在连续的内存空间中,由元素的下标来计算其地址(随机访问)非常快速,但是在容器的中间位置添加或删除元素非常耗时。
  • deque: 跟方面与 string 和 vector 相似。但是在 dequeue 的两端添加或删除元素都是很快的,与 list 或 forward_list 添加删除元素的速度相当。
  • list 和 forward_list: 这两种容器设计的目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两种容器不支持元素的随机访问,且相对其他顺序容器,这两种容器的额外内存开销也很大。
  • array: 固定大小的数组

容器选择基本原则

  • 除非有很好的理由选择其他容器,否则应使用 vector (首选 vector
  • 如果程序中有很多小的元素,且空间的额外开销很重要,则不要使用 list 或 forward_list
  • 如果程序要求随机访问元素,应使用 vector 或 deque
  • 如果程序需要在容器的中间插入或删除元素,应使用 list 或 forward_list
  • 如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用 deque
  • 如果程序只有在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则
    1. 首先,确定是否真的需要在容器中间位置添加元素。在处理输入数据时,通常可以很容易地向 vector 追加数据,然后再调用标准库的 sort 函数来重排序容器中的元素,从而避免添加元素。
    2. 如果必须在中间位置插入元素,考虑在输入阶段使用 list,一旦输入完成,将 list 中的内容拷贝到一个 vector 中

当不确定使用哪种容器时

当不确实应该使用哪种容器时,可以在程序中使用 vector 和 list 公共的操作: 使用迭代器,不使用下标操作,避免随机访问。这样,在必要时选择使用 vector 或 list 都很方便

容器库概述

容器可以保存的元素类型

顺序容器几乎可以保存任意类型的元素。特别的,容器的元素可以是另一个容器

容器操作

很大程度上,容器只定义了极少的操作。每个容器都定义了构造函数、添加和删除元素的操作、确定容器大小的操作以及返回指向特定元素的迭代器的操作(能完成容器的增、删、查、改的操作)。其他一些有用的操作,如排序或搜索,并不是由容器类型定义的,而是由标准库算法实现的。

以下容器操作是所有容器都支持的。

  1. 迭代器

    迭代器指定的范围是左闭右合的。即一般指定范围的迭代器右端是指向末尾元素的下一个位置。
    与容器一样,迭代器有着公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。

  2. 容器类型成员

    迭代器是容器类型成员,包括 iterator、const_iterator 还有 reverse_iterator

  3. begin 和 end 成员

    容器的 begin 和 end 操作生成指向容器中第一个元素和尾元素之后位置的迭代器。

    begin 和 end 有多个版本: 带 r 的版本返回反向迭代器;以 c 开头的版本返回 const 迭代器

  4. 容器的定义和初始化

    容器定义支持指定容器大小和初始化列表

    标准库 array 具有固定大小。与内置数组一样,标准库 array 的大小也是类型的一部分。

  5. 赋值和 swap

    赋值运算包括 赋值运算符 = 和 assign 操作

    赋值相关的运算会导致指向左边容器内部的迭代器、引用和指针失效。而 swap 操作将容器内容交换则不会(容器类型为 array 和 string 的情况除外)。

  6. 容器大小操作

    除 forward_list 外,每个容器类型都支持以下三个与大小相关的操作:

    • size: 返回容器中元素的数目
    • max_size: 返回一个大于或等于该类型容器所能容纳的最大元素数的值
    • empty: 当 size 为 0 时返回 true,否则返回 false
  7. 关系运算符

    • 每个容器类型都支持相等运算符 (== 和 !=)
    • 除无序关联容器外的所有容器都支持关系运算符 (<、<=、>、 >=)
    • 如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相关的关系运算符

顺序容器操作

向顺序容器添加元素

  • 向顺序容器添加元素通常有 insert 操作和 emplace 操作,还有 push_back 操作
  • 这些操作会改变容器的大小, array 不支持这类操作,且会使指向容器的迭代器、引用和指针失效
  • push_back 的元素是拷贝,与提供值的原对象无任何关联
  • insert 的返回值是指向新插入的元素的迭代器,因此可以循环插入
  • emplace 插入元素是将参数传递给元素类型的构造函数,因此参数应该对应元素的构造函数的参数

元素访问

  • 包括 arrary 在内的所有顺序容器都有一个 front 成员函数,而除 forward_list 之外的所有顺序容器都有一个 back 成员函数
  • at 操作和下标操作只适用于 string、vector、deque、array
  • 对一个空容器调用 front 和 back,就像使用一个越界的下标一样,是一种严重的程序设计错误
  • 下标越界是一种严重的程序设计错误,而且编译器不检查这种错误
  • at 成员函数是安全的,当下标越界时, at 成员函数会抛出一个 out_of_range 异常

删除元素

  • 删除元素的操作会改变容器的大小,所以不可以使用于 array
  • forward_list 有特殊版本的 erase
  • forward_list 不支持 pop_back; vector 和 string 不支持 pop_front
  • 删除 deque 中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向 vector 和 string 中删除点之后位置的迭代器、引用和指针都会失效
  • 在删除元素之前,程序员必须确保它(们)是存在的
  • pop_back 和 pop_front 成员函数的返回值是 void,所以需要弹出元素的值,就必须提前在执行弹出操作之前保存它
  • 从容器内部删除一个元素: erase(iterator),返回删除元素之后位置的迭代器
  • 删除多个元素: erase() 和 clear()

改变容器大小

  • c.resize(n, t) : 调整 c 的大小为 n 个元素,所有新添加的元素都初始化为 t
  • 如果所要求调整后的容器大小小于当前容器大小,容器后部的元素会被删除

容器操作可能会使迭代器失效

向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。

由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对 vector、string 和 deque 尤为重要。

在添加/删除 vector、string 或 deque 元素的循环程序,如果是循环中调用的是 insert 或 erase,那么更新迭代器很容易,因为二者调用的返回值是指向更新后元素的迭代器。更新此类容器后,原来的 end 返回的迭代器总是会失效,因此在更新的循环程序中,必须返回调用 end

Vector 的对象增长

vector 和 string 采用连续的内存存储。vector 保留了一定的预留内存,添加元素时减少内存重新分配次数。对于 vector 和 string,其部分实现已经渗透到了接口中。

以下为容器大小管理操作的接口:

  • c.capacity() 不重新分配内存空间的话, c 可以保存多少元素
  • c.shrink_to_fit() 将 capacity() 减少为 size() 相同大小
  • c.reserve(n) 分配至少能容纳 n 个元素的内存空间

shrink_to_fit 只适用于 vector、string 和 deque; capacity 和 reserve 只适用于 vector 和 string

额外的 string 操作

除了顺序容器共同的操作, string 类型还提供了一些额外的操作。这些操作中的大部分要么是提供 string 类和 C 风格字符数组之间的相互转换,要么是增加了允许我们用下标替代迭代器的版本。

  1. 修改 string 的操作

    • s.insert(pos, args) 在 pos 之前插入 args 指定的字符
    • s.erase(pos, len) 删除从 pos 开始的 len 个字符
    • s.assign(args)
    • s.append(args) 将 args 追加到 s
    • s.replace(range, args) 删除 s 中 range 内的字符,替换为 args 指定的字符
  2. string 搜索操作

    • s.find(args) 查找 s 中 args 第一次出现的位置
    • s.rfind(args) 查找 s 中 args 最后一次出现的位置
    • s.find_first_of(args) 在 s 中查找 args 中任何一个字符第一次出现的位置
    • s.find_first_not_of(args) 在 s 中查找第一个不在 args 中的字符
    • s.find_last_of(args) 在 s 中查找 args 中任何一个字符最后一次出现的位置
    • s.find_last_not_of(args)
  3. campare 函数

    与 C 标准库的 strcmp 函数很相似

  4. 数值转换

    • 普通数值数据转换为 string

      std::to_string(val) 一组重载函数

    • string 转数值

      • std::stoi(s, p, b) 返回 s 的起始子串的数值, stoi 返回值是 int。b 表示转换所用的基数。 p 是 size_t 指针,用来保存 s 中第一个非数值字符的下标, p 默认值为 0,即,函数不保存下标。
      • 类似的有 std::stol、std::stoul、std::stoll、std::stoull
      • stof(s, p) 返回 s 的起始子串的数值,返回值为 float, p 的作用同上
      • stod(s, p) 返回 s 的起始子串的数值,返回值为 double, p 的作用同上

顺序容器适配器

除了顺序容器外,标准库还定义了三个顺序容器适配器: stack、 queue 和 priority_queue

本质上,一个适配器是一种机制,能够使某事物的行为看起来像另外一种事物一样。

每个适配器都定义两个构造函数: 默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。可以创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。

参考资料

  1. 《C++ Primer(第五版)》[美] Stanley B.Lippman 著
  2. 《Essential C++ (中文版)》[美] Stanley B.Lippman 著,侯杰译
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值