第九章 顺序容器(重点)

9. 顺序容器(重点)

元素在顺序容器中的顺序与其加入容器时的位置相对应,关联容器中元素的位置由元素相关联的关键字值决定。所有容器类都共享公共的接口,不同容器按不同方式对其进行扩展。每种容器都提供了不同的性能和功能的权衡。顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。

9.1 顺序容器概述

  • 所有顺序容器都提供了快速顺序访问元素的能力,有 vector、deque、list、forward_list、array、string 。现代C++程序应该使用标准库容器,而不是更原始的数据结构。

  • 通常,使用 vector 是最好的选择,除非你有很好的理由选择其他容器。一般来说,应用中占主导地位的操作(执行的访问操作更多还是插入/删除更多)决定了容器类型的选择。

9.2 容器库概览

  • 一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。容器均定义为模板类。顺序容器几乎可以保存任意类型的元素,但是某些容器操作对元素类型有其自己的特殊要求。

  • 迭代器范围的概念是标准库的基础。迭代器 begin 和 end 必须指向相同的容器([ begin, end ))。

  • 通过类型别名,我们可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的 value_type 。如果需要元素类型的一个引用,可以使用 reference 或 const_reference 。

  • 当不需要写访问时,应使用 cbegin 和 cend 。不以 c 开头的函数都是重载过的。例如 begin 则是被重载过的,有两个版本:其中一个是 const 成员函数,也返回 const 迭代器;另一个则返回普通迭代器,可以对容器元素进行修改。

  • 每个容器类型都定义了一个默认构造函数。为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。

  • 如果元素类型没有默认构造函数,除了大小参数外,还必须指定一个显式的元素初始值。只有顺序容器的构造函数才接受大小参数,关联容器并不支持。

  • 当定义一个 array 时,除了指定元素类型,还要指定容器大小。数组类型包括元素类型和大小。虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但是 array 并无此限制。

  • 赋值运算符将其左边容器中的全部元素替换为右边容器中元素的拷贝。由于右边运算对象的大小可能与左边运算对象的大小不同,因此 array 类型不支持 assign 。与不允许用花括号包围的值列表进行赋值。

  • swap 通常比从 c2 向 c1 拷贝元素快得多。赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而 swap 操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效。

  • assign 操作用参数所指定的元素(的拷贝)替换左边容器中的所有元素。swap 操作交换两个相同类型容器的内容。与其他容器不同,对一个 string 调用 swap 会导致迭代器、引用和指针失效。

  • 成员函数 size 返回容器中元素的数目;empty 当 size 返回容器中元素的数目;empty 当 size 为 0 时返回布尔值 true,否则返回 false;max_size 返回一个大于或等于该类型容器所能容纳的最大元素的值。

  • 每个容器类型都支持相等运算符(== 和 !=):除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。

  • 只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。

