C++ Primer 学习笔记 第九章 顺序容器
元素在顺序容器中的顺序与其加入容器时的位置相对应。关联容器中元素的位置由元素相关联的关键字值决定。
所有容器类都共享公共的接口,不同容器按不同方式对其进行扩展。我们基于某种容器所学习的内容也都适用于其他容器。每种容器都提供了不同的性能和功能的权衡。
一个容器就是一些特定类型对象的集合。
所有顺序容器都提供了快速顺序访问元素的能力,但这些容器在以下方面都有不同的性能折中:
1.向容器添加或从容器中删除元素的代价。
2.非顺序访问容器中元素的代价。
标准库顺序容器类型:
除固定大小的array外,其他容器都提供高效、灵活的内存管理。容器保存元素的策略对容器操作的效率、是否支持特定操作都有影响。
如string和vector将元素保存在连续的内存空间中,由于元素是连续存储的,由元素下标来计算地址是非常快速的,但在这两种容器的中间位置添加或删除元素就会非常耗时:一次插入或删除后,需要移动操作位置之后的所有元素,来保持连续存储,而且,添加一个元素有时可能还需要分配额外的存储空间,这种情况下,每个元素都必须移动到新的存储空间中。
而list和forward_list两个容器的设计目的是令容器任何位置添加和删除操作都很快速,作为代价,这两个容器不支持元素的随机访问:为访问一个元素,我们只能遍历整个容器,并且与vector、deque和array相比,这两个容器的额外内存开销也很大。
deque与string和vector类似,支持快速随机访问,且在中间位置添加或删除元素的代价(可能)很高。但deque在两端添加和删除元素都是很快的,与list或forward_list添加删除元素的速度相当。
forward_list和array是C++ 11新标准添加的类型。与内置数组类型相比,array是一种更安全、更容易使用的数组类型。与内置数组相似,array对象的大小是固定的,因此,array不支持添加和删除元素以及改变容器大小的操作。forward_list的设计目标是达到与最好的手写单向链表数据结构相当的性能,因此,forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size保证是一个快速的常量时间的操作。
新标准库的容器比旧版本快很多,新标准库容器的性能几乎肯定比与最精心优化过的同类数据结构一样好(通常更好),现代C++应该使用标准库容器而不是原始的数据结构,如内置数组。
使用哪种容器,选择时的基本准则:
1.除非有很好的理由选择其他容器,否则应使用vector。
2.如果你的程序有很多小元素,且空间的额外开销很重要,则不要使用list或forward_list。
3.如果程序要求随机访问元素,应使用vector或deque。
4.如果程序要求在容器中间插入或删除元素,应使用list或forward_list。
5.如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用deque。
6.如果程序只有在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则:
(1)首先,确定是否真的需要在容器中间位置添加元素,当处理输入数据时,通常很容易向vector追加数据,然后再调用标准库的sort函数来重排容器中的元素,从而避免在中间位置添加元素。
(2)如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list中内容拷贝到一个vector中。
如果程序既需要随机访问元素又需要在容器中间位置插入元素,则由应用中占主导地位的操作决定。此情况下分别测试应用的性能是必要的。
容器上操作的层次:
1.所有容器类型都提供的操作。
2.另一些操作只针对顺序容器或关联容器或无序容器。
3.还有一些仅适用于一小部分容器。
一般,每个容器都定义在一个头文件中,文件名与类型名一致。容器均定义为模板类。对大多数,但不是所有容器,我们还需额外提供元素类型信息:
list<Sales_data> l;
deque<double> d;
顺序容器几乎可以保存任意类型的元素,特别是,我们可以定义一个元素类型是另一个容器的容器:
vector<vector<string>> lines; // C++11新标准
vector<vector<string> > lines; // 旧标准
当顺序容器构造函数的一个版本接收容器大小参数,它使用了元素类型的默认构造函数,但如果某些类没有默认构造函数:
vector<noDefault> v1(10, init); // 正确,提供了元素初始化器,init是一个noDefault类的对象
vector<noDefault> v2(10); // 错误,必须提供元素初始化器
例子:
struct A {
A(int i) : j(i) { }
int j;
};
int main() {
A a = A(42);
vector<A> vA(10); // 错误
vector<A> vA(10, a); // 正确,初始化为10个与a相同的A类对象
for (A temp : vA) {
cout << temp.j << endl; // 输出42
}
}
所有容器都提供的操作:
对vector比大小按字典顺序。
与容器类似,迭代器有公共接口,如一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式(使用方式)都是相同的,如解引用、自增。但有个例外不符合公共接口的特点,即forward_list迭代器不支持自减。
一对迭代器表示的范围(两迭代器指向同一容器中的元素或尾元素之后的位置)是左闭右开的。
对构成范围的迭代器要求(编译器不强制要求,但程序员要保证符合这些约定):
1.两迭代器指向同一容器中元素或尾后元素。
2.end不在begin前。
如begin和end相等,则范围为空。
当实现一个使用迭代器在容器中寻找特定值的函数时,若没找到可以返回尾后元素迭代器表示没找到。
list容器的迭代器不支持大于、小于、大于等于、小于等于操作,可以用==或!=判断两迭代器是否指向同一元素。
容器类型的成员我们已经用过的有size_type、iterator、const_iterator。
大多数容器提供反向迭代器,与正向迭代器相比,各个操作的含义也发生了改变,如++会得到上一个元素。
为了使用容器中的各种类型,我们必须显式使用类型名:
list<string>::iterator iter; // 写入值时使用
list<string>::const_iterator iter; // 读取值时使用
vector<int>::difference_type count;
vector<int>::size_type sz;
begin和end生成的两个迭代器范围包含整个容器。
begin和end有多个版本,带r的返回反向迭代器,带c的返回const迭代器:
list<int> li = { 1,2,3 };
list<int>::iterator b1 = li.begin();
list<int>::const_iterator b2 = li.cbegin();
list<int>::reverse_iterator b3 = li.rbegin();
list<int>::const_reverse_iterator b4 = li.crbegin();
以上以c开头的版本是C++11新标准引入的。在过去,没有类似cbegin等函数时,iterator是否是const的依赖于容器是否是const的。
用const容器的begin或cbegin函数的返回值赋值给非const类型的迭代器时会报错:
const list<int> li = { 1,2,3 };
list<int>::iterator it = li.begin(); // 报错
list<int>::const_iterator it = li.begin(); // 正确
每个容器类型都定义了默认构造函数,除array外,其他容器的默认构造函数都会创建一个指定类型的空容器。
容器定义和初始化操作:
将一个新容器创建为另一个容器的拷贝:
1.拷贝整个容器。(两个容器的类型及其元素类型必须完全匹配)
2.除array外,拷贝由一个迭代器对指定的元素范围。(只要迭代器指向的元素类型能转换成新容器中元素类型)
C++11新标准中可以对容器进行列表初始化。除了array外的其他容器类型,初始值列表还隐含地指定了容器的大小。
除array外的顺序容器还提供另一个构造函数,即提供一个数字和一个元素初始值(可选):
vector<int> ivec(10, -1); // 10个-1
forward_list<int> ivec(10); // 没提供初始值则值初始化
包含没有默认构造函数的类类型容器,如果初始化时只提供一个size,那么必须还提供一个元素初始值。但只有顺序容器的构造函数才接受一个size的参数,关联容器不支持。
与内置类型一样,array的大小也是类型的一部分,定义array时,必须指定容器大小:
array<int, 42> arri;
array<int, 42>::size_type i; // 大小也是类型的一部分
由于大小也是array类型的一部分,因此它不支持普通容器的构造函数,因为这些构造函数或隐式或显式地确定容器大小。
array的默认构造函数构造的array是非空的,它包含与其大小一样多的元素,这些元素是默认初始化的,并且用列表初始化时,列表中元素数量必须小于等于array大小,这就像数组一样。与数组不同的是,它可以直接拷贝或赋值。
赋值可用于所有容器:
c1 = c2;
c1 = { a, b, c };
第一个赋值运算后,如果两个容器原来大小不同,赋值运算后两个容器大小都会与右边容器大小相同。
array允许赋值:
array<int, 10> a1 = {0,1,2,3,4,5,6,7,8,9};
array<int, 10> a2 = {0};
a1 = a2; // 替换a1中的元素,a1、a2的类型必须相同,array的大小也是类型的一部分
a2 = {1}; // 书上说错误,理由是不能将一个花括号列表赋予数组,但我实际测试可以正常运行,a2大小还是10,除了a2[0]为1,剩余被值初始化
array<int, 3> arr1;
cout << arr1[0] << endl; // 输出随机值
arr1 = { 1 };
cout << arr1[0] << endl; // 输出1
cout << arr1[1] << endl; // 输出0
容器赋值运算:
赋值运算符要求左边和右边的运算对象具有相同类型,顺序容器(除array)还定义了一个名为assign的成员,允许我们从一个相容的类型赋值,或者从一个容器的子序列赋值。
由于使用assign的容器的旧元素被替换,因此传递给assign的迭代器不能指向调用assign的容器。(但我测试可以,应该是编译器未报错的错误,最好不要这样):
vector<int> ivec = { 1,2,3 };
ivec.assign(ivec.begin(), ivec.end());
for (int i : ivec) {
cout << i << endl;
}
容器使用assign后,容器的size函数返回值也会随之改变。
swap调用后,两容器中元素会交换:
vector<string> svec1(10);
vector<string> svec1(42);
swap(svec1, svec2);
除array外,swap交换两个容器内容的操作保证会很快,元素本身并未交换,只是交换了两个容器内部的数据结构。
元素并未被移动,除string外,指向容器的迭代器、引用和指针在swap操作后都不会失效,它们仍指向swap操作之前所指向的那些元素,但string的就会失效:
vector<int> ivec1 = { 1,2,3 };
vector<int> ivec2 = { 4,5,6 };
auto b = ivec1.begin(), e = ivec1.end();
swap(ivec1, ivec2);
for (; b != e; ++b) {
cout << *b << endl; // 还是输出123
}
对array来说,swap会真正交换它们的元素。因此迭代器会输出交换后的元素:
array<int, 3> ivec1 = { 1,2,3 };
array<int, 3> ivec2 = { 4,5,6 };
auto b = ivec1.begin(), e = ivec1.end();
swap(ivec1, ivec2);
for (; b != e; ++b) {
cout << *b << endl; // 输出456
}
在C++11新标准中,提供成员函数版和非成员函数版的swap函数,但在旧版本中,只提供成员函数版本的swap函数。非成员版本的swap在泛型编程中很重要,统一使用非成员版本的swap是一个好习惯。
除一个例外,所有容器都含size、empty、max_size函数(返回一个大于等于该类型容器所能容纳的最大元素数的值)。例外是forward_list,它只支持max_size和empty函数,但不支持size函数。
每个容器都支持==和!=,但只有除了无序关联容器外的所有容器才支持>、>=、<、<=。关系运算符两端的运算对象必须是相同类型的容器。容器关系运算符操作与string类似:
1.容器大小相同,且所有元素两两对应相等,则这两个容器相等。
2.若两个容器大小不相等,但一个是另一个的前缀,则较小容器小于较大容器。
3.它们的比较结果取决于第一个不相等元素的比较结果。
容器的关系运算符使用元素的关系运算符完成比较,即只有当元素类型也定义了相应的比较运算符时,我们才能使用关系运算符比较整个容器。
向顺序容器中添加元素(不包括array):
上图中的insert插入多个元素时,如插入迭代器范围内的元素c.insert(p, b, e)
,返回值为迭代器b。
使用时要记得不同容器使用不同策略分配元素空间,这对性能影响很大。如在vector或string的尾部之外的任何位置或一个deque的首尾之外任何位置添加元素,都需要移动元素。并且向vector或string中添加对象可能引起整个对象存储空间的重新分配,并将元素从旧空间移动到新空间中。
除了forward_list和array外,每个顺序容器都能push_back,这个操作会在容器(list、vector、deque)尾部创建新的元素,size+1。
string s;
s.push_back('a'); // 向s后添加字符'a'
用一个对象初始化容器或将对象插入容器,实际上放入容器中的对象是一个拷贝而非对象本身。
list、forward_list、deque容器还支持push_front,将元素插入到容器头部。
deque像vector一样提供了随机访问元素的能力,它还提供了vector不支持的push_front。deque保证在容器首尾进行插入和删除的操作只花费常数时间,与vector一样,在deque首尾之外的位置插入元素很耗时。
insert允许我们在容器中任意位置插入0个或多个元素,vector、deque、list、string都支持insert成员,forward_list支持了特殊版本的insert成员:
slist.insert(iter, "Hello!"); // iter为迭代器,将"Hello!"插入到迭代器iter之前
虽然某些容器不支持push_front操作,但支持insert操作:
vector<string> svec;
svec.insert(svec.begin(), "Hello!"); // 虽然vector不支持push_front函数,但这相当于push_front函数,虽然慢
将元素插入到vector、deque、string中任何位置都合法,不过会很耗时。
插入范围内元素:
svec.insert(svec.end(), 10, "Anna"); // 将10个"Anna"插入到svec末尾
vector<string> v = {"a", "b", "c"};
slist.insert(slist.begin(), v.end() - 2, v.end()); // 将v中最后两个元素插入到slist的最后
slist.insert(slist.end(), {"a", "b"}); // 将元素值列表中所有元素插入到末尾,返回指向元素"a"的迭代器
slist.insert(slist.begin(), slist.begin(), slist.end()); // 运行时错误,迭代器不能指向被添加元素的容器
在C++11新版本中,insert返回指向第一个新加入元素的迭代器,旧版本中会返回void。若新加元素为空,则返回表示插入位置的迭代器参数。
可以使用insert的返回值在容器中特定一个位置反复插入元素:
list<string> lst;
string word;
auto iter = lst.begin();
while (cin >> word) {
iter = lst.insert(iter, word); // 等于调用push_front
}
C++11新标准引入了emplace_front、emplace、emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应于push_front、insert、push_back。
当我们调用emplace时,它的参数被传递给元素类型的构造函数:
c.emplace_back("978-0590353403", 25, 15.99); // 在c的末尾构造一个Sales_data对象
c.push_back("978-0590353403", 25, 15.99); // 错误,没有接受三个元素的push_back版本
c.push_back(Sales_data("978-0590353403", 25, 15.99)); // 正确,创建一个临时的Sales_data对象传给push_back
如上例,emplace会在容器中直接创建一个对象,而最后一个push_back会先创建一个局部临时对象,再将这个局部临时对象压入容器。
c.emplace_back(); // 使用Sales_data的默认构造函数
c.emplace(iter, "999-999999999"); // 在iter前插入一个由Sales_data(string)构造的对象
如果容器中没有元素,则访问元素操作的结果未定义。
每个顺序容器都有front成员函数,除了forward_list外所有顺序容器都有back成员函数,这两个操作分别返回首元素和尾元素的引用:
if (!c.empty()) { // 解引用一个迭代器或调用front、back之前检查是否有元素
auto val = *c.begin(), val2 = c.front(); // val和val2是c中第一个元素值的拷贝
auto last = c.end();
// val3、val4是c中最后一个元素的拷贝
auto val3 = *(--last); // 不适用于forward_list,因为不能递减forward_list的迭代器
auto val4 = c.back(); // forward_list不支持
}
在顺序容器中访问元素的操作:
容器中关于访问元素的成员函数(front、back、at、下标)返回的是引用,如容器是一个const对象,则返回值是const的引用:
vector<double> ivec(3);
ivec.front() = 5;
cout << ivec[0] << endl; // 输出5
提供快速随机访问的容器(string、vector、deque、array)也都提供下标运算符,但下标运算符不提供范围检查,我们可以使用at函数,超过范围会抛出out_of_range异常。
顺序容器删除操作:
删除元素的函数并不检查其参数,程序员要保证它们是存在的。
pop_front和pop_back不返回弹出的元素。
删除一个list中所有奇数:
list<int> lst = {0,1,2,3,4,5,6,7,8,9};
auto it = lst.begin();
while (it != lst.end()) {
if (*it & 1) {
it = lst.erase(it); // 删除it后指向了下一个元素,不用++it了
} else {
++it;
}
}
删除迭代器范围内元素:
elem1 = slist.erase(elem1, elem2); // 调用后elem1 = elem2,删除范围为左闭右开
将int数组内容拷贝到一个vector和一个list容器中,使用单迭代器版本的erase从list中删除奇数元素,从vector中删除偶数元素:
#include <iostream>
#include <vector>
#include <iterator>
#include <list>
using namespace std;
int main() {
int nums[] = { 0,1,2,3,4,5,6,7,8,9 };
int* b = begin(nums), * e = end(nums);
vector<int> ivec(b, e);
list<int> ilst(b, e);
auto vb = ivec.begin();
while (vb != ivec.end()) {
if (*vb & 1) {
++vb;
} else {
vb = ivec.erase(vb); // 由于erase会使vb失效,把删除后返回的指向vb后的迭代器再赋值给vb
}
}
auto lb = ilst.begin();
while (lb != ilst.end()) {
if (*lb & 1) {
lb = ilst.erase(lb);
} else {
++lb;
}
}
for (int i : ivec) {
cout << i << endl;
}
for (int i : ilst) {
cout << i << endl;
}
}
在vector中特定值的元素前插入一个元素:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> ivec = { 0,1,0,1,0,1,0,1,0,1 };
auto b = ivec.begin(), mid = ivec.begin() + ivec.size() / 2;
while (b != mid) {
if (*b == 1) {
b = ivec.insert(b, 2 * 1); // 插入导致插入点后的迭代器失效,其中包括b,应重新赋值b
b += 2;
} else {
++b;
}
mid = ivec.begin() + ivec.size() / 2; // mid也失效了,重新赋值mid
}
for (int i : ivec) {
cout << i << endl;
}
}
forward_list有特殊版本的添加和删除操作,对于一个单向链表来说,删除一个节点的操作对它的前驱节点也有影响,它前驱节点指向下一个节点的指针会指向要删除节点的下一个节点。同理,添加一个节点时也会改变前驱节点的指针值。但我们无法在一个单向链表中直接找到一个节点的前驱节点,因此,在一个forward_list中,添加或删除元素的操作是通过改变给定元素之后的节点来完成的。
因此,forward_list的操作为insert_after、emplace_after、erase_after来代替insert、emplace、erase。为支持这些操作,forward_list也定义了before_begin来获取首前迭代器。
在forward_list中的插入和删除操作:
删除一个forward_list中的偶数:
#include <iostream>
#include <forward_list>
using namespace std;
int main() {
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 & 1) {
++prev;
++curr;
} else {
curr = flst.erase_after(prev); // 删除prev之后的元素(即curr)后,返回curr之后的元素的迭代器,此时,prev还是curr的前驱节点
}
}
for (int i : flst) {
cout << i << endl;
}
}
在forward_list<string>中找到指定字符串,在之后插入一个字符串,如没找到指定字符串,将该串插到单向链表尾:
#include <iostream>
#include <string>
#include <forward_list>
using namespace std;
void func(forward_list<string>& sflst, string toBeFund, string toBeInserted) {
auto prev = sflst.before_begin();
auto curr = sflst.begin();
while (curr != sflst.end()) {
if (*curr == toBeFund) {
sflst.insert_after(curr, toBeInserted);
break;
}
prev = curr;
++curr;
}
if (curr == sflst.end()) { // 由于forward_list迭代器不能自减,但我们设置了prev,就不需要再次遍历列表寻找尾元素的迭代器
sflst.insert_after(prev, toBeInserted);
}
}
int main() {
forward_list<string> sflst = { "aaa", "bbb" };
func(sflst, "aaaa", "ccc");
for (const string& s : sflst) {
cout << s << endl;
}
}
我们可以用resize()增大缩小容器,array不支持此功能。如当前大小比要求的大小大,那么容器后的元素被删除,否则会将新元素添加到容器后部:
list<int> ilst(10,42); // 初始化为10个42
ilst.resize(15); // 将5个0添加到容器后部,未提供新元素初始值,新元素执行值初始化,如是类类型,要么提供新元素初始值,要么存在默认构造函数
ilst.resize(25, -1); // 将10个-1添加到容器后部
ilst.resize(5); // 删除容器后的20个元素
顺序容器大小操作:
向容器中添加和删除元素的操作可能会使指向容器元素的指针、引用、迭代器失效。
在向容器添加元素后:
1.vector、string容器,且存储空间被重新分配,则指向容器的迭代器、指针、引用都会失效;如存储空间未重新分配,指向插入元素之前的迭代器、引用、指针仍有效,但指向插入位置之后的失效。
2.deque容器,插入到除首尾位置之外的任何位置都会导致迭代器、指针、引用失效;如在首尾位置添加,迭代器会失效,但指向已经存在的元素的引用和指针都不会失效。
3.list和forward_list,指向容器的迭代器(包括尾后、首前迭代器)、指针、引用都有效。
容器中删除元素后,指向被删除元素的迭代器、引用、指针失效。删除一个元素后:
1.对于list、forward_list,指向其他位置的迭代器、引用、指针仍有效。
2.deque,如删除首尾之外位置的元素,指向被删除元素之外的其他所有迭代器、引用、指针也会失效;如删除尾元素,则尾后迭代器会失效;删除首元素,其他不受影响。
3.vector、string,指向被删元素之后的迭代器、引用、指针失效(尾后迭代器总会失效)。
向迭代器添加或删除元素后建议每次都重新定位迭代器。
在vector、string中添加或删除元素后,或在deque的首元素之外的位置添加或删除元素后,原来的end会失效。因此不要保存end迭代器,每次循环都要重新定位尾后迭代器,标准库的end函数操作很快,这是部分原因。
为支持快速随机访问,vector和string将元素连续存储。那么再插入元素时就会再开辟一块新的空间以将已有元素从旧位置移到新位置。这样性能会很慢,vector和string采取的策略是当不得不获取新的空间时,会分配比需要的空间更大的空间,预留空间作为备用。
vector和string提供的管理分配内存空间的成员函数:
reserve并不实际改变容器中元素的数量,它仅影响vector预先分配多大的内存空间(即capacity值)。
只有当reserve函数的参数要求的内存空间超过当前容器capacity时,才会改变capacity值,此时reserve函数至少分配与需求一样大的内存空间。如reserve参数值小于等于当前capacity,reserve什么也不做。
reserve函数不会减少capacity,类似地,resize函数只改变容器中的元素数量。
C++11新标准中,我们可以调用shrink_to_fit要求deque、vector、string退回不需要的内存空间。但具体实现可以选择忽略此请求,即调用shrink_to_fit也并不保证会退回空间。
容器的capacity指在不分配新的内存空间的前提下它最多可以保存多少元素,而size指它已经保存的元素数目。
vector<int> ivec;
cout << ivec.size(); // 输出0
cout << ivec.capacity(); // 由具体实现定义
for (vector<int>::size_type ix = 0; ix != 24; ++ix) {
ivec.push_back(ix);
} // 插入24个元素
cout << ivec.size(); // 输出24
cout << ivec.capacity(); // 由具体实现定义,但至少>=24
ivec.reserve(50);
cout << ivec.size(); // 还是24
cout << ivec.capacity(); // 最小50,具体依赖于如何实现
// add elements(0) to use up the excess capacity
while (ivec.size() != ivec.capacity()) {
ivec.push_back(0);
} // 现在size和capacity相同了
只要进行的操作没有超出capacity,vector不允许重新分配空间。
// 接上
ivec.push_back(42); // 再添加一个元素
cout << ivec.size() << endl; // 51
cout << ivec.capacity() << endl; // 大于等于51,我测试时输出了75
ivec.shrink_to_fit(); // 归还超出的capacity到与size相同,但只是一个请求,不一定保证退还
每个vector实现都能选择自己的空间分配策略,但只有当必须分配空间时才会分配新空间。
对于vector,只有当用户执行插入数据的操作且size和capacity相等时,或resize和reserve的参数值大于当前capacity时,才会重新分配空间。
每种vector实现都要求push_back操作效率高,通过调用n次push_back来创建一个n个元素的vector的时间不能超过n的常数倍。
string除了拥有与顺序容器相同的构造函数外(但string没有接受一个表示容器大小的int的构造函数),还支持以下构造方法:
这些函数接受string或const char *参数,当传入string时能通过下标范围指出从哪拷贝:
const char *cp = "Hello World!!!"; // 含15个字符
char noNull[] = {'H', 'i'};
string s1(cp); // 拷贝cp中的字符直到遇到空字符
string s2(noNull, 2); // 从noNull中拷贝出Hi
string s3(noNull); // 未定义
string s4(cp + 6, 5); // 从第六个字符开始拷贝5个字符,即World
string s5(s1, 6, 5); // 从第六个字符开始拷贝5个字符,即World
string s6(s1, 6); // 从第六个字符开始拷贝直到s1末尾
string s7(s1, 6, 20); // 虽然20超过了最大下标,但仍能拷贝,从第6个字符开始,直到结束
string s8(s1, 16); // 虽然行为未定义,但我用的编译器抛出out_of_range错误
substr返回一个string,它是string的部分或全部拷贝:
string s("hello world");
string s2 = s.substr(0, 5); // s2=hello
string s3 = s.substr(6); // s3=world
string s4 = s.substr(6, 110); // s4=world
string s5 = s.substr(12); // 抛出out_of_range异常
假如你需要每次读取一个字符存到string中,且知道最少要存100个字符,高性能的方式应该是一次用reserve函数将string的capacity增加到100以上,可以省去多次重新分配空间的性能消耗。
string支持顺序容器的赋值运算符、assign、insert、erase操作。
作为接受迭代器版本的insert和erase的补充,string还接受以下含下标的版本:
s.insert(s.size(), 5, '!'); // 在s末尾插入5个'!',不支持省略5只插入一个元素,要想这么做,将5改为1
s.erase(s.size() - 5, 5); // 删除s的末尾5个元素
string还提供了接受C风格字符数组的insert和assign版本:
const char *cp = "Stately, plump Buck"; // 以空字符结尾
s.assign(cp, 7); // s=Stately
s.insert(s.size(), cp + 7, 7); // s=Stately, plump,s是在s.size()之前位置插入cp+7指向的数组中的7个值
s.insert(s.size(), cp + 14); // s=Stately, plump Buck,s是在s.size()之前位置插入cp+14指向的数组中的全部元素
string s1 = "some string", s2 = "some other string";
s1.insert(0, s2); // 将s2插入s1位置0之前
s1.insert(0, s2, 0, s2.size()); // 在s[0]之前插入s2中从s2[0]开始的s2.size()个字符
修改string的操作:
string的append和replace操作:
string s("aaa");
s.append("ddd"); // s=aaaddd
s.replace(3, 3, "bbbb"); // 从下标为3的元素开始,替换3个字符为bbbb,最终s=aaabbbb
replace提供了两种指定删除元素范围的方式,可以通过一个位置和一个长度来指定范围,也可以通过一个迭代器范围来指定。
insert允许我们用两种方式来指定插入点,用一个下标或一个迭代器。
指定要添加到string中的字符时,新字符可以来自string、字符指针(指向字符数组的)、花括号包围的字符列表、计数值+字符、字符,字符来自于一个string或字符指针时,我们可以提供一个额外的参数控制拷贝一部分或全部的字符。
对于string类型和C风格字符串:
string s = "aaa";
const char* c = "aaa";
if (s == c) { // true
cout << "相等" << endl;
}
用insert和erase将串s中所有目标串替换为另一个串:
void func(string s, string oldVal, string newVal) {
string part;
int szOfOldVal = oldVal.size();
for (auto b = s.begin(); b < s.end(); ) {
part.assign(b, b + szOfOldVal); // 将part赋值为迭代器b和b+szOfOldVal范围内的值
if (part == oldVal) {
b = s.erase(b, b + szOfOldVal); // 删除s的迭代器b和b+szOfOldVal范围内的值,返回最后一个删除的值的下一个迭代器
b = s.insert(b, newVal.begin(), newVal.end()); // 在s中的位置b前插入后面两个迭代器参数的范围内的值,返回最左边插入的值
b += newVal.size();
continue;
}
++b;
}
cout << s << endl;
}
重写上一题,这次使用下标和replace实现:
void func(string s, string oldVal, string newVal) {
string part;
int szOfOldVal = oldVal.size();
for (int i = 0; i < s.size(); ) { // 因为会改变s的size值,因此循环一次就要获取一次size值
part.assign(s, i, i + szOfOldVal); // 将part赋值为s中下标范围为i和i + szOfOldVal的左闭右开的值
if (part == oldVal) {
s.replace(i, szOfOldVal, newVal); // 将s中从位置i开始的长度为szOfOldVal的内容替换为newVal的值
i += newVal.size(); // 将下标调整到新替换部分的后边
continue;
}
++i;
}
cout << s << endl;
}
用insert和append在人名前加上Mr,人名后加上III:
void func(string s, string pre, string behind) {
s.insert(s.begin(), pre.begin(), pre.end()); // 在s的s.begin迭代器指向的元素前插入后两个迭代器参数的范围内的值
s.append(behind);
cout << s << endl;
}
重写上一题,只使用位置和长度管理string,并只使用insert:
void func(string s, string pre, string behind) {
s.insert(0, pre); // 在s的下标为0的位置前插入pre
s.insert(s.size(), behind); // 在s的下标为s.size(不存在)的位置前插入behind
cout << s << endl;
}
string的搜索函数会返回string::size_type类型的值,表示匹配发生位置的下标,如搜索失败,返回一个const string::size_type类型的static成员,名为string::npos,此值被初始化为-1,而string::size_type是一个unsigned类型,因此返回值是string的最大大小。string搜索函数的返回值不要用int来保存。
unsigned int i = -1;
cout << i << endl; // 输出4294967295
int a = i;
cout << a << endl; // 输出-1
string的搜索操作:
find大小写敏感:
string name(AnnaBelle");
auto pos1 = name.find("Anna"); // 返回0
find_first_of:
string name("r2d2"), numbers("0123456789");
auto pos = name.find_first_of(numbers); // pos=1,name中第一个出现的numbers中的字符是2
find_first_not_of:
string name("rd2"), numbers("0123456789");
auto pos = name.find_first_of(numbers); // pos=0,name中第一个出现的非numbers中的字符是r
通过循环找出串所有的出现过另一字串的位置:
void func(string s, string part) {
string::size_type pos = 0;
while ((pos = s.find(part, pos)) != string::npos) {
cout << pos << endl;
pos += part.size(); // 将pos移动到刚匹配过的字串的后面
}
}
rfind:
string river("Mississippi");
auto pos1 = river.find("is"); // pos1=1
auto pos2 = river.rfind("is"); // pos2=4
auto pos3 = river.rfind("is", 2); // pos3=1,只要匹配串的开头在下标0~pos的范围内即可
string river1("isisisisisi");
cout << river1.rfind("is", 4); // 输出4,从0~4范围内找匹配串的开头
string的compare函数与strcmp函数相似,s.compare()也返回0、正数、负数:
int i = 42;
string s = to_string(i); // 将i转换为字符串
double d = stod(s); // 将字符串s转换为浮点数
要转换为数值的string中第一个非空白符必须是数值中可能出现的字符:
string s2 = "pi = 3.14";
d = stod(s2.substr(s2.find_first_of("+-.0123456789"))); // 从s2的数值部分开始
在将string转换为数字时,第一个非空白的字符必须是+
、-
、.
或数字。string也能以0x或0X开头表示十六进制数。转化为浮点值时,也可以以.开头,并可以包含e或E来表示指数部分。对于转换成整数类型的,根据进制不同,string也可以包含大于9的字母。
int main() {
string s = " 4";
int i = stoi(s); // s前面的空白字符被忽略
cout << i << endl;
}
如要转换的string不能转换为一个数值,则抛出invalid_argument异常,如得到的数字太大以致于无法用任何数值类型表示,抛出out_of_range异常。
string与数值之间的转换:
string s = " 4000s";
size_t pos = 0;
int i = stoi(s, &pos, 10);
cout << i << endl; // 输出4000
cout << pos << endl; // 输出5
三个顺序容器适配器:stack、queue、priority_queue。用处:如stack适配器接受一个顺序容器(除array和forward_list)可以使其操作像stack一样。
所有适配器都支持的操作和类型:
每个适配器都有两个构造函数,一个创建一个空对象,一个接受一个容器并用其拷贝初始化适配器。
stack<int> stk(deq); // 从deq中拷贝元素到stk
默认,stack和queue是基于deque实现的,priority_queue是基于vector实现的。但可以更改:
stack<string, vector<string>> str_stk; // 在vector上实现的空栈
stack<string, vector<string>> str_stk2(svec); // 在vector上实现的栈并用svec初始化
适配器不能构造在array上,因为array不能增删元素。也不能用forward_list构造,因为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操作:
intStack.push(ix); // 只能用push而不能用底层容器的push_back操作
queue和priority_queue操作:
priority_queue按队列中的元素的优先级排列,新加入元素会安排在优先级比它低的元素之前。默认,在元素类型上使用<运算符来确定相对优先级。priority_queue默认相当于最大堆。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)