第九章 顺序容器
顺序容器概述
STL
容器在以下方面有不同的性能折中:
- 向容器添加或从容器中删除元素的代价
- 非顺序访问容器中元素的代价
容器 | 介绍 |
---|---|
vector | 可变大小数组。支持快速访问。在尾部之外的位置插入或删除元素可能很慢 |
deque | 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快 |
list | 双向链表。只支持双向顺序访问。在 list 中任何位置进行插入/删除操作速度都很快 |
forward_list | 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快 |
array | 固定大小数组。支持快速随机访问。不能添加或删除元素 |
string | 与 vectir 类似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快 |
迭代器
迭代器范围
一个迭代器范围( iterator range ) 由一堆迭代器表示,两个迭代器分别指向同一个容器中的元素或者尾元素之后的位置( one past the last element
)。,它们标记了容器中元素的一个范围。
标准 array 具有固定大小
与内置数组一样,标准库 array
的大小也是类型的一部分。当定义一个 array
的时候,除了指定元素类型,还要指定容器的大小
array<int, 42> // 类型为:保存 42 个 int 的数组
array<string, 10> // 类型为:保存 10 个 string 的数组
为了使用 array
类型,我们必须同时指定元素类型和大小
array<int, 10>::size_type i; // 数组类型包括元素类型和大小
array<int>::size_type j; // 错误: array<int> 不是一个类型
虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但是
array
可以
int digs[10] = {0,1,2,3,4,5,6,7,8,9};
int cpy[10] = digs; // 错误:内置数组不支持拷贝或赋值
array<int, 10> digits = {0,1,2,3,4,5,6,7,8,9};
array<int, 10> copy = digits; // 正确:只要数组类型匹配即合法
使用 assign (仅顺序容器)
赋值运算符要求左边和右边的运算对象具有相同的类型。它将右边运算对象中所有元素拷贝到左边运算对象中。顺序容器( array
除外 ) 害定义勒一个名为 assign
的成员,允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。assign
操作用参数所指定的元素(的拷贝)替换左边容器中的所有元素。例如,我们可以用 assign
实现将一个 vector
中的一段 char*
赋予一个 list
中的 string
。
cbegin()和cend()是C++11新增的,它们返回一个const的迭代器,不能用于修改元素。
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; // 错误:容器类型不匹配
// 正确:可以将 const char* 转换为 string
names.assign(oldstyle.cbegin(), oldstyle.cend());
这段代码中对 assgin
的调用将 names
中的元素替换为迭代器指定范围中的元素的拷贝。assign
的参数决定勒容器中将有多少个元素以及它们的指都是什么。
assign
的第二个版本接受一个整型值和一个元素值。它用指定数目且具有相同给定值的元素替换容器中原有的元素
// 等价于 slits1.clear()
// 后跟 slist1.insert(slist1.begin(), 10, "Hiya!");
list<string> slistl(1); // 1 个元素,为空 string
slist1.assign(10, "Hiya!"); // 10 个元素,每个都是 "Hiya!"
swap
vector<string> svec1(10);
vector<string> svec2(24);
swap(svec1, svec2);
交换两个容器内容的操作保证会很快—元素本身并未交换,swap
只是交换了两个容器的内部数据结构。
元素不会被移动的事实意味着,除 string
外,指向容器的迭代器、引用和指针在 swap
操作之后都不会失效。它们仍指向 swap
操作之前所指向的那些元素。但是,在 swap
之后,这些元素已经属于不同的容器了。例如,假定 iter
在 swap
之前指向 svec1[3]
的 string
,那么在 swap
之后它指向 svec2[3]
的元素。与其他容器不同,对一个 string
调用 swap
会导致迭代器、引用和指针失效
与其他容器不同,swap
两个 array
会真正交换它们的元素。因此,交换两个 array
所需的时间与 array
中元素的数目成证必
因此,对于 array
,在 swap
操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素值已经与另外一个 array
中对应的值进行了交换。
操作 | 作用 |
---|---|
c.insert( p, n, t ) | 在迭代器 p 指向的元素之前插入 n 个值为 t 的元素。返回指向新添加的第一个元素的迭代器;若 n 为 0,则返回 p |
c.insert( p, b, e ) | 在迭代器 b 和 e 指定的范围内的元素插入到迭代器 p 指向的元素之前。b 和 e 不能指向 c 中的元素。返回指向新添加的第一个元素的地带其;若范围为空,则返回 p |
c.insert( p, il ) | il 是一个花括号包围的元素值列表。将这些给定值插入到迭代器 p 指向的元素之前。返回指向新添加的第一个元素的迭代器;若列表为空,则返回 p |
向一个
vector、string 或 deque
插入元素会使所有指向容器的迭代器、引用和指针失效。
在容器中的特定位置添加元素
每个 insert
函数都接受一个迭代器作为其第一个参数。迭代器指出了容器中什么位置防止新元素。它可以指向容器中任何位置,包括容器尾部之后的下一个位置。由于迭代器可能指向容器尾部之后不存在的元素位置,而且在容器开始位置插入元素是很有用的功能,所以 insert
函数将元素插入到迭代器所指定的位置之前。例如,下面的语句
slist.insert(iter, "Hello!"); // 将 "Hello" 添加到 iter 之前的位置
虽然某些容器不支持 push_front
操作,但它们对于 insert
操作并无类似的限制( 插入开始位置 )。因此我们可以将元素插入到容器的开始位置,而不必担心容器是否支持 push_front
vector<string> svec;
list<string> slist;
// 等价于调用 slist.push_front("Hello!");
slist.insert(slist.begin(), "Hello!");
// vector 不支持 push_front, 但我们可以插入到 begin() 之前
// 插入到 vector 末尾以外的任何位置都可能很慢
svec.insert(svec.begin(), "Hello!");
插入范围内元素
svec.insert(svec.end(), 10, "Anna");
vector<string> v = {"quasi", "simba", "frollo", "scar"};
// 将 v 的最后两个元素添加到 slist 的开始位置
slist.insert(slist.begin(), v.end() - 2, v.end());
slist.insert(slist.end(), "these", "words", "will", "go", "at", "the", "end"));
// 运行时错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器
slist.insert(slist.begin(), slist.begin(), slist.end());
emplace
新标准三个新成员 – emplace_front、emplace 和 emplace_back
。这些操作构造而不是拷贝元素。
当调用 push
或 insert
成员函数时,则是将参数传递给元素类型的构造函数。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));
c.emplace_back(); // 使用 Sales_data 的默认构造函数
c.emplace(iter, "999-99999999"); // 使用Sales_data(string)
// 使用 Sales_data 的接受一个 ISBN、一个 count 和一个 price 的构造函数
c.emplace_front("978-0590353403", 25, 15.99);
在顺序容器中访问元素的操作
at
和下标操作只适用于string、vector、deque 和 array
。
back
不适用于forward_list
c.at(n)
返回下标为 n 的引用。如果下标越界,则抛出一out_of_range
异常
删除元素
写法 | 作用 |
---|---|
c.erase(p) | 删除迭代器 p 所指定的元素,返回一个指向被删元素之后元素的迭代器,若 p 指向尾元素,则返回尾后( off-the-end )迭代器。若 p 是尾后迭代器,则函数行为未定义 |
c.erase(b, e) | 删除迭代器 b 和 e 所指定范围内的元素。返回一个指向最后一个被删元素之后元素的迭代器,若 e 本身就是尾后迭代器,则函数也返回会尾后迭代器 |
slist.clear(); // 删除容器中所有元素
slist.erase(slist.begin(), slist.end()); // 等价调用
因为后继前驱的存在,forward_list
的操作实现方式不同,并未定义 insert、emplace 和 erase
,而是定义了名为 insert_after、emplace_after 和 erase_after
的操作。为了支持这些操作,forward_list
也定义了 before_begin
,它返回一个 **首前(off-the-beginning)**迭代器。这个迭代允许我们在链表首元素之前并不存在的元素“之后”添加或删除元素
lst.before_begin() 返回指向链表首元素之前不存在的元素的迭代器。此迭代器不能解引用。
lst.cbefore_begin() 返回一个 const_iterator
lst.insert_after(p, t) 在迭代器 p 之后的位置插入元素。t 是一个对象, n 是数量,
lst.insert_after(p, n, t) b 和 e 是表示范围的一堆迭代器(b 和 e 不能指向 lst 内),
lst.insert_after(p, b, e) il 是一个花括号列表。返回一个指向最后一个插入元素的
lst.insert_after(p, il) 迭代器。如果范围为空,则返回 P。若 p 为尾后迭代器,则函数
行为未定义
emplace_after(p, arg) 使用 args 在 p 指定的位置之后创建一个元素。返回一个指向
这个新元素的迭代器。若 p 为尾后迭代器,则函数行为未定义
lst.erase_after(p) 删除 p 指向的位置之后的元素,或删除从 b 之后直到(但不包
lst.erase_after(b, e) 含) e 之间的元素。返回一个指向被删元素之后元素的迭代器,
若不存在遮掩给的元素,则返回尾后迭代器。如果 p 指向 lst
的尾元素或者是一个尾后迭代器,则函数行为未定义
forward_list<int> flst = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto prev = flst.before_begin(); // 表示 flst 的“首前”元素
auto curr = flst.begin(); // 表示 flst 的第一个元素
while(curr != flst.end()){
if(*curr % 2)
curr = flst.erase_after(prev);
else{
prev = curr;
++curr;
}
}
容器操作可能使迭代器失效
向容器添加元素后:
- 如果容器是
vector
或string
,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效 - 对于
deque
,插入到除首尾元素之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效 - 对于
list
和forward_list
,指向容器的迭代器( 包括尾后迭代器和首前迭代器 )、指针和引用仍有效
当我们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效。
- 对于
list
和forward_list
,指向容器其他位置的迭代器( 包括尾后迭代器和首前迭代器 )、指针和引用仍有效 - 对于
deque
,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效。如果是删除尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。 - 对于
vector
和string
, 指向被删元素之前元素的迭代器、引用和指针仍有效。注意:当我们删除元素时,尾后迭代器总是会失效
使用失效的迭代器、指针或引用是严重的运行时错误
管理容量的成员函数
// shrink_to_fit 只适用于 vector、string 和 deque
// capacity 和 reserve 只适用于 vector 和 string
c.shrink_to_fit() 将 capacity() 减少为与 size() 相同的大小
c.capacity() 不重新分配内存空间的话,c 可以保存多少元素
c.reserve(n) 分配至少能容纳 n 个元素的内存空间
额外的 string 操作
const char* cp = "Hello World!!!"; // 以空字符结束的数组
char noNull[] = {'H', 'i'}; // 不是以空字符结束
string s1(cp); // 拷贝 cp 中的字符直到遇到空字符; s1 == "Hello World!!!";
string s2(noNull, 2); // 从 noNull 拷贝两个字符; s2 == "Hi"
string s3(noNull); // 未定义:noNull 不是以空字符结束
string s4(cp + 6, 5); // 从 cp[6] 开始拷贝 5 个字符; s4 == "World"
string s5(s1, 6, 5); // 从 s1[6] 开始拷贝 5 个字符; s5 == "World"
string s6(s1, 6); // 从 s1[6] 开始拷贝,直至 s1 末尾; s6 == "World!!!"
string s7(s1, 6, 20); // 正确,只拷贝到 s1 末尾; s7 == "Hello World!!!"
string s8(s1, 16); // 抛出一个 out_of_range 异常
通常当我们从一个 const char*
创建 string
时,指针指向的数组必须以空字符结束,拷贝操作遇到空字符时停止。如果我们害传递给构造函数一个计数值,数组就不必以空字符结尾。如果为传递计数值且数组也未以空字符结尾,或者给定计数值大于数组大小,则构造函数的行为是未定义的。
substr 操作
substr
操作返回一个 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 异常
改变 string 的其他方法
s.insert(s.size(), 5, '!'); // 在 s 的末尾插入 5 个感叹号
s.erase(s.size() - 5, 5); // 从 s 删除最后 5 个字符
const char* cp = "Stately, plump Buck";
s.assign(cp, 7); // s == "Stately"
s.insert(s.size(), cp + 7); // s == "Stately, plump Buck"
// 通过调用 assign 替换 s 的内容。我们赋予 s 的内容是从 cp 指向的地址开始的 7 个字符。要求赋值的字符数必须小于或等于 cp 指向的数组中的字符数(不包括结尾的空字符)
// 接下来在 s 上调用 insert,将 cp 开始的 7 个字符拷贝到 s 中
append
string s("C++ Primer"), s2 = s; // 将 s 和 s2 初始化为 "C++ Primer"
s.insert(s.size(), " 4 th Ed."); // s == "C++ Primer 4th Ed.";
s2.append(" 4 th Ed."); // s2 == "C++ Primer 4th Ed."; 等价
replace
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") // 等价 s2 == "C++ Primer 5th Ed.";
s.insert(pos, args) // 在 pos 之前插入 arg 指定的字符。pos 可以是一个下标或
// 一个迭代器。接受下标的版本返回一个指向 s 的引用; 接受
// 迭代器的版本返回指向第一个插入字符的迭代器
s.erase(pos, len) // 删除从位置 pos 开始ide len 个字符。如果 len 被省略,则
// 删除从 pos 开始直至 s 末尾所有字符。返回一个指向 s 的引用
s.assign(args) // 将 s 中的字符替换为 args 指定的字符。返回一个指向 s 的引用
s.append(args) // 将 args 追加到 s。
s.replace(range, arg) // 删除 s 中范围 range 内的字符,替换为 args 指定的字符。range
// 或者是一个下标和一个长度,或者是一对指向 s 的迭代器。返回
// 一个指向 s 的引用
arg 可以是下列形式之一; append 和 assign 可以使用所有形式
str 不能与 s 相同,迭代器 b 和 e 不能指向 s
str 字符串 str
str, pos, len str 中从 pos 开始最多 len 个字
cp, len 从 cp 指向的字符数组的前 len 个字符
cp cp 指向的以空字符结尾的字符数组
n, c n 个字符 c
b, e 迭代器 b 和 e 指定的范围内的字符
初始化列表 花括号包围的,以逗号分隔的字符列表
find
string
搜索操作都返回一个 string::size_type
指,表示匹配发生位置的下标。如果搜索失败,则返回一个名为 string::npos
的 static
成员。
string name("AnnaBelle");
auto pos1 = name.find("Anna"); // pos1 == 0
string lowercase("annabelle");
pos1 = lowercase.find("Anna"); // pos1 == npos
string numbers("0123456789"), name("r2d2");
// 返回 1,即,name 中第一个数字的下标
auto pos = name.find_first_of(numbers);
// 如果要搜索第一个不再参数中的字符,我们应该调用 find_first_not_of。例如,为了搜索一个 string 中的第一个非数字字符
string dept("03714p3");
// 返回 5 -- 字符 'p' 的下标
auto pos = dept.find_first_not_of(number);
s.find(args)
查找s
中args
第一次出现的位置s.rfind(args)
查找s
中args
最后一次出现的位置s.find_first_of(args)
在s
中查找args
中任何一个字符第一次出现的位置s.find_last_of(args)
在s
中查找args
中任何一个字符最后一次出现的位置s.find_first_not_of(args)
在s
中查找第一个不在args
出现的字符s.find_last_not_of(args)
在s
中查找最后一个不在args
中出现的字符
arg
必须是以下形式之一
c, pos
从s
中位置pos
开始查找字符c
。pos
默认为 0s2, pos
从s
中位置pos
开始查找字符串s2
。pos
默认为 0cp, pos
从s
中位置pos
开始查找指针cp
指向的以空字符结束的C
风格字符串,pos
默认为 0cp, pos, n
从s
中位置pos
开始查找指针cp
指向的数组的前n
个字符。pos
和n
无默认值
数值转换
int i = 42;
string s = to_string(i); // 将征数 i 转换为字符表示形式
double d = stod(s); // 将字符串 s 转换为浮点数
to_string(val)
stoi(s, p, b) // 返回 s 的起始子串(表示整数内容)的数值,返回值类型分别是 int
stol(s, p, b) // long、unsigned long、long long 、unsigned long long。 b 表示
stoul(s, p, b) // 转换所用的基数,默认值为 10。p 是 size_t 指针,用来保存 s 中第
stoull(s, p, b) // 一个非数值字符的下标, p 默认为 0,即,函数不保存下标
stof(s, p) // 返回 s 的起始子串( 表示浮点数内容 )的数值,返回值类型分别是
stod(s, p) // float、double 或 long double。参数 p 的作用与整数转换函数中
stold(s, p) // 一样