9.3 顺序容器操作

  • 顺序容器和关联容器的不同之处在于两者组织元素的方式。这些不同之处直接关系到元素如何存储、访问、添加以及删除。当我们使用相关操作时,必须记得不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能。

  • 当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。容器中的元素与提供值的对象之间没有任何关联。

  • 向一个 vector、string 或 deque 插入元素会使所有指向容器的迭代器、引用和指针失效。除了 array 和 forward_list 之外,每个顺序容器(包括 string 类型)都支持 push_back。除了 push_back,list、forward_list 和 deque 容器还支持名为 push_front 的类似操作。deque 保证在容器首尾进行插入和删除元素的操作都只花费常数时间。与 vector 一样,在 deque 首尾之外的位置插入元素会很耗时。将元素插入到 vector、deuqe 和 string 中的任何位置都是合法的。然而这样做可能很耗时。

  • 迭代器表示要拷贝的范围,不能指向与目的位置相同的容器。emplace 成员使用这些参数在容器管理的内存空间中直接构造元素。传递给 emplace 函数的参数必须与元素类型的构造函数相匹配。

  • 不同容器在不同位置添加元素的性能是有差异的。对 deque 来说,在首尾位置添加新元素性能最佳,在中间位置插入新元素性能会很差。对遍历操作,可高效完成。对 list 来说,在任何位置添加新元素都有很好的性能,遍历操作也能高效完成。容器插入操作的副作用-向一个 vector、string 或 deque 插入元素会使现有指向容器的迭代器、引用和指针失效。

  • 如果容器中没有元素,访问操作的结果是未定义的。at 和下标操作只适用于 string、vector、dequearray 。back 不适用于 forward_list 。对一个空容器调用 front 和 back,就像使用一个越界的下标一样,是一种严重的程序设计错误。

  • 给定下标必须 “ 在范围内 ” (即,大于等于 0,且小于容器的大小)。保证下标有效是程序员的责任,下标运算符并不检查下标是否在合法范围内。使用越界的下标是一种严重的程序设计错误,而且编译器并不检查这种错误。at 成员函数类似下标运算符,但如果下标越界,at 会抛出一个 out_of_range 异常。

  • forward_list 有特殊版本的 erase。forward_list 不支持 pop_back;vector 和 string 不支持 pop_front 。删除 deque 中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向 vector 或 string 中删除点之后位置的迭代器、引用和指针都会失效。

  • 当在 forward_list 中添加或删除元素时,我们必须关注两个迭代器-一个指向我们要处理的元素,另一个指向其前驱。

  • 如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部。如果 resize 缩小容器,则指向被删除元素的迭代器、引用和指针都会失效;对 vector、string 或 deque 进行 resize 可能导致迭代器、指针和引用失效。

  • resize 操作接受一个可选的元素值参数,用来初始化添加到容器中的元素。如果调用者未提供此参数,新元素进行值初始化。如果容器保存的是类类型元素,且 resize 向容器添加新元素,则我们必须提供初始值,或者元素类型必须提供一个默认构造函数。

  • 向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针一样的问题。

  • 添加/删除 vector、string 或 deque 元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用或指针。

  • 当我们添加/删除 vector 或 string 的元素后,或在 deque 中首元素之外任何位置添加/删除元素后,原来 end 返回的迭代器总是会失效。因此,添加或删除元素的循环程序必须反复调用 end,而不能在循环之前保存 end 返回的迭代器,一直当作容器末尾使用。

  • list 和 forward_list 与其他容器的一个不同是,迭代器不支持加减运算,究其原因,链表中元素并非在内存中连续存储,因此无法通过地址的加减再元素间远距离移动。因此,应多次调用++来实现与迭代器加法相同的效果。对于 forward_list,由于是单向链表结构,删除元素时,需将前驱指针调整为指向下一个节点,因此需维护 “ 前驱 ”、“ 后继 ” 两个迭代器。向 vector 中插入新元素后,原有迭代器都会失效。

9.4 vector 对象是如何增长的

  • 为了支持快速随机访问,vector 将元素连续存储-每一个元素紧挨着前一个元素存储。

  • shrink_to_fit 只适用于 vector、string、deque。capacity 和 reserve 只适用于 vector 和 string。reserve 并不改变容器中元素的数量,它仅影响 vector 预先分配多大的内存空间。容器的 size 是指它已经保存的元素的数目;而 capacity 则是在不分配新的内存空间的前提下它最多可以保存多少元素。

  • capacity 至少于 size 一样大,具体会分配多少额外空间则视标准库具体实现而定。vector 的实现采用的策略似乎是在每次需要分配新内存空间时将当前容量翻倍。调用 shrink_to_fit 只是一个请求,标准库并不保证退还内存。

  • 每个 vector 实现都可以选择自己的内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间。

  • 理解 list 和 array 与 vector 在数据结构上的差异导致内存分配方式的不同。list 是链表,当有新元素加入时,会从内存空间中分配一个新节点保存它;当从链表中删除元素时,该节点占用的内存空间会被立刻释放。因此,一个链表占用的内存空间总是与它当前保存的元素所需空间相等。(换句话说,capacity 总是等于 size)。而 array 是固定大小数组,内存一次性分配,大小不变,不会变化。

  • 调用 reserve 永远也不会减少容器占用的内存空间。类似的,resize 成员函数只改变容器中元素的数目,而不是容器的容量。我们同样不能使用 resize 来减少容器预留的内存空间。

