顺序容器
一些前言
清明,教会我们在爱中告别
网上有个问题:“祭祖扫墓,真会得到祖先的庇佑吗?”有人回答说:“祭祖,相信的并不是鬼神,而是相信亲人对我们的爱是不会消失的,他们在我们心里留下的回忆也不会消失。” 死亡,只是改变了生命的状态,并未结束我们与亲人的联系。在这场年复一年的仪式里,最好的缅怀是记得,也是放下。那些有关生死的话语,教会我们在爱中学会告别。
顺序容器
顺序容器概述(Overview of the Sequential Containers)
类型 | 特性 |
---|---|
vector |
可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢 |
deque |
双端队列。支持快速随机访问。在头尾位置插入或删除速度很快 |
list |
双向链表。只支持双向顺序访问。在任何位置插入或删除速度都很快 |
forward_list |
单向链表。只支持单向顺序访问。在任何位置插入或删除速度都很快 |
array |
固定大小数组。支持快速随机访问。不能添加或删除元素 |
string |
类似vector ,但用于保存字符。支持快速随机访问。在尾部插入或删除速度很快 |
forward_list
和array
是C++11新增类型。与内置数组相比,array
更安全易用。forward_list
没有size
操作。
容器选择原则:
- 除非有合适的理由选择其他容器,否则应该使用
vector
。 - 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用
list
或forward_list
。 - 如果程序要求随机访问容器元素,则应该使用
vector
或deque
。 - 如果程序需要在容器头尾位置插入/删除元素,但不会在中间位置操作,则应该使用
deque
。 - 如果程序只有在读取输入时才需要在容器中间位置插入元素,之后需要随机访问元素。则:
- 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向
vector
追加数据,再调用标准库的sort
函数重排元素,从而避免在中间位置添加元素。 - 如果必须在中间位置插入元素,可以在输入阶段使用
list
。输入完成后将list
中的内容拷贝到vector
中。
- 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向
- 不确定应该使用哪种容器时,可以先只使用
vector
和list
的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择vector
或list
都很方便。
容器库概览(Container Library Overview)
每个容器都定义在一个头文件中,文件名与类型名相同。容器均为模板类型。
迭代器(Iterators)
forward_list
类型不支持递减运算符--
。
一个迭代器范围(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
。
1 | while (begin != end) |
容器类型成员(Container Type Members)
通过类型别名,可以在不了解容器元素类型的情况下使用元素。如果需要元素类型,可以使用容器的value_type
。如果需要元素类型的引用,可以使用reference
或const_reference
。
begin
和end
成员(begin
and end
Members)
begin
和end
操作生成指向容器中第一个元素和尾后地址的迭代器。其常见用途是形成一个包含容器中所有元素的迭代器范围。
begin
和end
操作有多个版本:带r
的版本返回反向迭代器。以c
开头的版本(C++11新增)返回const
迭代器。不以c
开头的版本都是重载的,当对非常量对象调用这些成员时,返回普通迭代器,对const
对象调用时,返回const
迭代器。
1 | list<string> a = {"Milton", "Shakespeare", "Austen"}; |
当auto
与begin
或end
结合使用时,返回的迭代器类型依赖于容器类型。但调用以c
开头的版本仍然可以获得const
迭代器,与容器是否是常量无关。
当程序不需要写操作时,应该使用cbegin
和cend
。
容器定义和初始化(Defining and Initializing a Container)
将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。
传递迭代器参数来拷贝一个范围时,不要求容器类型相同,而且新容器和原容器中的元素类型也可以不同,但是要能进行类型转换。
1 | // each container has three elements, initialized from the given initializers |
C++11允许对容器进行列表初始化。
1 | // each container has three elements, initialized from the given initializers |
定义和使用array
类型时,需要同时指定元素类型和容器大小。
1 | array<int, 42> // type is: array that holds 42 ints |
对array
进行列表初始化时,初始值的数量不能大于array
的大小。如果初始值的数量小于array
的大小,则只初始化靠前的元素,剩余元素会被值初始化。如果元素类型是类类型,则该类需要一个默认构造函数。
可以对array
进行拷贝或赋值操作,但要求二者的元素类型和大小都相同。
赋值和swap
(Assignment
and swap
)
赋值运算符两侧的运算对象必须类型相同。assign
允许用不同但相容的类型赋值,或者用容器的子序列赋值。
1 | list<string> names; |
由于其旧元素被替换,因此传递给assign
的迭代器不能指向调用assign
的容器本身。
swap
交换两个相同类型容器的内容。除array
外,swap
不对任何元素进行拷贝、删除或插入操作,只交换两个容器的内部数据结构,因此可以保证快速完成。
1 | vector<string> svec1(10); // vector with ten elements |
赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap
操作交换容器内容,不会导致迭代器、引用和指针失效(array
和string
除外)。
对于array
,swap
会真正交换它们的元素。因此在swap
操作后,指针、引用和迭代器所绑定的元素不变,但元素值已经被交换。
1 | array<int, 3> a = { 1, 2, 3 }; |
对于其他容器类型(除string
),指针、引用和迭代器在swap
操作后仍指向操作前的元素,但这些元素已经属于不同的容器了。
1 | vector<int> a = { 1, 2, 3 }; |
在这段代码中,即使在执行完 a.swap(b)
之后,p
和 q
仍然保留了交换前的值,因为它们是在 a
被交换前就已经初始化的,它们所指向的仍然是 a
的原始内存。
因此,输出仍然是原始的 a
的值:
1 | 1 |
array
不支持assign
,也不允许用花括号列表进行赋值。
1 | array<int, 10> a1 = {0,1,2,3,4,5,6,7,8,9}; |
这段代码涉及了
std::array
的初始化和赋值操作。
array<int, 10> a1 = {0,1,2,3,4,5,6,7,8,9};
- 这行代码创建了一个名为
a1
的std::array
,它包含了 10 个整数,初始化为 0 到 9。array<int, 10> a2 = {0};
- 这行代码创建了另一个名为
a2
的std::array
,它包含了 10 个整数,所有元素的值都被初始化为 0。注意,只提供了一个 0,但是因为std::array
在初始化时会使用剩余的元素自动填充默认值,所以所有的元素都被初始化为 0。a1 = a2;
- 这行代码将
a2
的值赋给了a1
,即用a2
中的元素替换了a1
中的元素。因为a1
和a2
都是相同类型和大小的std::array
,所以可以直接进行赋值操作。a2 = {0};
- 这行代码尝试将一个大括号初始化列表赋给
a2
,但是这种赋值方式是不合法的。原因是对于std::array
,不能直接将大括号初始化列表赋值给它,必须通过逐个元素赋值或者通过另一个同类型的std::array
进行赋值。因此,这行代码会导致编译错误。
容器大小操作(Container Size Operations)
size
成员返回容器中元素的数量;empty
当size
为0时返回true
,否则返回false
;max_size
返回一个大于或等于该类型容器所能容纳的最大元素数量的值。forward_list
支持max_size
和empty
,但不支持size
。
关系运算符(Relational Operators)
每个容器类型都支持相等运算符(==
、!=
)。除无序关联容器外,其他容器都支持关系运算符(>
、>=
、<
、<=
)。关系运算符两侧的容器类型和保存元素类型都必须相同。
两个容器的比较实际上是元素的逐对比较,其工作方式与string
的关系运算符类似:
- 如果两个容器大小相同且所有元素对应相等,则这两个容器相等。
- 如果两个容器大小不同,但较小容器中的每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
- 如果两个容器都不是对方的前缀子序列,则两个容器的比较结果取决于第一个不等元素的比较结果。
1 | vector<int> v1 = { 1, 3, 5, 7, 9, 12 }; |
容器的相等运算符实际上是使用元素的==
运算符实现的,而其他关系运算符则是使用元素的<
运算符。如果元素类型不支持所需运算符,则保存该元素的容器就不能使用相应的关系运算。
顺序容器操作(Sequential Container Operations)
向顺序容器添加元素(Adding Elements to a Sequential Container)
除array
外,所有标准库容器都提供灵活的内存管理,在运行时可以动态添加或删除元素。
push_back
将一个元素追加到容器尾部,push_front
将元素插入容器头部。
1 | // read from standard input, putting each word onto the end of container |
insert
将元素插入到迭代器指定的位置之前。一些不支持push_front
的容器可以使用insert
将元素插入开始位置。
1 | vector<string> svec; |
将元素插入到vector
、deque
或string
的任何位置都是合法的,但可能会很耗时。
在新标准库中,接受元素个数或范围的insert
版本返回指向第一个新增元素的迭代器,而旧版本中这些操作返回void
。如果范围为空,不插入任何元素,insert
会返回第一个参数。
1 | list<string> lst; |
新标准库增加了三个直接构造而不是拷贝元素的操作:emplace_front
、emplace_back
和emplace
,其分别对应push_front
、push_back
和insert
。当调用push
或insert
时,元素对象被拷贝到容器中。而调用emplace
时,则是将参数传递给元素类型的构造函数,直接在容器的内存空间中构造元素。
1 | // construct a Sales_data object at the end of c |
这段代码涉及了使用
push_back()
和emplace_back()
方法向容器中添加元素的不同方式。下面对每一部分进行解释:
c.emplace_back("978-0590353403", 25, 15.99);
- 这行代码使用
emplace_back()
方法向容器c
的末尾添加一个元素。在这种情况下,使用了Sales_data
类型的构造函数,该构造函数接受三个参数,分别是 ISBN 编号、售出册数和价格。emplace_back()
方法会在容器中直接构造一个Sales_data
对象,而不是先创建一个临时对象然后再复制或移动到容器中。因此,它比push_back()
方法更高效。c.push_back("978-0590353403", 25, 15.99);
- 这行代码试图使用
push_back()
方法将一个具有三个参数的Sales_data
对象添加到容器c
的末尾。然而,push_back()
方法并不支持接受多个参数的情况,因此会导致编译错误。c.push_back(Sales_data("978-0590353403", 25, 15.99));
- 这行代码使用
push_back()
方法向容器c
的末尾添加一个元素。首先,创建了一个临时的Sales_data
对象,然后将其作为参数传递给push_back()
方法。这种方式虽然可以实现向容器中添加元素,但是需要额外的复制或移动操作,可能会影响性能。
传递给emplace
的参数必须与元素类型的构造函数相匹配。
forward_list
有特殊版本的insert
和emplace
操作,且不支持push_back
和emplace_back
。vector
和string
不支持push_front
和emplace_front
。
访问元素(Accessing Elements)
每个顺序容器都有一个front
成员函数,而除了forward_list
之外的顺序容器还有一个back
成员函数。这两个操作分别返回首元素和尾元素的引用。
在调用front
和back
之前,要确保容器非空。
在容器中访问元素的成员函数都返回引用类型。如果容器是const
对象,则返回const
引用,否则返回普通引用。
可以快速随机访问的容器(string
、vector
、deque
和array
)都提供下标运算符。保证下标有效是程序员的责任。如果希望确保下标合法,可以使用at
成员函数。at
类似下标运算,但如果下标越界,at
会抛出out_of_range
异常。
1 | vector<string> svec; // empty vector |
删除元素(Erasing Elements)
删除deque
中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。删除vector
或string
的元素后,指向删除点之后位置的迭代器、引用和指针也都会失效。
删除元素前,程序员必须确保目标元素存在。
pop_front
和pop_back
函数分别删除首元素和尾元素。vector
和string
类型不支持pop_front
,forward_list
类型不支持pop_back
。
erase
函数删除指定位置的元素。可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的erase
都返回指向删除元素(最后一个)之后位置的迭代器。
1 | // delete the range of elements between two iterators |
clear
函数删除容器内的所有元素。
特殊的forward_list
操作(Specialized
forward_list
Operations)
在forward_list
中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。
改变容器大小(Resizing a Container)
resize
函数接受一个可选的元素值参数,用来初始化添加到容器中的元素,否则新元素进行值初始化。如果容器保存的是类类型元素,且resize
向容器添加新元素,则必须提供初始值,或元素类型提供默认构造函数。
容器操作可能使迭代器失效(Container Operations May Invalidate Iterators)
向容器中添加或删除元素可能会使指向容器元素的指针、引用或迭代器失效。失效的指针、引用或迭代器不再表示任何元素,使用它们是一种严重的程序设计错误。
- 向容器中添加元素后:
- 如果容器是
vector
或string
类型,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前元素的迭代器、指针和引用仍然有效,但指向插入位置之后元素的迭代器、指针和引用都会失效。 - 如果容器是
deque
类型,添加到除首尾之外的任何位置都会使迭代器、指针和引用失效。如果添加到首尾位置,则迭代器会失效,而指针和引用不会失效。 - 如果容器是
list
或forward_list
类型,指向容器的迭代器、指针和引用仍然有效。
- 如果容器是
- 从容器中删除元素后,指向被删除元素的迭代器、指针和引用失效:
- 如果容器是
list
或forward_list
类型,指向容器其他位置的迭代器、指针和引用仍然有效。 - 如果容器是
deque
类型,删除除首尾之外的任何元素都会使迭代器、指针和引用失效。如果删除尾元素,则尾后迭代器失效,其他迭代器、指针和引用不受影响。如果删除首元素,这些也不会受影响。 - 如果容器是
vector
或string
类型,指向删除位置之前元素的迭代器、指针和引用仍然有效。但尾后迭代器总会失效。
- 如果容器是
必须保证在每次改变容器后都正确地重新定位迭代器。
不要保存end
函数返回的迭代器。
1 | // safer: recalculate end on each trip whenever the loop adds/erases elements |
vector
对象是如何增长的(How
a vector
Grows)
vector
和string
的实现通常会分配比新空间需求更大的内存空间,容器预留这些空间作为备用,可用来保存更多新元素。
capacity
函数返回容器在不扩充内存空间的情况下最多可以容纳的元素数量。reserve
函数告知容器应该准备保存多少元素,它并不改变容器中元素的数量,仅影响容器预先分配的内存空间大小。
只有当需要的内存空间超过当前容量时,reserve
才会真正改变容器容量,分配不小于需求大小的内存空间。当需求大小小于当前容量时,reserve
并不会退回内存空间。因此在调用reserve
之后,capacity
会大于或等于传递给reserve
的参数。
在C++11中可以使用shrink_to_fit
函数来要求deque
、vector
和string
退回不需要的内存空间(并不保证退回)。
额外的string
操作(Additional
string Operations)
构造string
的其他方法(Other
Ways to Construct string
s)
从另一个string
对象拷贝字符构造string
时,如果提供的拷贝开始位置(可选)大于给定string
的大小,则构造函数会抛出out_of_range
异常。
如果传递给substr
函数的开始位置超过string
的大小,则函数会抛出out_of_range
异常
改变string
的其他方法(Other
Ways to Change a string
)
append
函数是在string
末尾进行插入操作的简写形式。
1 | string s("C++ Primer"), s2 = s; // initialize s and s2 to "C++ Primer" |
replace
函数是调用erase
和insert
函数的简写形式。
1 | // equivalent way to replace "4th" by "5th" |
string
搜索操作(string
Search Operations)
string
的每个搜索操作都返回一个string::size_type
值,表示匹配位置的下标。如果搜索失败,则返回一个名为string::npos
的static
成员。标准库将npos
定义为const string::size_type
类型,并初始化为-1。
不建议用int
或其他带符号类型来保存string
搜索函数的返回值。
compare
函数(The
compare
Functions)
string
类型提供了一组compare
函数进行字符串比较操作,类似C标准库的strcmp
函数。
数值转换(Numeric Conversions)
C++11增加了string
和数值之间的转换函数。
进行数值转换时,string
参数的第一个非空白字符必须是符号(+
或-
)或数字。它可以以0x
或0X
开头来表示十六进制数。对于转换目标是浮点值的函数,string
参数也可以以小数点开头,并可以包含e
或E
来表示指数部分。
如果给定的string
不能转换为一个数值,则转换函数会抛出invalid_argument
异常。如果转换得到的数值无法用任何类型表示,则抛出out_of_range
异常。
容器适配器(Container Adaptors)
标准库定义了stack
、queue
和priority_queue
三种容器适配器。容器适配器可以改变已有容器的工作机制。
默认情况下,stack
和queue
是基于deque
实现的,priority_queue
是基于vector
实现的。可以在创建适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
1 | // empty stack implemented on top of vector |
这段代码展示了如何使用
vector
作为底层容器来实现一个栈(stack)。
stack<string, vector<string>> str_stk;
- 这行代码声明了一个名为
str_stk
的栈对象,它使用vector<string>
作为底层容器。这意味着str_stk
将使用vector
来存储栈中的元素,以实现栈的基本功能。stack<string, vector<string>> str_stk2(svec);
- 这行代码声明了另一个名为
str_stk2
的栈对象,并通过复制svec
来初始化它。在这种情况下,str_stk2
中的元素与svec
中的元素相同,因为它们是从同一份数据复制而来的。这种初始化方式可以用于创建一个已有容器的副本,作为新的栈的初始状态。
所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在array
上。适配器还要求容器具有添加、删除和访问尾元素的能力,因此也不能用forward_list
构造适配器。
栈适配器stack
定义在头文件stack
中。队列适配器queue
和priority_queue
定义在头文件queue
中。
queue
使用先进先出的存储和访问策略。进入队列的对象被放置到队尾,而离开队列的对象则从队首删除。