C++ 学习笔记之(9)-顺序容器及适配器
顺序容器就是元素的顺序与其加入容器时的位置相对应,而 关联容器中元素的位置由元素相关联的关键字值决定。所有容器类都共享公共的借口,只是不同的容器会按不同方式对其进行扩展。
顺序容器概述
下表列出标准库中的顺序容器,所有顺序容器都提供了快速顺序访问元素的能力,只是在两方面有不同的性能折中
- 向容器添加或从容器中删除元素的代价
非顺序访问容器中元素的代价
除了固定大小的
array
外,其他容器都提供高效、灵活的内存管理string
和vector
将元素保存在连续的内存空间中,故支持随机访问,但中间位置添加和删除操作非常耗时list
和forward_list
在任何位置添加和删除操作都很快速,但不支持随机访问deque
支持随机访问,但中间位置添加和删除元素代价很高,但两端添加或删除元素很快forward_list
和array
是C++11增加的类型,array
大小固定,不支持添加和删除以及其他改变容器大小的操作。
容器库概览
迭代器
- 迭代器范围(iterator range):由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置,通常被称为
being
和end
范围是左闭合区间[begin, end)
容器类型成员
容器定义了多个类型,如图所示
begin
和end
成员
list<string> a = {"Milton", "Shakespeare", "Austen"};
auto it1 = a.begin(); // list<string>::iterator
auto it2 = a.rbegin(); // list<string>::reverse_iterator
auto it3 = a.cbegin(); // list<string>::const_iterator
auto it4 = a.crbegin(); // list<string>::const_reverse_iterator
容器定义和初始化
每个容器类型都定义了一个默认构造函数,除array
外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数
将一个容器初始化为另一个容器的拷贝
两种方法
直接拷贝整个容器,要求两个容器的类别及其元素类型必须相同
拷贝由一个迭代器对指定的元素范围,
array
除外, 不要求容器类别相同,元素类型也可以不用,但要能互相转换list<string> authors = {"Milton", "Shakespeare", "Austen"}; vector<const char *> articles = {"a", "an", "the"}; list<string> list2(authors); // 正确:类型匹配 deque<string> authList(authors); // 错误:容器类型不匹配 vector<string> words(articles); // 错误:容器类型必须匹配 forward_list<string> words(articles.begin(), articles.end()); // 正确:元素类型可以转换
标准库
array
的大小是类型的一部分,定义array
时,除了指定元素类型,还要指定容器大小array<int, 42>; // 类型为:保存 42 个 int 的数组 array<int, 10>::site_type i; // 数组类型包括元素类型和大小 array<int>::sizt_type j; // 错误: array<int> 不是一个类型
内置数组类型无法进行拷贝或对象赋值操作,但
array
可以
赋值和 swap
下表列出的与赋值相关的运算符可用于所有容器
swap
操作交换两个相同类型容器的内容, 元素本身并为交换, 只是交换了两个容器的内部数据结构,故可在常数时间内完成,且指向容器的迭代器、引用和指针在swap
操作后不会失效swap
两个array
会真正交换他们的元素,所需时间与array
中元素数目成正比,且虽然指针、引用和迭代器绑定的元素保持不变,但元素值已经交换了。
容器大小操作
每个容器类型都有三个大小相关操作
- 成员函数
size
:返回容器中元素的数目, 但forward_list
不支持size
empty
:当size
为0
时,返回true
max_size
:返回一个大于或等于该类型容器所能容纳的最大元素数的值
关系运算符
每个容器类型都支持相等运算符==
和!=
, 除了无序关联容器
外的所有容器都支持关系运算符>、>=、<、<=
。
- 若两个容器大小相同且元素相等,则相等
- 若大小不同,但较小容器每个元素都等于较大容器的对应元素时,较小容器大于较大容器
- 若两个容器都不是另一个容器的前缀子序列,结果取决于第一个不相等的元素的比较结果
容器的关系运算符使用元素的关系运算符完成比较
顺序容器操作
顺序容器和关联容器的不同之处在于组织元素的方式,所以导致元素的存储、访问、添加和删除的不同。下面介绍顺序容器特有的操作
向顺序容器中添加元素
对于
vector
和string
的尾部之外的位置添加元素,都需要移动元素,可能还会导致整个对象存储空间的重新分配。重新分配对象的存储空间需要分配新的内存,并将元素从旧空间移动到新空间中对于
deque
首尾之外的任何位置添加元素,都要移动元素,当用对象初始化容器或插入到容器中时,实际上放入到容器中的是对象值的拷贝,并不是对象本身。
记住,
insert
插入在元素之前,并且返回指向新插入元素的迭代器emplace
操作将参数传递给元素类型的构造函数,直接在容器管理的内存空间中构造函数。而push
或insert
操作是传递元素类型对象,然后拷贝到容器中// 在 c 的末尾构造一个 Sales_data 对象,使用 Sales_data 的三个参数的构造函数 c.emplace_back("978-0590353403", 25, 15.99); //错误:没有接受三个参数的 push_back 版本 c.push_back("978-0590353403", 25, 15.99); // 正确:创建一个临时的 Sales_data 对象传递给 push_back c.push_back(Sales_data("978-0590353403", 25, 15.99));
访问元素
- 访问成员函数(即
front
、back
、下标和at
)返回的都是引用。若容器是const
对象,则返回值是const
引用 - 下标运算符不检查下标是否合法,若想确保下标合法,可使用
at
成员函数,越界会抛出out_of_range
异常
删除元素
特殊的forward_list
操作
由于在forward_list
中,添加或删除元素都会改变后继元素,所以添加和删除的操作都是通过改变给定元素之后的元素完成的。
改变容器大小
如图所示,可使用resize
增大或缩小容器。array
不支持
容器操作可能是迭代器失效
对容器进行添加或删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效
添加元素
vector
或string
- 重新分配存储空间:迭代器、指针和引用都会失效
- 未重新分配存储空间:指向插入位置之前的元素的迭代器、指针和引用仍有效,指向之后的将失效
deque
- 插入到除首尾位置之外的任何位置:都会导致迭代器、指针和引用失效
- 若在首尾位置添加元素:迭代器失效,但指针和引用有效
list
和forward_list
- 指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效
删除元素(指向被删除元素的迭代器、指针和引用都会失效)
vector
和string
- 指向被删除元素之前的迭代器、引用和指针:仍有效
deque
- 若在首尾之外的任何位置删除元素:都会失效
- 若删除尾元素:尾后迭代器也会失效,其他不受影响
- 删除首元素:都有效
list
和forward_list
- 都有效
vector
对象是如何增长的
vector
为了支持快速随机访问,故将元素连续存储。当空间不足时,就会重新分配空间,将元素从旧空间移动到新空间,然后添加新元素,释放旧存储空间。 vector
和string
通常会分配比新空间需求更大的内存空间,预留些空间做备用,可以减少容器空间重新分配的次数
* vector
的实现策略是每次分配新内存空间时将当前容量翻倍
* shrink_to_fit
请求vector
将超出当前大小的多余内存退回给系统,但标准库并不保证
额外的string
操作
构造string
的其他方法
C++ 学习笔记之(3)-字符串、向量和数组 介绍过string
的构造函数,以及与其他顺序容器相同的构造函数外,string
还有其他三个构造函数,如表所示
- 若用
const char *
创建string
时,指针执行的数组必须以空字符结尾,拷贝操作遇到空字符时停止
substr
操作
改变string
的其他方法
replace
草市是调用erase
和insert
的一种简写形式
string
搜索操作
string::npos
:static
成员,类型为const string::size_type
类型,初始值为-1
,无符号类型
compare
函数
除了关系运算符外,string
类型还提供了一组compare
函数,与C
标准库的strcmp
函数很相似
数值转换
新标准引入了多个函数,用来实现数值数据与标准库string
之间的转换。
转换为数值的
string
中第一个非空白字符必须是数值中可能出现的字符string s2 = "pi = 3.14"; // 转换 s2 中以数字开始的第一个子串,结果 d = 3.14 d = stod(s2.substr(s2.find_first_of("+-.0123456789")));
若
string
不能转换为一个数值,则函数抛出invalid_argument
异常。若转换得到的数值无法用任何类型表示,则抛出out_of_range
异常
容器适配器
除顺序容器外,标准库还定义了三个容器适配器:stack
, queue
和priority_queue
。
- 适配器(adaptor):标准库的通用概念。容器、迭代器和函数都有适配器,其本质上是一种机制,可以是某种事物的行为表现成另一种事物。
默认情况下,
stack
和queue
基于deque
实现的,priority_queue
是在vector
之上实现的所有适配器都要求容器具有添加和删除元素的能力,故适配器不能构造在
array
之上
栈适配器
stack
只要求push_back
、pop_back
和back
操作,故可使用除array
和forward_list
之外的任何容器构造
队列适配器
queue
适配器要求back
、push_back
、front
和push_front
, 故可构造于list
或queue
之上,但不能基于vector
构造priority_queue
除了front
、push_back
和pop_back
操作外,还要求随机访问能力, 故可构造于vector
和deque
之上,但不能基于list
构造
结语
- 标准库容器是模板类型,用来保存给定类型的对象。顺序容器中元素是顺序存放的,通过位置来访问
- 所有容器(除
array
外)都提供高效的动态内存管理 - 在容器中执行添加和删除操作后,要注意这些操作可能是其迭代器、引用和指针失效