第九章——顺序容器
元素在顺序容器中的顺序与其加入容器时的位置相对应。
所有的容器都共享公共的接口,但不同的容器按不同的方式对其进行扩展。
9.1 顺序容器概述
所有顺序容器都提供了快速访问元素的能力,但是这些容器在以下方面都有不同的性能这种:
- 向容器添加或从容器删除元素的代价
- 非顺序访问容器中元素的代价
除了固定大小的array外,其他容器都提供高效、灵活的内存管理。我们可以添加和删除元素,扩张和收缩容器的大小。
确定使用哪种顺序容器
通常,使用
vector
是最好的选择,除非有更好的理由选择其他容器
选择容器的基本原则:
- 除非你有很好的理由选择其他容器,否则应使用
vector
- 如果你的程序有很多小的元素,且空间的额外开销很重要,则不要使用
list
或forward_list
- 如果程序要求随机访问元素,应使用
vector
或deque
- 如果程序要求在容器的中间插入或删除元素,应使用
list
或forward_list
- 如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用
deque
。 - 如果程序只有在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则
-
- 首先,确定是否真的需要在容器中间位置添加元素。当处理输入数据时,通常可以很容易地向vector追加数据,然后再调用标准库的sort函数来重排容器中的元素,从而避免在中间位置添加元素。
-
- 如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector中。
9.2 容器库概览
- 某些操作是所有容器类型都提供的
- 另外一些操作仅针对顺序容器、关联容器、无序容器
- 还有一些操作只适用于一小部分容器
对容器可以保存的元素类型的限制
顺序容器几乎可以保存任意类型的元素。
虽然我们可以在容器中保存几乎任何类型,但某些容器操作对元素类型有其自己的特殊要求。我们可以为不支持特定操作需求的类型定义容器,但这种情况下就只能使用那些没有特殊要求的容器操作了。
例如,顺序容器构造函数的一个版本接受容器大小参数,它使用了元素类型的默认构造函数。但某些类没有默认构造函数。我们可以定义一个保存这种类型对象的容器,但我们在构造这种容器时不能只传递给它一个元素数目参数:
// 假定noDefault是一个没有默认构造函数的类型
vector<noDefault> v1(10, init); // 正确:提供了元素初始化器
vector<noDefault> v2(10); // 错误:必须提供一个元素初始化器
9.2.1 迭代器
与容器一样,迭代器有着公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。(例如解引用、递增)
表3.6列出了容器迭代器支持的所有操作,其中有一个例外不符合公共接口特点——forward_list
迭代器不支持递减运算符(–)。 表3.7列出了迭代器支持的算术运算,这些运算只能应用于string、vector、deque和array的迭代器。我们不能将它们用于其他任何容器类型的迭代器。
迭代器范围
迭代器范围的概念是标准库的基础
一个迭代器范围由一对迭代器表示,这两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。这种元素范围被称为左闭合区间,即[begin, end)
使用左闭合范围蕴含的编程假定
- if (begin == end)
- if (begin != end)
- 我们可以递增若干次,使得begin == end
9.2.2 容器类型成员
每个容器都定义了多个类型,如size_type
、iterator
、const_iterator
。除此之外海域反向迭代器。
与正向迭代器相比,各种操作的含义也都发生了颠倒。例如,对一个反向迭代器执行++操作,会得到上一个元素。
剩下的就是类型别名了,通过类型别名,我们可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的value_type
。 如果需要元素类型的一个引用,可以使用reference
或const_ reference
。这些元素相关的类型别名在泛型编程中非常有用。
为了使用这些类型,我们必须显式使用其类名:
// iter是通过list<string>定义的一个迭代器类型
list<string>::iterator iter;
// count是通过vector<int>定义的一个difference_type类型
vector<int>::difference_type count;
9.2.3 begin和end成员
begin和end有多个版本:带r的版本返回反向迭代器;以c开头的版本则返回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
9.2.4 容器定义和初始化
每个容器类型都定义了一个默认构造函数。除array之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数。
将一个容器初始化为另一个容器的拷贝
- 直接拷贝整个容器。此时,两个容器类型、元素类型必须匹配。
- 拷贝有一个迭代器对指定的元素范围(array除外)。此时,容器类型可以不同,而且只要拷贝的元素可以转换为新容器的元素类型即可。
// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};
list<string> list(authors); // 正确:类型匹配
deque<string> authList(authors); // 错误:容器类型不匹配
vector<string> words(articles); // 错误:元素类型不匹配
// 正确:可以将const char*元素转换为string
forward_list<string> words(articles.begin(), articles.end());
列表初始化
// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};
初始化列表还隐含的制定了容器的大小:容器将包含与初始值一样多的元素。
与顺序容器大小相关的构造函数
顺序容器还提供另一个构造函数,他接受一个容器大小和一个(可选的)元素初始值。如果我们不提供元素初始值,则标准库会创建一个值初始化器:
vector<int> ivec(10, -1); // 10个int元素,每个都初始化为-1
list<string> svec(10, "hi"); // 10个string元素,每个都初始化为"hi"
forward_list<int> ivec(10); // 10个int元素,每个都初始化为0
deque<string> svec(10); // 10个string元素,每个都为空
【Note】只有顺序容器的构造函数才接受大小参数,关联容器并不支持
标准库array具有固定大小
当定义一个array
时,除了指定元素类型,还要制定容器大小:
array<int, 42> // 类型为:保存42个int的数组
array<string, 10> // 类型为:保存10个string的数组
array大小固定的特性也影响了它所定义的构造函数的行为。与其他容器不同,一个默认构造的array是非空的:它包含了与其大小一样多的元素。这些元素都被默认初始化,就像一个内置数组中的元素那样。
如果我们对array进行列表初始化,初始值的数目必须等于或小于array的大小。如果初始值数目小于array的大小,则它们被用来初始化array中靠前的元素,所有剩余元素都会进行值初始化。在这两种情况下,如果元素类型是一个类类型,那么该类必须有一个默认构造函数,以使值初始化能够进行:
array<int, 10> ial; // 10个0
array<int, 10> ia2 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
array<int, 10> ia3 = {42}; // ia3[0]为42,其余为0
值得注意的是,虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array可以
array<int, 10>digits = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
array<int, 10>copy = digits;
array不仅要求类型相同,还要求大小相同,因为大小是array类型的一部分。
9.2.5 赋值和swap
赋值运算符可用于所有容器
c1 = c2;
c1 = {a, b, c};
使用assign(仅顺序容器)
assign
操作用参数所指定的元素替换左边容器中的所有元素。例如,我们可以用assgin实现将一个vector中的一段char *值赋予一个list中的string:
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; // 错误:类型不匹配
names.assign(oldstyle.cbegin(), oldstyle.cend()); // 正确:可以将const char*转换为string
这段代码中对assign的调用将names中的元素替换为迭代器指定的范围中的元素的拷贝。assign的参数决定了容器中将有多少个元素以及它们的值都是什么。
由于其旧元素被替换,因此传递给assign的迭代器不能指向调用assign的容器
assign的第二个版本接受一个整型值和一个元素值。它用指定数目且具有相同给定值的元素替换容器中原有的元素:
// 等价于slist1.clear()
// 后跟slist1.insert(slist1.begin(), 10, "Hiya");
list<string> slist1(1); // 1个元素,为空string
slist.assign(10, "Hiya"); // 10个元素,每个都是"Hiya"
使用swap
swap
操作交换两个相同类型容器的内容:
vector<string> svec1(10); // 10个元素
vector<string> svec2(20); // 20个元素
swap(svec1, svec2);
调用swap之后,svec1包含20个元素,svec2包含10个元素。
swap操作很快,只需要常数时间(除array),元素本身并为交换,只是交换了两个容器的内部数据结构。
这意味着,除string外,指向容器的迭代器、引用和指针,在swap操作之后都不会失效,但这些元素已经属于不同容器了。
例如,假定iter在swap之前指向svec1[3]的string,那么在swap之后它指向svec2[3]的元素。与其他容器不同,对一个string调用swap会导致迭代器、引用和指针失效。
如:
vector<int> a = { 0, 1, 2 };
vector<int> b = { 3, 4, 5, 6, 7, 8 };
auto a_begin = a.begin();
auto a_end = a.end();
auto b_begin = b.begin();
auto b_end = b.end();
for (auto it = a_begin; it != a_end; ++it) cout << *it << " ";
cout << endl;
for (auto it = b_begin; it != b_end; ++it) cout << *it << " ";
cout << endl;
swap(a, b);
for (auto it = a_begin; it != a_end; ++it) cout << *it << " ";
cout << endl;
for (auto it = b_begin; it != b_end; ++it) cout << *it << " ";
cout << endl;
由此可见a、b经过swap之后,迭代器并未指向新容器的元素,仍指向老容器的元素
赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容教会不会导致指向容器的迭代器、引用和指针失效(array、string除外)
9.2.6 容器大小操作
除了一个例外(forward_list
只支持empty
和max_size
),每个容器类型都有三个与大小相关的操作:
size
:返回容器中的元素数目empyt
:如果size为0返回truemax_size
:返回一个大于或等于该容器所嗯呢该容纳的最大元素数的值
9.2.7 关系运算符
每个容器都支持相等运算符(==、!=);除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。
关系运算符左右两边的运算对象必须是相同类型的容器,且保存的是相同类型的元素
【Note】只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器
9.3 顺序容器操作
顺序容器和关联容器的不同之处在于两者组织元素的方式。这些不同之处直接关系到了元素如何存储、访问、添加以及删除。
9.3.1 向顺序容器添加元素
除array外,所有标准库容器都提供林获得内存管理。在运行时可以动态添加或删除元素来改变容器大小
在一个vector
或string
的尾部之外的任何位置,或是一个deque
的首尾之外的任何位置添加元素,都需要移动元素。而且,向一个vector或string添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。
使用push_back
除了array
和forward_list
之外,每个顺序容器都支持push_back
。
当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。
使用push_front
除了push_back
,list
、forward_list
和deque
还支持名为push_front
的类似操作,此操作将元素插入到容器头部
注意,deque
想vector
一样提供了随机访问元素的能力,但它提供了vector
所不支持的push_front
。deque
保证在容器首位进行插入和删除元素的操作都只花费常数时间。与vector
一样,在deque
首位之外的位置插入元素会很耗时
在容器中的特定位置添加元素
insert
成员提供了更一般的添加功能,它允许我们在容器中任意位置插入0个或多个元素。每个容器都支持insert
成员
每个insert函数都接受一个迭代器作为其第一个参数。迭代器指出了在容器中什么位置放置新元素。它可以指向容器中任何位置,包括容器尾部之后的下一个位置。由于迭代器可能指向容器尾部之后不存在的元素的位置,而且在容器开始位置插入元素是很有用的功能,所以insert函数将元素插入到迭代器所指定的位置之前。例如:
slist.insert(iter, "Hello");
将元素插入到vector、deque和string中的任何位置都是合法的,但这样可能很耗时
插入范围内元素
除了第一个迭代器参数之外,insert
函数还可以接受更多的参数,这与容器构造函数类似。其中一个版本接受一个元素数目和一个值,它将指定数量的元素添加到指定位置之前,这些元素都按给定值初始化:
svec.insert(svec.end(), 10, "Anna");
接受一对迭代器或一个初始化列表的insert
版本将给定范围中的元素插入到指定位置之前:
vector<int> v = {0, 1, 2, 3};
// 将v的最后两个元素添加到slist的开始位置
slist.insert(slist.begin(), v.end() - 2, v.end());
// 运行时错误:迭代器表示要拷贝的范围,不能指向目的位置相同的容器
slist.insert(slist.begin(), slist.begin(), slist.end());
如果我们传递给insert一对迭代器,它们不能指向添加元素的目标容器。
使用insert的返回值
通过使用insert的返回值,可以在容器中一个特定位置反复插入元素:
list<string> lst;
auto iter = lst.begin();
while (cin >> word)
iter = lst.insert(iter, word); // 等价于调用push_back
insert
返回的迭代器恰好指向这个新元素
使用emplace操作
新标准引入了三个新成员——emplace_front
、emplace
和emplace_back
,这些操作构造而不是拷贝元素。这些操作分别对应push_front
、insert
和push_back
,允许我们将元素放置在容器头部、指定位置之前、容器尾部。
当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。例如,假定c保存Sales_data元素:
// 在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));
其中对emplace_back
的调用和第二个push_back
调用都会创建新的Sales_data对象。在调用emplace_back时,会在容器管理的内存空间中直接创建对象。而调用push_back则会创建一个局部临时对象,并将其压入容器中。
【Note】
emplace
函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配。
9.3.2 访问元素
如果容器中没有元素,访问操作的结果是未定义的。
包括array在内的每个顺序容器都有一个front
成员函数,但除了forward_list
之外的所有顺序容器都有个back
成员函数
程序可以用两种不同方式来获取中的首元素和尾元素的引用。直接的方法是调用front
和back
而间接的方法是通过解引用begin
返回的迭代器来获得首元素的引用,以及通过递减然后解引用end
返回的迭代器来获得尾元素的引用。
访问成员函数返回的是引用
访问元素的成员函数(front
、back
、at
)返回的都是引用。
如果容器是一个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异常
9.3.3 删除元素
与添加元素的多种方式类似,(非array
)容器也有多种删除元素的方式:
删除
deque
中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效
指向vector
或string
中删除点之后位置的迭代器、引用和指针都会失效
删除元素的成员函数并不检查其参数。在删除元素之前,程序员必须确保它是存在的。
pop_front和pop_back成员函数
pop_front
和pop_back
成员函数分别删除首元素和尾元素。与vector
和string
不支持push_front
一样,这些类型也不支持pop_front
。类似的,forward_list
不支持pop_back
。
与元素访问成员函数类似,不能对一个空容器执行弹出操作。如果你需要弹出的元素的值,就必须在执行弹出操作之前保存它:
while (!ilist.empty()) {
process(ilist.front()); // 对ilist的首元素进行处理
ilist.pop_front(); // 完成处理后删除首元素
}
从容器内部删除一个元素
成员函数erase从容器中指定位置删除元素。
我们可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。
两种形式的erase都返回指向删除的(最后一个)元素之后位置的迭代器。即,若j是i之后的元素,那么erase(i)将返回指向j的迭代器。
list<int> lst = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto it = lst.begin();
while (it != lst.end())
if (*it % 2) // 若为奇数
it = lst.erase(it); // 删除此元素,返回下一元素的迭代器
else
++it;
删除多个元素
接受一对迭代器的erase版本允许我们删除一个范围内的元素(左闭右开),返回指向最后一个被删元素之后位置的迭代器:
elem1 = slist.earse(elem1, elem2); // 调用后,ele1 == ele2
为了删除一个容器中的所有元素,我们既可以调用clear
,也可以调用begin
和end
获得的迭代器作为参数调用earse
:
slist.clear();
slist.erase(slist.begin(), slist.end());
9.3.4 特殊的forward_list操作
forward_list
是一个单链表,正因为它是单链表,所以在插入、删除某一元素需要获取这个元素的前驱,所以并未定义insert
、emplace
和earse
,取而代之的是insert_after
、emplace_after
和earse_after
。
为了删除elem3,应该使用指向elem2的迭代器调用erase_after
。
为了支持这些操作,forward_list
也定义了before_begin
,它返回一个首前迭代器。这个迭代器允许我们在链表首元素之前并不存在的元素“之后”添加或删除元素(亦即在链表首元素之前添加删除元素)。
当在forward_list
中添加或删除元素时,我们必须关注两个迭代器——一个指向我们要处理的元素,另一个指向其前驱。例如,从forward_list
中删除元素:
forward_list<int> flst = {0, 1, 2, 3, 4, 5, 6, 7, 8 ,9};
auto prev = flst.before_begin();
auto curr - flst.begin();
while(curr != flst.end()) {
if (*curr % 2)
curr = flst.erase_after(prev);
else {
prev = curr; // 移动迭代器current,指向下一个元素,prev指向curr之前的元素
++curr;
}
}
9.3.5 改变容器大小
可以用resize
来增大或缩小容器,array
不支持此操作。
如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部:
list<int> ilist(10, 42);
ilist.resize(15); // 将5个0添加到ilist末尾
ilist.resize(25, -1); // 将10个-1添加到ilist末尾
ilist.resize(5); // 从ilist末尾删除20个元素
resize操作接受一个可选的元素值参数,用来初始化添加到容器中的元素。如果调用者未提供此参数,新元素进行值初始化。如果容器保存的是类类型元素,且resize向容器添加新元素,则我们必须提供初始值,或者元素类型必须提供一个默认构造函数。
如果
resize
缩小容器,则指向被删除元素的迭代器、引用、指针都会失效
对vector
、string
、deque
进行resize
可能导致迭代器、引用、指针失效
9.3.6 容器扫做可能是迭代器失效
向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。使用失效的指针、引用或迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针一样的问题
向容器添加元素后:
- 如果容器是
vector
或string
,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效。 - 对于
deque
,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。 - 对于
list
和forward_list
,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。
当删除一个元素后:
- 对于
list
和forward_list
,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效。 - 对于
deque
,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效。如果是删除deque的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。 - 对于
vector
和string
,指向被删元素之前元素的迭代器、引用和指针仍有效。
注意:当我们删除元素时, 尾后迭代器总是会失效。
编写改变容器的循环程序
添加、删除vector
、string
或deque
元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用或指针。如果循环中调用的是insert
或erase
,那么更新迭代器很容易。这些操作都返回迭代器,我们可以用来更新:
// 删除偶数元素,赋值奇数元素
vector<int> vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto iter = vi.begin(); 调用begin而不是cbegin,因为我们要改变vi
while(iter != vi.end()) {
if (*iter % 2) {
iter = vi.insert(iter, *iter); // 复制当前元素
iter += 2; // 向前移动迭代器,跳过当前元素以及插入到他之前的元素
} else
iter = vi.erase(iter); // 删除偶数元素
// 不应向前移动迭代器,iter指向我们删除的元素之后的元素
}
不要保存end返回的迭代器
当我们添加/删除vector
或string
的元素后,或在deque
中首元素之外任何位置添加/删除元素后,原来end返回的迭代器总是会失效。因此,添加或删除元素的循环程序必须反复调用end,而不能在循环之前保存end返回的迭代器,一直当作容器末尾使用。通常C++标准库的实现中end()操作都很快,部分就是因为这个原因。
9.4 vector对象是如何增长的
当不得不获取新的内存空间时,vector
和string
的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,可用来保存更多的新元素。这样就不需要每次添加新元素都重新分配容器的内存空间了。
这种分配策略比每次添加新元素时都重新分配容器内存空间的策略要高效得多。其实际性能也表现得足够好,虽然vector在每次重新分配内存空间时都要移动所有元素,但使用此策略后,其扩张操作通常比list和deque还要快。
管理容量的成员函数
vector
和string
类型提供了一些成员函数,允许我们与它的实现中内存分配部分互动。
capacity
操作告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素。reserve
操作允许我们通知容器它应该准备保存多少个元素。
reserve
并不改变容器中元素的数量,它仅影响vector
预先分配多大的空间
resize
并不改变容器分配空间的大小,它仅影响容器中元素的个数
只有当需要的内存空间超过当前容量时,reserve
调用才会改变vector
的容量。如果需求大小大于当前容量,reserve
至少分配与需求一样大的内存空间(可能更大)。
如果需求大小小于或等于当前容量,reserve
什么也不做。特别是,当需求大小小于当前容量时,容器不会退回内存空间。因此,在调用reserve之后,capacity将会大于或等于传递给reserve的参数。这样,调用reserve永远也不会减少容器占用的内存空间。
类似的,resize
成员函数只改变容器中元素的数目,而不是容器的容量。我们同样不能使用resize来减少容器预留的内存空间。
在新标准库中,我们可以调用shrink_to_fit
来要求deque
、vector
或string
退回不需要的内存空间。此函数指出我们不再需要任何多余的内存空间。但是,具体的实现可以选择忽略此请求。也就是说,调用shrink_ to_fit也并不保证一定退回内存空间。
capacity和size
容器的size是指它已经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。
vector<int> ivec;
for (vector<int>::size_type ix = 0; ix != 24; ++ix)
ivec.push_back(ix);
cout << "size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl; // size: 24 capacity: 28
ivec.reserve(50);
// capacity大于等于50
cout << "size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl; // size: 24 capacity: 50
while(ivec.size() != ivec.capacity())
ivec.push_back(0);
cout << "size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl; // size: 50 capacity: 50
ivec.push_back(42);
cout << "size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl; // size: 51 capacity: 75
可以调用shrink_to_fit
来要求vector
将超出当前大小的多与元素退回给系统:
ivec.shrink_to_fit(); // 要求归还内存
// size应该未改变,capacity依赖于具体实现
cout << "size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl; // size: 51 capacity: 51
9.5 额外的string操作
9.5.1 构造string的其他方法
除了我们以前介绍过的构造函数,以及与其他顺序容器相同的构造函数外,string类型还支持另外三个构造函数:
substr操作
sustr
操作返回一个string
,他是原始string的一部分或者全部的拷贝。可以传递给substr一个客源的开始位置和计数值:
string s("hello world");
string s2 = s.substr(0, 5); // s2 = hello
string s3 = s.substr(6); // s3 = world
string s4 = s.substr(6, 11); // s4 = world
string s5 = s.substr(12); // 抛出一个out_of_range异常
9.6 容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:stack
、queue
和priority_queue
。适配器(adaptor)是标准库中的一个通用概念。 容器、迭代器和函数都有适配器。本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。例如,stack适配器接受一个顺序容器(除array或forward_list 外),并使其操作起来像一个stack一样。
定义一个适配器
每个适配器都定义两个构造函数:默认构造函数创建一个空对象, 接受一个容器的构造函数拷贝该容器来初始化适配器。例如,假定deq是一个deque<int>
,我们可以用deq来初始化一个新的stack
,如下所示:
stack<int> stk(deq); // 从deq拷贝元素到stack
默认情况下,stack
和queue
是基于deque实现的,priority_queue
是在vector
之上实现的。我们可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
// vector上实现的空栈
stack<string, vector<string>> str_stkl;
// str_stk2在vector上实现初始化时保存svec的拷贝
stack<string, vector<string>> str_stk2(svec);
对于一个给定的适配器,可以使用哪些容器是有限制的。
所有适配器都要求容器具有添加和删除元素的能力。因此,适配器不能构造在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
构造。
栈适配器
stack<int> intStack;
for (size_t ix = 0; ix != 10; ++ix)
intStack.push(ix); // 保存0-9十个数
while(!intStack.empty()) { //intStack有值就循环
int value = intStack.top();
intStack.pop(); // 弹出栈顶元素,继续循环
}
队列适配器
标准库queue
使用一种先进先出(FIFO)的存储和访问策略。进入队列的对象被放置到队尾,而离开队列的对象则从队首删除。饭店按客人到达的顺序来为他们安排座位,就是一个先进先出队列的例子。
priority_queue
允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。饭店按照客人预定时间而不是到来时间的早晚来为他们安排座位,就是一个优先队列的例子。默认情况下,标准库在元素类型上使用<
运算符来确定相对优先级。
小结
标准库容器是模板类型,用来保存给定类型的对象。在一个顺序容器中,元素是按顺序存放的,通过位置来访问。顺序容器有公共的标准接口:如果两个顺序容器都提供一个特定的操作,那么这个操作在两个容器中具有相同的接口和含义。
所有容器(除array外)都提供高效的动态内存管理。我们可以向容器中添加元素,而不必担心元素存储在哪里。容器负责管理自身的存储。vector和string都提供更细致的内存管理控制,这是通过它们的reserve和capacity成员的数来实现的。
很大程度上,容器只定义了极少的操作。每个容器都定义了构造函数、添加和删除元素的操作、确定容器大小的操作以及返回指向特定元素的迭代器的操作。其他一些有用的操作,如排序或搜索,并不是由容器类型定义的,而是由标准库算法实现的。
当我们使用添加和删除元素的容器操作时,必须注意这些操作可能使指向容器中元素的迭代器、指针或引用失效。很多会使迭代器失效的操作,如insert
和erase
,都会返回一个新的迭代器,来帮助程序员维护容器中的位置。如果循环程序中使用了改变容器大小的操作,就要尤其小心其中迭代器、指针和引用的使用。