1. 顺序容器
后文的“前”即首部的方向,“后”为尾部的方向。 forward_list只能递增。从前向后遍历。 允许随机访问的,插入和删除一般不快。插入和删除快的,一般不支持随机访问。deque
forward_list的目的是达到与最好的手写的单向链表数据结构相当的性能,所以不支持size操作,因为保存和计算size会带来开销。
list和forward_list是唯一删除效率高的顺序容器。
没有必需用其他容器的理由,请使用vector。如果需要在中间插入元素且需要随机访问,就比较vector和list的缺点谁带来的损失小。
2. 容器库概览
容器上操作的层次:1. 所有容器类型都提供。 2. 仅针对顺序容器、关联容器、无序容器。 3. 只适用于小部分容器。 所有容器都提供的操作:
容器都定义为模板类。大部分容器需要额外提供元素类型信息。 容器类元素类型可能会影响可以进行的容器操作。如: 这种容器就不能使用只接受一个参数的构造函数版本。
2.1 迭代器
迭代器有公共接口,所有类型的迭代器对某个操作的实现方式都是一样的(前提是提供该操作)。 迭代器范围是指从begin到end的一个左闭右开区间。 迭代器声明:所访问对象名::iterator 迭代器名。
begin和end有多个版本,带r的版本返回反向迭代器,带c的版本返回const迭代器。如下: 当用容器初始化另一个容器时,两个容器的类型和元素类型必须匹配。当传递迭代器参数只拷贝一个范围时,不要求容器类型和容器元素类型相同,只要元素类型可相互转换即可。新容器大小和范围中元素数目相同,新元素用范围中对应元素的值来初始化。 如下: 顺序容器具有和关联容器不同的构造函数:使用值初始化。只有顺序容器才可接受容器大小参数,关联容器不可。值初始化示例如下:
2.1.1 标准库array
元素类型和容器大小是array类型的一部分,不管是在声明还是在使用中都需要带上: 默认构造的array是非空的。包含和大小一样多的默认初始化的元素。 虽然不能对内置数组完成数组拷贝和赋值,但对array没有这个限制,只要求双方类型和大小一致。 array可用列表初始化,但是不可用列表赋值。
2.2 赋值和swap
赋值运算将左边容器的全部元素替换为右边容器元素的拷贝。要求左边和右边的运算对象具有相同的类型。 赋值运算会导致左边容器的内部迭代器、引用和指针失效。swap操作不会,string除外。
assign操作:assign用参数所指定的元素替换左边容器中的所有元素。且允许从一个不同但是相容的类型拷贝。可以说assign是用于补充赋值运算符的。 assign指定元素有两种方式:1. 迭代器范围。 2. 类似值初始化方式,传递数目以及初始值作为参数。
swap操作:交换两个容器的内容。元素本身并未交换,只交换了容器的内部数据结构。 除了array外,swap不对任何元素进行拷贝、删除、插入操作,所以可以保证在常数时间内完成。 swap后,指向容器元素的迭代器、指针和引用仍然指向之前的位置,但是值已经被交换过了。
swap的两个特例,string和array:
swap会真正交换array的元素。交换array的时间和数组长度成正比。 swap两个string对象会导致迭代器、引用和指针失效
2.3 容器大小操作
有三个:size,empty,max_size。 每个容器都支持== 和!= 操作,除了无序容器外的所有容器都支持关系运算符(>,>=,<,<=)。 关系运算符两侧要求容器类型和容器元素类型相同。 容器的比较和string类似。容器的相等运算符实际上是对元素使用== 运算符实现的,容器的其他运算符是基于元素的< 运算符实现的,所以容器要进行该类关系运算,要求元素能进行对应的运算。
3. 顺序容器操作
3.1 插入元素
顺序容器支持的操作: 只有list,forward_list,deque既能往头部插入/删除,也能往尾部插入/删除。 vector,string只能往尾部插入/删除。 array不能用这些东西。
向vector,string的非尾部插入元素会导致元素移动。且无论往哪里插入元素,都导致重新分配对象空间,并将元素从旧空间移动到新空间。 insert主要两部分,第一部分是插入位置,传入一个迭代器指针,后面是插入内容,有各式各样的表达方式。
要注意,当使用迭代器指针来表示范围时,传入的迭代器不能指向添加元素的目标容器。如下:
除了array以外的顺序容器基本都能使用insert。其中forward_list提供了一个特殊版本的insert.
为什么Insert要设置为在迭代器p之前插入? 因为p有可能是尾后迭代器,而且会有在第一个元素之前插入数据的需求,在p之前插入可以保证所有地方都能插入。
insert返回第一个新加元素的迭代器。如果插入范围为空,不插入元素,则会返回第一个参数。示例如下,要理解为什么等价于push_front。 使用insert和push时,需要传入一个创建好的对象,然后将其拷贝到容器内。emplace只需传入参数,然后该成员函数会在容器管理的内存空间内直接构造元素。 第一和第三种方法是不同的,使用push_back不能自动构造新对象,需要显式构造一个临时局部对象然后才能拷贝到容器内。使用emplace会在容器管理的内存空间内直接创建对象。
3.2 访问元素
除了forward_list没有back成员函数外。所有容器包括array都有front和back成员函数。 at和下标运算符区别在于使用at会抛出异常,而不是产生未定义行为。可以通过捕捉异常更好的确定错误的位置。
3.3 删除元素
3.4 forward_list(单向链表)操作
因为单向链表单向访问的特性,想要在某个位置删除或者添加元素,需要获得它的前驱节点。为此,引入了获取容器首元素之前迭代器的成员函数before_begin(),返回一个首前迭代器。 鉴于自身实现特点,forward_list版本的insert和erase都是插入/删除**之后的元素。 实际使用时,需要关注两个位置:要处理的位置和其前驱。示例:
3.5 改变容器大小
array 不支持。
3.6 插入或者删除元素对迭代器的影响
不要保存end处的迭代器。当添加/删除vector,string元素,或者在deque尾部添加/删除元素,或者在deque首部添加元素,都会导致尾后指针end失效。因此添加/删除元素的循环程序必须不断调用end。所以C++的end操作被设计的很快,部分也是这个原因。
4. vector是如何增长的
为了支持连续访问—> vector将元素连续存储—> 空间不足不能简单的随便找块空间分配给容器—> 需要开辟一块新空间,把旧元素和新元素全部复制过去,并释放旧空间—> 每次添加新元素都要完成一次分配和复制,开销太大—> 当空间不足时,vector分配比所需的空间大得多的空间,作为备用,这样不用每次都分配和复制。具体多分配多少随标准库具体实现而定,C++是两倍。
4.1 管理容量的成员函数
容量指的是容器最大能存储的元素数目,不是空余空间数。 reserve函数,只有当当前容量小于所需容量n时才起作用,分配大于等于n的空间。否则,reserve什么也不做,也不会缩减当前容量。 shrink_to_fit请求将空闲空间退还给内存,但只是请求,标准库不保证退还。
5. 额外string操作
5.1 创建string
s可以是string也可以是const char*。 如果参数没有给定长度和起始位置,在使用char初始化时,拷贝操作会遇到空字符才停止,所以char结尾最好是"\0";
5.2 修改string
string提供了下标版本的插入和删除,以下标方式指明插入/删除的位置。 除此以外,string还提供了C风格的insert和assign(用char* 代替string):
5.3 额外的append和replace
string重载函数参数的说明:
5.4 string搜索
搜索操作返回一个string::size_type的值,如果搜索失败,返回一个string::nops的static成员,nops被设置为const string::size_type类型,初始化为-1,由于size_type是unsigned的,所以nops事实上是最大的size_type值。
5.5 compare函数
根据s是大于、等于、还是小于参数指定的字符串,返回整数、0、负数。
5.6 string转数值
6. 容器适配器
适配器是一种机制,使得某个事物的行为看起来像另一种事物。 容器、函数、迭代器都有适配器。 容器适配器接受一种已有容器类型,使他看起来向另一种类型。使用容器适配器后不能使用底层容器的操作,只能用适配器的操作管理内存。 默认情况下,stack和queue是基于deque实现的,priority_queue是基于vector实现的。但是可以在创建适配器时指定底层实现所用的容器: 这种指定也并非毫无限制。如下: stack适配器:要求在尾部插入,弹出,和访问尾首元素。底层可用除了array和forward_list。
queue适配器:要求要求能访问首尾元素,不能基于vector构造,可使用list和deque。 priority_queue适配器:要求随机访问能力。不可基于list构造。和队列queue的区别在于,queue是先进先出的,priority是优先级高的先出,什么时候进不重要,可以理解为常规队列,但是优先级高的能插队。 priority_queue无法访问尾元素。
|