9.5 额外的 string 操作

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

    const char *cp = "Hello World!!!"; //以空字符结束的数组

    char nuNull[ ] = { 'H', 'i' }; //不是以空字符结束

  • 通常当我们从一个 const char* 创建 string 时,指针指向的数组必须以空字符结尾,拷贝操作遇到空字符时停止。如果我们还传递给构造函数一个计数值,数组就不必以空字符结尾。如果我们未传递计数值且数组也未以空字符结尾,或者给定计数值大于数组大小,则构造函数的行为是未定义的。

  • vector 有提供 data 成员函数,返回其内存空间的首地址。

  • 除了接受迭代器的 insert 和 erase 版本外,string 还提供了接受下标的版本。下标指出了开始删除的位置,或是 insert 到给定值之前的位置。标准库 string 类型还提供了接受 C 风格字符数组的 insert 和 assign 版本。

  • append 操作是在 string 末尾进行插入操作的一种简写形式。replace 操作是调用 erase 和 insert 的一种简写形式。

  • assign 和 append 函数无须指定要替换 string 中哪个部分:assign 总是替换 string 中的所有内容,append 总是将新字符追加到 string 末尾。replace 函数提供了两种指定删除元素范围的方式。这两种情况下,新元素都会插入到给定下标(或迭代器)之前的位置。并不是每个函数都支持所有形式的参数,例如 insert 就不支持下标和初始化列表参数。

  • gcc 4.8中,返回迭代器的 insert 只支持单个字符插入。

  • 除非你确定目标程序会有效率上的问题,否则应尽量使用标准库功能来编写你的程序,而非自己从头编写代码。

  • 每个搜索操作都返回一个 string::size_type 值,表示匹配发生位置的下标。如果搜索失败,则返回一个名为 string :: npos 的 static 成员。标准库将 npos 定义为一个 const string :: size_type 类型,并初始化为值 -1。搜索(以及其他 string 操作)是大小写敏感的。

  • 字符串中常常包含表示数值的字符。一般情况,一个数的字符表示不同于其数值。string 参数中第一个非空白符必须是符号(+或-)或数字。对那些将字符串转换为浮点值的函数,string 参数也可以以小数点(.)开头,并可以包含 e 或 E 来表示指数部分。

  • 如果 string 不能转换为一个数值,这些函数抛出一个 invalid argument 异常。如果转换得到的数值无法用任何类型来表示,则抛出一个 out_of_range。

9.6 容器适配器

  • 除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue 和 priority_queue。适配器是标准库中的一个通用概念。容器、迭代器和函数都有适配器。本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。

  • 每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。

  • 默认情况下,stack 和 queue 是基于 deque 实现的,priority_queue 是在 vector 之上实现的。

  • 所有适配器都要求容器具有添加和删除元素的能力。因此,适配器不能构造在 array 之上。类似的,我们也不能用 forward_list 来构造适配器,因为所有适配器都要求容器具有添加、删除以及访问尾元素的能力。stack 只要求 push_back、pop_back 和 back 操作,因此可以使用除 array 和 forward_list 之外的任何容器类型来构造 stack。queue 适配器要求 back、push_back、front 和 push_front,因此它可以构造于 list 或 deque 之上,但不能基于 vector 构造。priority_queue 除了 front、push_back 和 pop_back 操作之外还要求随机访问能力,因此它可以构造于 vector 或 deque 之上,但不能基于 list 构造。

  • 每个容器适配器都基于底层容器类型的操作定义了自己的特殊操作。我们只可以使用适配器操作,而不能使用底层容器类型的操作。

待完善…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值