顺序容器
顺序容器(sequential container) 为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。
顺序容器类型:
除了固定大小的 array
外,其他容器都提供高效、灵活的内存管理。例如,
**string 和 vector 将元素保存在连续的内存空间中。**由于元素是连续存储的,由元素的下标来计算其地址是非常快速的。但是,在这两种容器的中间位置添加或删除元素就会非常耗时:在一次插入或删除操作后,需要移动插入/删除位置之后的所有元素,来保持连续存储。而且添加一个元素有时可能还需要分配额外的存储空间。在这种情况下,每个元素都必须移动到新的存储空间中。
**list 和 forward_list 两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。**作为代价,这两个容器不支持元素的随机访问:为了访问一个元素,我们只能遍历整个容器。而且与 vector 、deque 和array 相比, 这两个容器的额外内存开销也很大。
**deque 是一个更为复杂的数据结构。**与 string 和 vector 类似,deque 支持快速的随机访问。与 strìng 和 vector 一样,在 deque 的中间位置添加或删除元素的代价(可能)很高。但是,在 deque 的两端添加或删除元素都是很快的,与 list 或forward_list 添加删除元素的速度相当。
**forward_list 和 array 是C++11新增类型。**与内置数组相比,array 更安全易用。array 对象的大小是固定的,forward_list 没有 size 操作。
顺序容器的定义
首先得包含相关的头文件,然后指定容器中存放元素的类型,因为容器均为模板类型:
#include <list>
list<int> ilist; //这就使用默认构造函数定义了一个空的list容器,可以容纳int型元素
-
容器元素的初始化
除了默认构造函数的其他构造函数:
C c; 创建一个名为c的空容器,C为容器类型名,T是元素类型。适用于所有容器 C c(c2); 创建c2的副本;适用于所有容器 C c(b,e); 创建c,其元素是迭代器b和e标示的范围内元素的副本;适用于所有容器 C c(n,t); 有n个值为t的元素创建容器;只适用于顺序容器 C c(n); 创建有n个值初始化元素的容器;只适用于顺序容器 注意:
将一个容器复制给另一个容器时,类型必须匹配:容器类型和元素类型都必须相同; 接受容器大小做形参的构造函数只适用于顺序容器,而关联容器不支持这种初始化;
定义和使用
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); // 错误:容器类型必须匹配 // 正确:可以将const char*元素转换为string forward_list<string> words(articles.begin(), articles.end()); array<int, 42> // 类型为:保存42个int的数组 array<string, 10> // 类型为:保存10个string的数组 array<int, 10>::size_type i; // 数组类型包括元素类型和大小 array<int>::size_type j; // 错误:array<int>不是一个类型 //可以对 array 进行拷贝或赋值操作,但要求二者的元素类型和大小都相同。
-
容器内元素的类型约束
- 元素类型必须支持赋值运算
- 元素类型的对象必须可以复制
迭代器和迭代器范围
-
一个 迭代器范围(iterator range) 由一对迭代器表示。这两个迭代器通常被称为 begin 和 end,分别指向同一个容器中的元素或尾后地址。end 迭代器不会指向范围中的最后一个元素,而是指向尾元素之后的位置。这种元素范围被称为 左闭合区间(left-inclusive interval),其标准数学描述为 [begin,end)。迭代器 begin 和 end 必须指向相同的容器,end 可以与 begin 指向相同的位置,但不能指向 begin 之前的位置(由程序员确保)。
-
假定
begin
和end
构成一个合法的迭代器范围,则:-
如果
begin
等于end
,则范围为空。 -
如果
begin
不等于end
,则范围内至少包含一个元素,且begin
指向该范围内的第一个元素。 -
可以递增
begin
若干次,令begin
等于end
。while (begin != end) { *begin = val; // 正确:范围非空,因此begin指向一个元素 ++begin; // 移动迭代器,获取下一个元素 }
-
顺序容器的操作
4种操作:
- 在容器中添加元素
- 在容器中删除元素
- 设置容器的大小
- (如果有的话)获取容器内的第一个和最后一个元素
-
容器类型别名——编写泛型程序的时候非常有用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7WPSzmBF-1594007215054)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200706104317175.png)]
-
begin 和 end 成员
begin 和 end 操作生成指向容器中第一个元素和尾后地址的迭代器。其常见用途是形成一个包含容器中所有元素的迭代器范围。
begin 和 end 操作有多个版本:带 r 的版本返回反向迭代器。以 c 开头的版本(C++11新增)返回 const 迭代器。不以 c 开头的版本都是重载的,当对非常量对象调用这些成员时,返回普通迭代器,对 const 对象调用时,返回 const 迭代器。
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
以 c 开头的版本是C++新标准引入的,用以支持 auto 与 begin 和 end 函数结合使用。
当 auto 与 begin 或 end 结合使用时,返回的迭代器类型依赖于容器类型。但调用以 c 开头的版本仍然可以获得 const 迭代器,与容器是否是常量无关。
当程序不需要访问时,应该使用 cbegin 和 cend
。 -
向顺序容器添加元素
除
array
外,所有标准库容器都提供灵活的内存管理,在运行时可以动态添加或删除元素。关键概念:容器元素都是副本
在容器中添加元素时,系统是将元素值复制到容器里。类似地,使用一段元素初始化新容器时,新容器存放的是原始元素的副本。被复制的原始值与新容器中的元素各不相关,此后,容器内元素值发生变化时,被复制的原值不会受到影响
-
容器大小的操作
size
成员返回容器中元素的数量;empty
当size
为0时返回true
,否则返回false
;max_size
返回一个大于或等于该类型容器所能容纳的最大元素数量的值。
forward_list
支持max_size
和empty
,但不支持size
。resize
函数接受一个可选的元素值参数,用来初始化添加到容器中的元素,否则新元素进行值初始化。如果容器保存的是类类型元素,且resize
向容器添加新元素,则必须提供初始值,或元素类型提供默认构造函数。 -
访问元素
每个顺序容器都有一个 front 成员函数,而除了 forward_list 之外的顺序容器还有一个 back 成员函数。这两个操作分别返回首元素和尾元素的引用。
- 迭代器 end 指向的是容器尾元素之后的(不存在的)元素。为了获取尾元素,必须首先递减此迭代器;
- 在调用front和back之前(或解引用 begin 和 end 返回的迭代器)之前,要确保容器非空。
顺序容器的元素访问操作:
在容器中访问元素的成员函数都返回引用类型。如果容器是
const
对象,则返回 const 引用,否则返回普通引用;可以快速随机访问的容器(string、vector、deque 和 array)都提供下标运算符。保证下标有效是程序员的责任;如果希望确保下标合法,可以使用at
成员函数。at 类似下标运算,但如果下标越界,at 会抛出 out_of_range 异常。vector<string> svec; // 空vector cout << svec[0]; // 运行时错误:svec中没有元素! cout << svec.at(0); // 抛出一个out_of_range异常
-
删除元素
注意:
删除元素的成员函数并不检查其参数。删除元素前,程序员必须确保目标元素存在。pop_front 和 pop_back 函数分别删除首元素和尾元素。vector 和 string 类型不支持 pop_front,forward_list 类型不支持 pop_back。
while (!ilist.empty()){c process(ilist.front()); // 对ilist的首元素进行一些处理 ilist.pop_front(); // 完成处理后删除首元素
erase 函数删除指定位置的元素。可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的 erase 都返回指向删除元素(最后一个)之后位置的迭代器。
// 删除两个迭代器表示的范围内的元素 // 返回指向最后一个被删元素之后位置的迭代器 elem1 = slist.erase(elem1, elem2); // 调用后,elem1 == e1em2
clear 函数删除容器内的所有元素:
slist.clear(); // 删除容器中所有元素 slist.erase(slist.begin(), slist.end()); // 等价调用
-
赋值与swap
赋值运算符两侧的运算对象必须类型相同。assign 允许用不同但相容的类型赋值,或者用容器的子序列赋值。
list<string> names; vector<const char*> oldstyle; names = oldstyle; // 错误: 容器类型不匹配 // 正确:可以将const char*转换为string names.assign(oldstyle.cbegin(), oldstyle.cend());
由于其旧元素被替换,因此传递给 assign 的迭代器不能指向调用 assign 的容器本身。
swap 交换两个相同类型容器的内容。
除 array 外,swap 不对任何元素进行拷贝、删除或插入操作,只交换两个容器的内部数据结构,因此可以保证快速完成。
vector<string> svec1(10); // 10个元素的vector vector<string> svec2(24); // 24个元素的vector swap(svec1, svec2);
对于 array,swap 会真正交换它们的元素。因此在 swap 操作后,指针、引用和迭代器所绑定的元素不变,但元素值已经被交换。
对于其他容器类型(除 string),指针、引用和迭代器在 swap 操作后仍指向操作前的元素,但这些元素已经属于不同的容器了。
新标准库同时提供了成员和非成员函数版本的 swap。非成员版本的 swap 在泛型编程中非常重要,建议统一使用非成员版本的 swap。
vector容器的自增长
vector
和 string
的实现通常会分配比新空间需求更大的内存空间,容器预留这些空间作为备用,可用来保存更多新元素。
容器大小管理操作:
reserve 并不改变容器中元素的数量,它仅影响 vector 预先分配多大的内存空间。
capacity
函数返回容器在不扩充内存空间的情况下最多可以容纳的元素数量。reserve
函数告知容器应该准备保存多少元素,它并不改变容器中元素的数量,仅影响容器预先分配的内存空间大小。
只有当需要的内存空间超过当前容量时,reserve 才会真正改变容器容量,分配不小于需求大小的内存空间。当需求大小小于当前容量时,reserve 并不会退回内存空间。因此在调用 reserve 之后,capacity 会大于或等于传递给 reserve 的参数。
在C++11中可以使用 shrink_to_fit
函数来要求 deque、vector 和 string 退回不需要的内存空间(并不保证退回)。
每个 vector 实现都可以选择自己的内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间。
容器的选用
容器选择原则:
- 除非有合适的理由选择其他容器,否则应该使用
vector
。 - 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用
list
或forward_list
。 - 如果程序要求随机访问容器元素,则应该使用
vector
或deque
。 - 如果程序需要在容器头尾位置插入/删除元素,但不会在中间位置操作,则应该使用
deque
。 - 如果程序只有在读取输入时才需要在容器中间位置插入元素,之后需要随机访问元素。则:
- 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向
vector
追加数据,再调用标准库的sort
函数重排元素,从而避免在中间位置添加元素。 - 如果必须在中间位置插入元素,可以在输入阶段使用
list
。输入完成后将 list 中的内容拷贝到vector
中
- 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向
- 不确定应该使用哪种容器时,可以先只使用
vector
和list
的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择vector
或list
都很方便。
再谈string类型
-
构造string对象的其他方法
从另一个
string
对象拷贝字符构造string
时,如果提供的拷贝开始位置(可选)大于给定string
的大小,则构造函数会抛出out_of_range
异常。子字符串操作:
如果传递给
substr
函数的开始位置超过string
的大小,则函数会抛出out_of_range
异常。 -
修改string对象的其他方法
append
函数是在string
末尾进行插入操作的简写形式:string s("C++ Primer"), s2 = s; // 将s和s2初始化为"C++ Primer" s.insert(s.size(), " 4th Ed."); // s == "C++ Primer 4th Ed." s2.append(" 4th Ed."); // 等价方法:将" 4th Ed."追加到s2; s == s2
replace
函数是调用erase
和insert
函数的简写形式:// 将"4th"替换为"5th"的等价方法 s.erase(11, 3); // s == "C++ Primer Ed." s.insert(11, "5th"); // s == "C++ Primer 5th Ed." // 从位置11开始,删除3个字符并插入"5th" s2.replace(11, 3, "5th"); // 等价方法: s == s2
-
string类型的查找操作
string
的每个搜索操作都返回一个string::size_type
值,表示匹配位置的下标。如果搜索失败,则返回一个名为string::npos
的static
成员。标准库将npos
定义为const string::size_type
类型,并初始化为-1;不建议用
int
或其他带符号类型来保存string
搜索函数的返回值。 -
string对象的比较
compare函数:
-
数值转换
进行数值转换时,
string
参数的第一个非空白字符必须是符号(+
或-
)或数字。它可以以0x
或0X
开头来表示十六进制数。对于转换目标是浮点值的函数,string
参数也可以以小数点开头,并可以包含e
或E
来表示指数部分。如果给定的
string
不能转换为一个数值,则转换函数会抛出invalid_argument
异常。如果转换得到的数值无法用任何类型表示,则抛出out_of_range
异常。
容器适配器
标准库定义了 stack
、queue
和 priority_queue
三种容器适配器。容器适配器可以改变已有容器的工作机制。
这些适配器支持的操作和类型:
默认情况下,stack
和 queue
是基于 deque
实现的,priority_queue
是基于 vector
实现的。可以在创建适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
// 在vector上实现的空栈
stack<string, vector<string>> str_stk;
// strstk2在vector上实现,初始化时保存svec的拷贝
stack<string, vector<string>> str_stk2(svec);
所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在 array
上。适配器还要求容器具有添加、删除和访问尾元素的能力,因此也不能用 forward_list
构造适配器。
栈适配器stack
定义在头文件stack中,其支持的操作如下:
队列适配器queue
和priority_queue
定义在头文件queue中,其支持的操作如下:
queue
使用先进先出(first-in,first-out,FIFO)的存储和访问策略。进入队列的对象被放置到队尾,而离开队列的对象则从队首删除。