1、元素在顺序容器中的顺序 与其加入容器时的位置 相对应。区别于 顺序容器,标准库 还定义了 关联容器,关联容器中元素的位置 由元素相关联的关键字值 决定
2、所有容器类 都共享公共的接口,不同容器 按不同的方式 对其进行扩展。每种容器都提供了 不同的性能 和 功能的权衡
3、一个容器就是 一些特定类型对象的集合。顺序容器 提供了 控制元素存储和访问顺序的 能力
1、顺序容器概述
1、所有顺序容器 都提供了 快速访问元素的能力
在以下方面 有不同的性能折中:
1)向容器添加 或 向容器中删除元素的代价
2)非顺序 访问容器中元素的代价
顺序容器类型 | 说明 |
---|---|
vector | 可变大小数组。支持快速随机访问。在尾部之外的位置 插入或删除元素可能很慢 |
deque | 双端队列。支持快速随机访问。在头尾位置插入 / 删除速度很快 |
list | 双向链表。只支持 双向顺序访问。在list中任何位置 进行插入/删除操作速度都很快 |
forward_list | 单向链表。只支持 单向顺序访问。在链表任何位置 进行插入 / 删除操作速度都很快 |
array | 固定大小数组。支持 快速随机访问。不能添加或删除元素 |
string | 与vector相似的容器,但专门用于 保存字符。随机访问快。在尾部插入 / 删除速度快 |
2、除了 固定大小的array外,其他容器 都提供高效、灵活的内存管理。可以 添加和删除元素,扩张和收缩容器的大小
例如:string和vector 将元素保存在 连续的内存空间中,所以 由元素的下标 来计算其地址 是非常快速的。但这两种容器的 除尾部位置 添加或删除元素 会非常耗时:一次插入或删除后,需要 移动插入/删除位置之后的所有元素,保持连续存储
添加一个元素 有时需要 分配额外的存储空间,每个元素都必须 移动到新的存储空间中
3、list和forward_list 设计目的是 令容器任何位置的 添加和删除操作 都很快速。这两个容器 不支持元素的随机访问:为了访问一个元素,只能遍历整个容器。与 vector、deque和array比,这两个容器的额外内存开销 也很大
4、deque支持快速随机访问,在中间位置 添加或删除元素的代价很高,但在deque的两端 添加或删除元素 是很快的,与list 或 forward_list添加删除元素的速度相当
5、forward_list和array是 新C++标准增加的类型
与内置数组相比,array是一种更安全、更容易使用的数组类型。与内置数组类似,array对象的大小 是固定的,因此 array不支持 添加或删除元素 以及 改变元素大小的操作
forward_list的设计目标是 达到与最好的手写的 单向链表数据结构相当的性能。因此,forward_list没有size操作,因为 保存或计算其大小 就会比手写链表 多出额外的开销。对其他容器而言,size保证是 一个快速的常量时间的操作
新标准库容器的性能 几乎和最精心优化过的同类数据结构一样好
6、确定使用哪种顺序容器:
1)除非 有很好的理由 选择其他容器,否则应该使用vector
2)有很多小的元素,且 空间的额外开销很重要,则 不要使用 list 或 forward_list
3)要求随机访问元素,应使用 vector或deque
4)要求在容器中间插入或删除元素,但不会 在中间位置 进行插入或删除操作,则使用deque
5)只有在读取输入时 才需要在容器中间位置插入元素,随后 需要随机访问元素,则
确定是否真的需要在容器中间位置 插入元素。当处理输入数据时,很容易向vector追加数据,然后 再调用标准库的sort函数 来重排容器中的元素,从而 避免在中间位置添加元素
必须在中间位置 插入元素,考虑 在输入阶段使用list,一旦输入完成,将list中的内容 拷贝到一个vector中
程序既需要 随机访问元素,又需要 在容器中间位置插入元素:取决于 在list 或 forward_list中访问元素 与vector或deque中插入/删除元素的相对性能
不确定应该使用哪种容器,可以在程序中只使用 vector和list公共的操作:使用迭代器,不使用下标操作,避免随机访问。在必要时选择 使用vector或list都很方便
2、容器库概览
1、容器上的操作:
1)所有容器类型都提供的:下面第一张表
2)仅针对顺序容器、关联容器 或 无序容器:下面第二张表
3)只适用于 一小部分容器
2、每个容器都定义在 一个头文件中,文件名与类型名相同(deque定义在头文件deque中)。容器均定义为 模板类,对vector,必须提供额外信息 来生成特定的容器类型。对大多数,还需要 额外提供元素类型信息:list<Sales_data>
3、顺序容器 几乎可以保存 任意类型的元素。可以定义 一个容器,其元素的类型 是另一个容器:vector<vector<string>> lines
4、顺序容器 构造函数的一个版本接受容器大小参数。但 某些类没有默认构造函数。可以定义一个保存 这种类型对象的容器,但 在构造这种容器时 不能只传递给它 一个元素数目参数:
// 假定noDefault是一个 没有默认构造函数的类型
vector<noDefault> v1(10, init); // 正确:提供了元素初始化器
vector<noDefault> v2(10); // 错误:必须提供一个元素初始化器
#include <iostream>
#include <vector>
// 假设 noDefault 是一个没有默认构造函数的类型
class noDefault {
public:
noDefault(int value) : data(value) {}
// 假设该类还有其他成员和方法
private:
int data;
};
int main() {
// 假设 init 是一个 noDefault 类型的对象
noDefault init(100);
// 创建包含 10 个元素的 vector,每个元素都使用 init 进行初始化
std::vector<noDefault> v1(10, init);
// 输出 vector 中的元素
for (const auto& element : v1) {
// 假设 noDefault 类型有一个成员函数 getData() 来获取 data 的值
std::cout << element.getData() << " ";
}
std::cout << std::endl;
return 0;
}
5、所有容器类型 都提供的:
类型别名:
类型别名 | 解释 |
---|---|
iterator | 此容器类型的迭代器类型 |
const_iterator | 可以读取元素,但不能修改元素的 迭代器类型 |
size_type | 无符号整数类型,足够保存此种容器类型 最大可能容器的大小,存着容器的大小 |
difference_type | 带符号整数类型,足够保存 两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型;与value_type& 含义相同 |
const_reference | 元素的const左值类型(const value_type&) |
构造函数:
构造函数 | 解释 |
---|---|
C c; | 默认构造函数,构造空容器 |
C c1(c2); | 构造c2的拷贝c1 |
C c(b, e); | 构造c,将迭代器b和e指定的范围内的 元素拷贝到c(array不支持) |
C c{a, b, c …}; | 列表初始化c |
赋值与swap:
赋值与swap | 解释 |
---|---|
c1 = c2 | 将c1中的元素 替换为 c2中元素 |
c1 = {a, b, c…} | 将c1中的元素 替换为 列表中的元素(不适用于 array) |
a.swap(b) | 交换a和b的元素 |
swap(a, b) | 与a.swap(b)等价 |
大小:
大小 | 解释 |
---|---|
c.size() | c中元素的数目(不支持forward_list) |
c.max_size() | c可保存的最大元素数目 |
c.empty() | 若c中存储了元素,返回false |
添加 / 删除元素(不适用于array)在不同容器中,操作的接口不同
添加删除元素 | 解释 |
---|---|
c.insert(args) | 将args中的元素拷贝进c |
c.emplace(inits) | 使用inits构造c中的一个元素 |
c.erase(args) | 删除args指定的元素 |
c.clear() | 删除c中的所有元素,返回void |
MyClass 类有一个带有两个参数的构造函数,用于初始化 id_ 和 name_ 成员变量。在 main 函数中,我们使用 emplace 在指定位置插入元素。第一个 emplace 将一个新元素插入到 list 的开头,第二个 emplace 将一个新元素插入到 给定迭代器位置之前,最后一个 emplace 将一个新元素插入到 list 的末尾
当你包含 <string>
头文件时,std::move 已经被包含在其中
#include <iostream>
#include <list>
#include <string>
class MyClass {
public:
MyClass(int id, std::string name) : id_(id), name_(std::move(name)) {}
void display() const {
std::cout << "ID: " << id_ << ", Name: " << name_ << std::endl;
}
private:
int id_;
std::string name_;
};
int main() {
std::list<MyClass> c;
// 使用 emplace 在指定位置插入元素
auto it = c.emplace(c.begin(), 1, "First");
c.emplace(it, 2, "Second");
c.emplace(c.end(), 3, "Third");
// 输出 list 中的元素
for (const auto& element : c) {
element.display();
}
return 0;
}
std::move 是 C++ 中的一个函数模板,用于将给定的对象转换为 右值引用。右值引用 是 C++11 引入的一种新的引用类型,用于表示 可以被移动的对象。通过使用 std::move,你可以将一个左值(例如,具名的对象)转换为 右值引用,从而允许对其 进行移动操作而不是拷贝操作
#include <utility>
// 假设有一个对象 obj
Type obj;
// 将 obj 转换为右值引用,并将结果赋值给变量 new_obj
Type&& new_obj = std::move(obj);
Type&& 表示右值引用类型。右值引用允许你绑定到临时对象或将要被移动的对象上。在 C++11 中,右值引用是为了支持移动语义而引入的一种新的引用类型
当编写 Type&& new_obj 时,声明了一个变量 new_obj,它是一个右值引用,类型为 Type。这意味着你可以将 new_obj 绑定到右值(例如,临时对象或将要被移动的对象)上
std::move(obj) 返回一个右值引用,因此将其赋给 new_obj 就会使 new_obj 成为 obj 的右值引用。这样做的目的是为了告诉编译器你希望将 obj 的资源移动到 new_obj 中,而不是拷贝
关系运算符:
关系运算符 | 解释 |
---|---|
==,!= | 所有容器 都支持相等(不等)运算符 |
<. <=, >, >= | 关系运算符(无序关联运算符不支持) |
获取迭代器:
获取迭代器 | 解释 |
---|---|
c.begin(), c.end() | 返回指向c的首元素和尾元素之后位置 的迭代器 |
c.cbegin(), c.cend() | 返回const_iterator |
反向容器的额外成员(不支持forward_list)
反向容器的额外成员 | 解释 |
---|---|
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不能修改元素的逆序迭代器 |
c.rbegin(), c.rend() | 返回指向c的尾元素和首元素之前位置的迭代器 |
c.crbegin(), c.crend() | 返回const_reverse_iterator |
2.1 迭代器
1、迭代器有 公共的接口:一个迭代器 提供某个操作,所有提供相同操作的迭代器 对这个操作的实现方式都是 相同的。所有迭代器 都定义了 递增运算符,从当前元素 移动到 下一个元素
2、4.1 使用迭代器 3 迭代器运算符 表格 列出了 容器迭代器 支持的所有操作,forward_list 不支持递减(- -)运算符
4.1 使用迭代器 12 迭代器运算 表格 列出了 迭代器支持的算术运算符,这些运算只能应用于 string、vector、deque 和 array 的迭代器
3、迭代器范围:由一对迭代器表示,两个迭代器 分别指向同一个容器中的元素 或者 是尾元素之后的位置,标记了 容器中元素的一个范围,包含从first开始 直至 last(但不包含last)之间的所有元素(左闭合区间 [begin, end))
4、对构成范围的迭代器的要求:迭代器begin和end必须指向相同的容器。end可以与begin指向相同的位置,但不能指向begin之前的位置
5、左闭右合范围 有三种性质:
1)begin和end相等,则 范围为空
2)begin和end不等,则 范围至少包含一个元素
3)可以对begin递增若干次,使得 begin == end
可以用一个循环 来处理一个元素范围:
while (begin != end) {
*begin = val; // begin指向一个元素
++begin; // 移动迭代器 获取下一个元素
}
6、编写函数,接受一对指向vector的迭代器和一个int值。在两个迭代器指定的范围中查找给定的值,返回一个布尔值来指出是否找到
#include <vector>
#include <iterator>
#include <iostream>
using namespace std;
bool findNum(vector<int>::const_iterator b, vector<int>::const_iterator e, int n) {
while (b != e) {
if (*b == n) return true;
b++;
}
return false;
}
int main()
{
vector<int> t = { 1,2,3,4,5,6 };
cout << boolalpha << findNum(t.begin(), t.end(), 0) << endl;
// begin和end都要加括号,加了boolalpha就能显示true/false而不是1/0
return 0;
}
7、下面的程序有何错误
list<int> lst1;
list<int>::iterator iter1 = lst1.begin(),
iter2 = lst1.end();
while (iter1 < iter2) /* ... */
与 vector 和 deque 不同,list 的迭代器不支持 < 运算,只支持递增、递减、== 以及 != 运算
原因在于 这几种数据结构实现上的不同。vector 和 deque 将元素 在内存中连续保存,而 list 则是 将元素以链表方式存储,因此前者可以方便地实现 迭代器的大小比较(类似指针的大小比较)来体现 元素的前后关系。而在 list 中,两个指针的大小关系 与它们指向的元素的前后关系 并不一定是吻合的,实现 < 运算将会非常困难和低效
2.2 容器类型成员
1、反向迭代器 就是一种 反向遍历容器的迭代器,各种操作的含义 也发生了 颠倒:对一个 反向迭代器 执行++操作,会得到上一个元素
// iter是通过 list<string> 定义的一个迭代器类型
list<string>::iterator iter;
// count是通过 vector<int> 定义的一个 difference_type 类型
vector<int>::difference_type count;
这些声明语句 使用了作用域运算符,说明希望使用 list<string>
类的iterator成员 及vector<int>
类定义的 difference_type
如果 写入list,需要非常量引用类型:list<string>::reference
#include <iostream>
#include <list>
#include <string>
int main() {
std::list<std::string> mylist = {"apple", "banana", "cherry"};
// 修改list中的元素
std::list<std::string>::reference third_element = mylist.back(); // 获取对最后一个元素的引用
third_element = "grape"; // 使用引用修改元素值
// 输出修改后的list
for (const auto& element : mylist) {
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
2.3 begin 和 end成员
1、begin和end的多个版本:带r的版本 返回反向迭代器;以c开头的版本 返回const迭代器
list<string> a = {"aaa", "bbb", "cc"};
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开头的函数(begin(), end(), rbegin(), rend()…)都是被重载过的。实际上 有两个名为begin的成员。一个是const成员,返回容器的const_iterator类型;另一个是 非常量成员,返回容器的 iterator类型
对一个非常量对象 调用 这些成员时,得到的是 返回 iterator 的版本;只有在对一个const对象 调用这些函数时,才会得到一个const版本
与const指针 和 引用类似,可以将 一个普通的iterator 转换为对应的const_iterator,但反之不行
以c开头的版本 是C++11引入的,用以支持auto 与begin和end函数结合使用
auto it7 = a.begin(); // 仅当a是const时,it7是const_iterator
// 获得的迭代器类型 依赖于 容器类型,与想要如何使用迭代器不相干
auto it8 = a.cbegin(); // it8是const_iterator
// 以c开头的版本 还是可以获得const_iterator的,不管容器的类型是什么
2.4 容器定义和初始化
1、每个容器类型 都定义了一个默认构造函数。除array外,其他容器的默认构造函数 都会创建一个指定类型的空容器,都可以 接受指定容器大小 和 元素的初始值
2、容器的定义 和 初始化:
定义和初始化 | 解释 |
---|---|
C c; | 默认构造函数。如果C是一个array,则c中元素按默认方式初始化;否则c为空 |
C c1(c2); | c1初始化为c2的拷贝。c1和c2必须是相同类型(相同的容器类型+相同的元素类型,对于array类型,加相同的大小) |
C c={a, b, c…} | 元素类型相容。对于array类型,列表中的元素的类型 必须与C的元素类型 相容。对于array类型,列表中元素数目 必须小于等于array大小,任何遗漏的元素 都进行值初始化 |
C c(b, e) | c初始化为 迭代器b和e指定范围中的元素的拷贝。范围中元素的类型 必须与C的元素类型相容(array不适用) |
只有 顺序容器(不包括array)的构造函数 才能接受大小参数
初始化 | 解释 |
---|---|
C seq(n) | seq包含n个元素,这些元素进行了 值初始化;此构造函数是explicit的(string不适用) |
C seq(n, t) | seq包含n个初始化为值t的元素 |
附:explicit
在C++中,当一个构造函数被声明为 explicit 时,它表明该构造函数不会 被用于隐式转换 或 复制初始化。这意味着只有在显式地调用该构造函数时才会被使用
MyClass obj2(5); // 正确,使用显式调用构造函数
MyClass obj = 5; // 错误,不能进行隐式转换
// 这里会尝试将整数值 5 隐式转换为 MyClass 类型对象,但是由于构造函数是 explicit 的,所以会导致编译错误
隐式转换 是指在 不显式调用转换函数的情况下,由编译器 自动执行的类型转换。这种类型转换 会在一些情况下发生,例如在表达式中 使用不同类型的操作数,或者 在函数参数传递中
explicit 构造函数的目的是 阻止类类型对象从其它类型隐式转换为该类类型对象。当构造函数被声明为 explicit 时,编译器将不会自动调用该构造函数 来进行隐式转换
类类型对象从其它类型隐式转换为该类类型对象 指的是在不显式调用构造函数的情况下,由编译器自动执行的类型转换。在 C++ 中,这种转换通常发生在以下几种情况下:
1)单参数构造函数:如果类中定义了一个 带有单个参数的构造函数,那么当用这个参数类型的对象 对类对象进行初始化时,编译器会 自动调用该构造函数 来进行转换
class MyClass {
public:
MyClass(int x) : value(x) {}
private:
int value;
};
MyClass obj = 5; // 这里会自动调用 MyClass 的构造函数进行转换
2、函数参数传递:当一个函数参数的类型与所需类型不匹配时,编译器会尝试执行隐式转换以匹配函数参数的类型
void func(MyClass obj) {
// 函数体
}
func(5); // 这里会尝试将整数值 5 隐式转换为 MyClass 类型对象,并传递给 func 函数
3、将一个容器初始化为 另一个容器的拷贝:方法有两种:直接拷贝整个容器;(除array)拷贝由一个迭代器 对指定元素的范围
拷贝的两个容器的类型 及其元素类型 必须匹配
当 传递迭代器参数 来拷贝一个范围时,就不要求 容器类型是相同的了。新容器 和 原容器中的元素类型 可以不同,只要能将 要拷贝的元素转换为 要初始化的容器的元素类型 即可
list<string> authors = {"mmm", "sss", "aa"};
vector<const char*> articles = {"a", "an", "the"};
vector<string> words(articles); // 错误:容器类型必须匹配
// 正确:可以将 const char* 元素转换为 string
forward_list<string> words(articles.begin(), articles.end());
两个迭代器 分别标记 想要拷贝的第一个元素 和 尾后元素的位置。新容器的大小和范围中元素的数目相同。新容器中的每个元素 都用范围中 对应元素的值 进行初始化
附:关于 const char* 和 string
const char* 和 string 都是用于表示字符串的 C++ 数据类型,但它们有一些重要的区别:
1)内存管理:
const char* 是一个 指向以 null 结尾的字符数组的指针,通常称为 C 风格字符串或者字符指针。它需要 手动管理内存,包括分配和释放内存。如果使用 动态分配的内存,则需要使用 new 和 delete 运算符,或者使用 malloc() 和 free() 函数
string 是 C++ 标准库提供的字符串类,它封装了一个 动态分配的字符数组,并提供了一系列的方法来方便地操作字符串。在使用 string 类时,内存管理是自动的,不需要手动管理内存
2)长度限制:
const char* 的长度没有明确的限制,它可以指向任意长度的字符串
string 的长度可以根据需要动态调整,因此它没有固定的长度限制
3)使用方式:
const char* 通常用于与 C 语言 API 进行交互,或者在需要对字符串进行底层内存操作时使用
string 更适合在 C++ 代码中使用,它提供了更多的字符串操作方法,而且使用起来更加方便和安全
4)可变性:
const char* 指向的字符串是不可变的,一旦创建就不能修改它的内容
string 是可变的,你可以随意修改它的内容
虽然 string 对象不是一个地址。string 是 C++ 标准库提供的字符串类,用于表示和操作字符串。当创建一个 string 对象时,会在堆或栈上分配内存来存储该对象的数据。这个内存包含了字符串的字符数组以及一些额外的数据,如字符串的长度、容量等信息
const char* 是一个指向字符的指针,但它在C++中用于表示以null结尾的C风格字符串。当你用const char*指针指向一个以null结尾的字符数组时,C++标准库中的输出流函数会将它解释为一个以null结尾的字符串,因此会输出字符串的内容而不是其地址
#include <iostream>
int main() {
const char* str = "Hello, world!";
std::cout << str << std::endl; // 输出字符串内容
return 0;
}
str 是一个指向以null结尾的字符数组的指针,它指向字符串 “Hello, world!” 的首字符。当你将它传递给 std::cout 输出流时,std::cout 会将其解释为一个字符串,并输出其内容。
这种行为是因为 C++ 的标准输出流类 std::ostream(包括 std::cout)对 const char* 类型的参数进行了重载,以便能够直接输出以null结尾的字符串,而不是输出其地址
但要注意:std::cout 不会检查指针是否指向有效的字符串,如果你传递一个不以null结尾的字符数组,它可能会导致未定义的行为。因此,在实际使用中,请确保 const char* 指向一个以null结尾的有效字符串
4、列表初始化:对除array之外的容器类型,列表初始化 还隐含地指定了 容器的大小:vector<const char*> articles = {"a", "an", "the"};
5、与顺序容器大小相关的构造函数:顺序容器(array除外)还提供 另一个构造函数,接受一个 容器大小 和 一个(可选的)元素初始值。如果 不提供元素初始值,标准库 会创建一个 值初始化器
list<string> svec(10, "hi!"); // 10个string;每个都初始化为“hi"
forward_list<int> ivec(10); // 10个元素,每个都初始化为0
如果 元素类型是内置类型 或者是 有默认构造函数的类类型,可以 只为构造函数 提供一个容器大小参数。如果 元素类型 没有默认构造函数,除了 大小参数 外,还必须 指定一个显式的元素初始值
只有 顺序容器的构造函数 才接受大小参数,关联容器 不支持
6、标准库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类型的一部分,array不支持 普通的容器构造函数。同时,对于构造函数的行为:一个默认构造的array 是非空的:它包含了 与其大小一样多的元素。这些元素 都被默认初始化(内置数组一样)
对array进行表初始化,如果 初始值数目小于array的大小,则 它们用来被初始化array中 靠前的元素,所有 剩余元素都会被 值初始化,如果 元素类型是 一个类类型,那么 该类必须有一个默认构造函数,以使 值初始化能够进行:
array<int, 10> ia3 = {42}; // ia3[0] 为42, 剩余元素为0
不能对 内置数组进行拷贝 或 对象赋值操作,array并无此限制
int digs[2] = {0, 1};
int cpy[2] = digs; // 错误,内置数组不支持 拷贝或赋值
array<int, 2> digits = {0};
array<int, 2> copy = digits; // 正确:只要数组类型匹配(元素类型+数量)即合法
7、如何从一个list初始化一个vector?从一个vector又该如何创建
#include <vector>
#include <iostream>
#include <list>
#include <iterator>
using namespace std;
int main()
{
list<int> l(5, 1);
vector<double> vec(l.begin(), l.end());
for (double e : vec) cout << e << " ";
cout << endl;
vector<int> v(5, 1);
vector<double> vec2(l.begin(), l.end());
// 不能多次初始化 vector<double> vec(l.begin(), l.end());错误
for (double e : vec2) cout << e << " ";
return 0;
}
2.5 赋值和swap
1、赋值运算符 将其左边容器中的 全部元素替换为 右边容器中元素的拷贝:
c1 = c2; // 将c1的内容替换为c2中的元素的拷贝
c1 = {a, b, c}; // 赋值后,c1的大小为3
如果 两个容器原来大小不同,赋值运算后 两者的大小 都与 右边容器的原大小相同
与内置数组不同,标准库array类型 允许赋值。赋值号左右两边的运算对象 必须具有相同的类型(所以array类型不支持assign):
array<int, 10> a = {0}; // 所有元素均为0
a2 = {0, 1, 2};
// 用花括号包围的值列表进行赋值 可以了现在,a2的数值是 {0, 1, 2, 0, 0, 0, 0, 0, 0, 0}
// 初始化
array<double, 10> arr1 = { 0, 1, 2 };
array<int, 10> arr2 = arr1; // 报错,类型不匹配
// 赋值
array<double, 10> arr1 = { 0, 1, 2 };
array<int, 10> arr2 = { 0, 1, 2, 3, 4, 5, 6, 7 };
arr2 = arr1; // 报错,类型不匹配
2、容器的赋值运算(可用于 所有容器)
容器赋值 | 解释 |
---|---|
c1=c2 | 将c1中的元素 替换为c2中元素的拷贝。c1和c2必须有相同的类型 |
c={a, b, c…} | 将c1中元素 替换为 初始化列表中元素的拷贝 |
swap(c1, c2) | 交换c1和c2中的元素。c1和c2必须有相同的类型。swap通常比从c2向c1拷贝元素快得多 |
assign操作不适用于关联容器和array
容器赋值 | 解释 |
---|---|
seq.assign(b, e) | 将seq中的元素 替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素 |
seq.assign(i1) | 将seq中的元素 替换为 初始化列表i1中的元素 |
seq.assign(n, t) | 将seq中的元素 替换为n个值为t的元素 |
赋值相关的运算 会导致 左边容器内部的迭代器、引用 和 指针失效。而swap操作 将容器内容交换 不会导致指向容器的迭代器、引用和指针失效
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = {4, 5, 6};
// 获取vec1的第一个元素的迭代器
auto it = vec1.begin();
// 对vec1进行赋值操作
vec1 = vec2;
// 尝试通过之前获取的迭代器访问元素
std::cout << *it << std::endl; // 这里可能会导致未定义的行为,因为it已经失效了
return 0;
}
对 vec1 进行赋值操作,将其赋值为 vec2。由于赋值操作可能会重新分配内存或修改容器的内部结构,导致之前保存的迭代器 it 不再有效。因此,尝试通过 it 来访问元素可能会导致未定义的行为
类似地,引用和指针也会受到相同的影响。在赋值操作后,之前指向左边容器的引用和指针也会失效,因为它们指向的是被修改或销毁的容器。因此,在进行赋值操作后,需要注意重新初始化或重新获取迭代器、引用和指针,以确保它们仍然指向有效的容器元素
3、使用assign(仅顺序容器):顺序容器(array除外)还定义了一个名为 assign的成员,允许 从一个不同但相容(跟=不一样)的类型赋值,或者从容器的一个子序列 赋值。assign操作 用参数所指定的元素(的拷贝)替换左边容器中 的所有元素
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; // 错误,容器类型不匹配(需要容器与元素类型双匹配,类型相容也不行)
vector<double> arr1 = { 0, 1, 2 };
vector<int> arr2 = { 0, 1, 2, 3, 4, 5, 6, 7 };
arr2 = arr1; // 错误,容器类型不匹配
// 正确:可以将 const char* 转换为 string
names.assign(oldstyle.cbegin(), oldstyle.cend());
由于 其旧元素被替换,因此 传递给assign的迭代器 不能指向调用assign的容器
assign的第二个版本 接受一个整型值和一个元素值。它用 指定数目 且具有相同给定值的元素 替换容器中原有的元素
// 等价于 slist1.clear()
// 后跟 slist1.insert(slist1.begin(), 10, "Hiya");
list<string> slist(1); // 1个元素为 空string
slist1.assign(10, "Hiya!"); // 10个元素,每个都是“Hiya!”
4、使用swap:swap操作 交换两个 相同类型容器(容器+元素类型)的内容
vector<string> svec1(10); // 10个元素的vector
vector<string> svec2(24); // 24个元素的vector
swap(svec1, svec2);
调用swap后,svec1将包含24个string元素,svec2将包含10个string
除array外,交换两个容器内容的操作 保证会很快——元素本身并未交换,swap只是交换了 两个容器的内部数据结构
除array外,swap不对任何元素 进行拷贝、删除 或 插入操作,因此 可以保证在 常数时间内完成
元素不会被移动,所以 除string外,指向容器的迭代器、引用 和 指针 在swap操作之后 都不会失效:仍指向swap操作之前 所指向的那些元素,尽管 在swap之后,这些元素已经属于 不同的容器了
例如:假定iter在swap之前 指向svec[3] 的string,那么在swap之后 它指向svec2[3]的元素
与其他容器不同,对一个string调用swap会导致 迭代器、引用和指针失效
与其他容器不同,swap两个array会真正交换 它们的元素,因此 交换两个array所需的时间 与array中元素的数目 成正比。对于 array,在swap操作之后,指针、引用 和 迭代器 所绑定的元素 保持不变,但元素值 已经和另一个array中的对应元素的值 进行了交换(也就是指针、引用和迭代器指的值 改变了,其他容器的指针、引用和迭代器指的值 是不改变的)但是如果遍历同一个名字 array和其他容器一样 都互换了
array的 指针、引用 和 迭代器 所绑定的元素,遍历同一个名字:
#include <iostream>
#include <array>
using namespace std;
int main() {
array<int, 10> arr1 = { 0, 1, 2 };
int* fir = &arr1[0];
cout << *fir << endl; // 为0
array<int, 10> arr2 = { -1, -1, -2, -3, -4, -5, -6, -7 };
arr1.swap(arr2);
cout << *fir << endl; // 为-1
for (int i : arr2) cout << i << " "; // 为 0 1 2 0 0 0 0 0 0 0
return 0;
}
其他容器的 指针、引用 和 迭代器 所绑定的元素,遍历同一个名字:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> vec1 = { 0, 1, 2 };
int* fir = &vec1[0];
cout << *fir << endl; // 为0
vector<int> vec2 = { -1, -1, -2, -3, -4, -5, -6, -7 };
vec1.swap(vec2);
cout << *fir << endl; // 为0
for (int i : vec2) cout << i << " "; // 为0 1 2
return 0;
}
5、统一使用 非成员版本的 swap是一个好习惯
6、编写程序,将一个list中的char * 指针元素赋值给一个vector中的string
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main() {
list<const char*> slist = { "hello", "world", "!" }; // list<char*>不行
vector<string> svec;
char c1[6] = "hello";// 另一种赋值方法
char c2[7] = "C++";
char c3[2] = "!";
list<char*> l1 = { c1,c2,c3 };
// 容器类型不同,不能直接赋值
// svec = slist;
// 元素类型相容,可以采用范围赋值
svec.assign(slist.begin(), slist.end());
for (string s : svec) cout << s << " ";
cout << endl;
svec.assign(l1.begin(), l1.end());
for (string s : svec) cout << s.size() << " " << s << " ";// c2变成string后,大小变为3
cout << endl;
return 0;
}
附:使用字符串 初始化 C风格字符串的长度
在上面的代码中,c2 作为一个C风格的字符串,它的长度是7(包括结尾的空字符 \0)
但是当你将其赋值给 string 类型的元素时,string 类会根据以null结尾的字符串构造函数来创建一个 string 对象,这个构造函数会从 c2 的地址开始直到遇到 null 字符为止,而不是根据数组的大小
因此,string 对象中只会包含 “C++” 这个字符串,它的长度是3,而不是7
这也说明了C++标准库中的 string 类是基于以null结尾的字符串来构造的,并且会自动计算字符串的长度
因此,当你将一个C风格的字符串赋值给 string 对象时,只会拷贝字符串内容,而不会拷贝整个字符数组
2.6 容器大小操作
除了一个例外,每个容器类型 都有三个 与大小有关的操作
成员函数size返回 容器中元素的数目;empty 当size为0时 返回布尔值true;max_size返回一个 大于或等于 该类型容器 所能容纳的最大元素数的值(只跟容器类型 有关)如:在上面的代码中加入 cout << l1.max_size() << " " << slist.max_size(); // 这两个值是一样的
forward_list 支持 max_size和empty,但不支持 size
2.7 关系运算符
1、每个容器类型 都支持相等运算符(==和!=);除了 无序关联容器外的 所有容器都支持 关系运算符(>、>=、<、<=)。关系运算符 左右两边的运算对象 必须是 相同类型的容器,且 必须保存 相同类型的元素:不能将一个vector<int>
与一个list<int>
或一个vector<double>
进行比较
比较 两个容器实际上 是进行 元素的逐对比较(与string的关系运算类似)
1)如果 两个容器具有 相同大小 且 所有元素都两辆对应相等,则 这两个元素相等
2)如果 两个容器大小不同,但 较小容器中 每个元素都等于 较大容器中的对应元素,则 较小容器 小于 较大容器
3)如果 两个容器都不是 另一个容器的前缀子序列,则 它们的比较结果 取决于 第一个不相等的元素的比较结果
vector<int> v1 = {1, 3, 5, 7, 9, 12};
vector<int> v2 = {1, 3, 9};
v1 < v2
2、容器的关系运算符 使用元素的关系运算符 完成比较:
只有 当其元素类型 也定义了 相应的比较运算符时,才可以 使用关系运算符 来比较两个容器
容器的相等运算符 实际上是 使用元素的==
运算符实现比较的,而 其他关系运算符 是使用元素的<运算符
如果 元素类型不支持所需 运算符,保存 这种元素的容器 就不能使用相应的关系运算
在第七章定义的 Sales_data类型 并未定义==
和<运算。就不能 比较两个保存 Sales_data元素的容器
3、假定c1 和 c2 是两个容器,下面的比较操作有何限制:if (c1 < c2)
c1和c2不能是无序容器,且容器类型要相同,最后,元素类型要支持运算符
3、顺序容器操作
剩余部分将介绍顺序容器所特有的操作
3.1 向顺序容器添加元素
1、除array外,所有标准库容器都提供灵活的内存管理,在运行时可以动态添加或删除元素来改变容器大小
2、向顺序容器(非array)添加元素的操作,都在前面插入,insert都返回 新插入第一个元素 的迭代器
这些操作会改变容器的大小;array 不支持这些操作
forward_list有自己专有版本的insert 和 emplace(下面会说)
forward_list不支持push_back 和 emplace_back(往后到最后走代价高)
vector和string不支持push_front 和 emplace_front(连续存储的)
函数 | 作用 |
---|---|
c.push_back(t),c.emplace_back(args) | 在c的尾部创建一个值为t或由args创建的元素。返回void |
c.push_front(t),c.emplace_front(args) | 在c的头部创建一个值为t或由args创建的元素。返回void |
c.insert(p,t),c.emplace(p, args) | 在选代器p指向的元素 之前 创建一个值为t 或 由args创建的元素。返回 指向新添加的元素的送代器 |
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(除了链表)插入元素会使 所有指向容器的选代器引用和指针失效
3、不同容器使用不同的策略 来分配元素空间,而这些策略直接影响性能。在一个vector或string的尾部 之外的任何位置,或是一个deque的首尾之外 的任何位置添加元素,都需要移动元素
向一个vector或string(连续存储的)添加元素 可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间 需要分配新的内存,并将元素 从旧的空间移动到新的空间中
4、使用push_back:除array和forward_list之外,每个顺序容器(包括string类型)都支持push_back
//从标准输入读取数据,将每个单词放到容器末尾
string word;
while(cin>>word)
container.push_back(word)
对push_back的调用 在container尾部创建了一个新的元素,将container的size增大了1。该元素的值为word的一个 拷贝。container的类型 可以是list、vector 或 deque
5、关键概念:容器元素是拷贝
用一个对象来 初始化容器时,或将一个对象 插入到容器中时,实际上放入到容器中的是 对象值的一个拷贝,而不是 对象本身
就像 将一个对象传递给非引用参数一样,容器中的元素 与 提供值的对象 之间没有任何关联,随后对容器中元素的任何改变都不会影响到原始对象,反之亦然
6、使用push_front:除了push_back,list、forward_list和deque容器(不连续存储的) 还支持名为push_front的类似操作。此操作 将元素插入到容器头部:
list<int> ilist;
//将元素添加到ilist开头
for(size_t ix=0; ix != 4; ++ix)
ilist.push_front(ix); //ilist保存序列3、2、1、0
deque像vector一样提供了 随机访问元素的能力,但它提供了vector所不支持的push_front
7、在容器中的特定位置添加元素:push_back 和 push_front操作 提供了一种方便地在顺序容器尾部 或 头部插入 单个 元素的方法。insert成员 在容器中任意位置插入0个或多个元素
vector、deque、lis和string都支持insert成员。forward_list提供了特殊版本的insert成员
每个insert函数都接受一个送代器作为其第一个参数,由于迭代器 可能指向容器尾部之后 不存在的元素的位置,而且在容器开始位置 插入元素是很有用的功能,所以insert函数 将元素插入到选代器所指定的位置之前
list<string> slist;
slist.insert(iter,"Hello!"); //将“Hello!”添加到iter 之前 的位置
//等价于调用slist.push_front("Hello!");
slist.insert(slist.begin(), "Hello!");
//vector不支持push_front,但我们可以插入到begin() 之前
//警告:插入到vector末尾之外 的任何位置都可能很慢
s vec.insert(s vec.begin(), "Hello!");
将元素插入到vector、deque和string中 的任何位置都是合法的。然而,这样做可能很耗时(list和forward_list更可以插入到任何位置了,而且很快)
8、插入范围内元素:insert函数还可以接受更多的参数,其中一个版本 接受一个元素数目和一个值,它将指定数量的元素添加到指定位置 之前,这些元素都按给定值初始化:svec.insert(svec.end(), 10, "Anna");
接受一对选代器 或一个初始化列表的insert版本 将给定范围中的元素插入到指定位置 之前:
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());
传递给insert一对选代器,不能指向 添加元素的目标容器
在新标准下,接受元素个数 或 范围的insert版本 返回指向 第一个 新加入元素的迭代器。(在旧版本的标准库中,这些操作返回void)如果范围为空,不插入任何元素,insert操作 会将 第一个 参数返回
9、使用insert的返回值:通过使用insert的返回值,可以在容器中 一个特定位置反复插入元素
list<string> lst;
auto iter = lst.begin();
while (cin >> word)
iter = lst.insert(iter, word); //等价于调用push_front
10、使用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则会 创建一个局部临时对象,并将其压入容器中
emplace函数的参数 根据元素类型而变化,参数必须 与元素类型的构造函数相匹配
//iter指向c中一个元素,其中保存了Sales_data元素
c.emplace_back(); //使用Sales_data的默认构造函数
c.emplace(iter,"999-999999999"); //使用Sales_data(string)
//使用Sales_data的接受一个ISBN、一个count和一个price的构造函数
c.emplace_front("978-0590353403", 25, 15.99);
11、下面的程序存在什么错误
vector<int>::iterator iter = iv.begin(),
mid = iv.begin() + iv.size() / 2;
while (iter != mid)
if (*iter == some_val)
iv.insert(iter, 2 * some_val);
循环中未对 iter 进行递增操作,iter 无法向中点推进。其次,即使加入了 iter++ 语句,由于向 iv 插入元素后,iter 已经失效,iter++ 也不能起到将迭代器向前推进一个元素的作用。修改方法如下:
首先,将 insert 返回的迭代器赋予 iter,这样,iter 将指向新插入的元素 y。我们知道,insert 将 y 插入到 iter 原来指向的元素 x 之前的位置,因此,接下来我们需要进行两次 iter++ 才能将 iter 推进到 x 之后的位置
其次,insert() 也会使 mid 失效,因此,只正确设置 iter 仍不能令循环在正确的时候结束,我们还需要设置 mid 使之指向 iv 原来的中央位置的元素。在未插入任何新元素之前,此位置是 iv.begin() + iv.size() / 2 ,我们将此时的 iv.size() 的值记录在变量 org_size 中。然后在循环过程中统计新插入的元素的个数 new_ele ,则在任何时候,iv.begin() + org_size / 2 + new_ele 都能正确指向 iv 原来的中央位置的元素
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> iv = {1, 1, 2, 1}; // int 的 vector
int some_val = 1;
vector<int>::iterator iter = iv.begin();
int org_size = iv.size(), new_ele = 0; // 原大小和新元素个数
// 每个循环步都重新计算 mid,保证正确指向 iv 原中央位置的元素
while (iter != (iv.begin() + org_size / 2 + new_ele))
if (*iter == some_val) {
iter = iv.insert(iter, 2 * some_val); // iter 指向新元素
++new_ele;
iter += 2; // 将 iter 推进到旧元素的下一个元素
} else {
++iter; // 指向后推进一个位置
}
// 用 begin() 获取 vector 首元素迭代器,遍历 vector 中的所有元素
for (iter = iv.begin(); iter != iv.end(); ++iter)
cout << *iter << endl;
return 0;
}
非连续存储的 list 和 forward_list 不支持迭代器加减元素(不支持 iter += 2
这样的代码),应多次调用 ++ 来实现与迭代器加法相同的效果(++iter; ++iter;
)
3.2 访问元素
1、可以用来在顺序容器中 访问元素的操作。如果容器中没有元素,访问操作的结果是未定义的,且都返回 引用
at和下标操作 只适用于string、vector、deque和array
back不适用于forward_list(与push_back一致,除了array)
函数 | 作用 |
---|---|
c.back() | 返回c中尾元素的 引用。若c为空,函数行为未定义 |
c.front() | 返回c中首元素的 引用。若c为空,函数行为未定义 |
c[n] | 返回c中下标为n 的元素的 引用,n是一个无符号整数。若n>=c.size(),则函数行为未定义 |
c.at(n) | 返回下标为n的元素的 引用。如果下标越界,则抛出 out_of_range异常 |
2、包括array在内的每个顺序容器都有一个 front成员函数,而除forward_list之外的 所有顺序容器都有一个 back成员函数。这两个操作分别返回 首元素 和 尾元素的 引用:
//在解引用一个选代器 或 调用front或back之前检查是否有元素
if(!c.empty()) {
//val和val2是c中第一个元素值的拷贝,注意begin和front的区别
auto val = *c.begin(), val2 = c.front();
//val3和val4是c中最后一个元素值的拷贝
auto last = c.end();
auto val3 = *(--last); //不能递减forward_list选代器
auto val4 = c.back(); //forward_list不支持back()
}
用两种不同方式来获取 c中的首元素和尾元素的 引用
迭代器end指向的是 容器尾元素之后的(不存在的)元素。为了获取尾元素,必须首先递减此送代器
在调用front和back(或解引用begin和end返回的选代器)之前,要确保c非空。如果容器为空,if中操作的行为将是未定义的
3、访问 成员函数返回的是 引用:如果容器是一个const对象,则返回值是const的引用。如果容器不是const的,则
返回值是普通引用,可以用来 改变元素的值:
if(!c.empty()) {
//将42赋予c中的第一个元素
c.front() = 42;
//获得 指向最后一个元素的引用
auto &v=c.back();
//改变c中的元素
v=1024;
auto v2 = c.back();
//v2不是一个引用,它是c.back()的一个拷贝
//未改变C中的元素
v2 = 0;
4、下标操作和安全的随机访问:提供快速随机访问的容器(string、vector、deque和array)也都提供 下标运算符
下标运算符接受一个 下标参数,返回容器中该位置的元素的 引用。下标运算符并不检查下标是否在合法范围内,而且编译器并不检查这种错误
希望确保下标是 合法的,可以使用 at成员函数。at成员函数 类似下标运算符,但如果 下标越界,at会抛出一个out of range异常
//空vector
vector<string>s vec:
//运行时错误:s vec中没有元素!
cout << svec[0];
//抛出一个out of_range异常
cout << svec.at(0);
3.3 删除元素
1、(非array)容器也有多种删除元素的方式
这些操作会改变容器的大小,所以 不适用于array
forward_list 有特殊版本的erase
forward_list不支持pop_back(push_back() 和 back() 同样不支持);vector和string不支持pop_front(同样 不支持push_front 和 emplace_front)
pop_back,push_back,pop_front,push_front都是返回void
erase都是返回被删元素之后元素 的迭代器
函数 | 作用 |
---|---|
c.pop_back() | 删除c中 尾元素。若c为空,则函数行为未定义。函数返回void |
c.pop_front() | 删除c中 首元素。若c为空,则函数行为未定义。函数返回void |
c.erase(p) | 删除送代器p 所指定的元素,返回一个指向被删元素 之后元素 的送代器,若p指向 尾元素,则返回 尾后(off-the-end)选代器。若p是 尾后迭代器,则函数行为未定义 |
c.erase(b, e) | 删除送代器b和e所指定范围内的元素。返回一个指向 最后一个被删元素之后 元素的选代器,若e本身就是 尾后选代器,则函数也返回 尾后迭代器 |
c.clear() | 删除c中的所有元素。返回void |
2、删除deque中除首尾位置之外 的任何元素都会使 所有选代器、引用和指针失效。指向vector或string中删除点之后位置 的迭代器、引用和指针都会失效
3、删除元素的成员函数并不检查其参数。在删除元素之前,程序员必须确保它(们)是存在的
4、pop_front和pop_back成员函数:与vector和string不支持push_front一样,这些类型也不支持pop_front,类似的,forward_list
不支持pop_back。与元素访问成员函数类似,不能对一个空容器执行弹出操作
这些操作返回void。如果你需要 弹出的元素的值,就必须在 执行弹出操作之前保存它:
while (!ilist.empty()) {
process(ilist.front()); //对ilist的首元素进行一些处理
ilist.pop_front(); //完成处理后删除首元素
}
5、从容器内部删除一个元素:成员函数erase从容器中指定位置 删除元素。可以删除 由一个迭代器指定的单个元素,也可以删除由 一对送代器 指定的范围内 的所有元素。两种形式的erase都返回 指向删除的(最后一个)元素之后位置 的选代器
下面的循环删除一个list中的所有奇数元素
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;
6、删除多个元素:接受一对迭代器的erase版本 允许 删除一个范围内的元素
//删除两个迭代器表示的范围内的元素
//返回指向最后一个被删元素之后位置的迭代器
elem1 = slist.erase(elem1, elem2); //调用后,eleml == elem2
迭代器elem1指向 要删除的第一个元素,elem2指向 要删除的最后一个元素 之后的位置
删除一个容器中的所有元素
slist.clear(); //删除容器中所有元素
slist.erase(slist.begin(), slist.end()); //等价调用
7、删除一个范围内的元素的程序:
如果elem1与elem2相等,则一个元素都不会删除;
如果elem2是尾后迭代器,则会从elem1元素删除到最后一个元素;
如果elem1与elem2都是尾后迭代器,则一个元素都不会删除
8、删除一个范围内的元素的程序,如果 elem1 与 elem2 相等会发生什么?如果 elem2 是尾后迭代器,或者 elem1 和 elem2 皆为尾后迭代器,又会发生什么?
#include <vector>
#include <list>
#include <iostream>
#include <iterator>
using namespace std;
int main()
{
int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 };
vector<int> vec(ia, end(ia));
list<int> l(ia, end(ia));
for (auto iter = vec.begin(); iter != vec.end(); ) {// 不能iter++,不然erase之后就加两遍了
if (*iter % 2 == 0)
iter = vec.erase(iter);
else
iter++;
}
for (auto iter = l.begin(); iter != l.end(); ) {
if (*iter % 2)
iter = l.erase(iter);
else
iter++;
}
for (int i : vec)
cout << i << " ";
cout << endl;
for (int i : l)
cout << i << " ";
return 0;
}
3.4 特殊的forward_list操作
1、考虑当 从一个单向链表中 删除一个元素时会发生什么,当添加或删除一个元素时,删除或添加的元素之前的那个元素的后继会发生改变。为了添加或删除一个元素,我们需要访问其 前驱,以便改变前驱的链接
但是,forward_list是单向链表,没有简单的方法来获取一个元素的前驱。所以,在一个forward_list中 添加或删除元素的操作 是通过改变给定元素 之后 的元素来完成的。这样,总是可以访问到 被添加或删除操作所影响的元素
forward_list并未定义insert、 emplace 和 erase,而是定义了名为 insert_after、emplace_after 和 erase_after的操作
例如,为了删除elem3,应该用 指向elem2的迭代器 调用erase_after。为了支持这些操作,forward_list也定义了before_begin,它返回一个首前(off-the-beginning)迭代器。这个迭代器允许我们在链表首元素之前 并不存在的元素“之后” 添加或删除元素(即在链表首元素之前 添加删除元素)
2、在forward_list中 插入或删除元素的操作:
函数 | 作用 |
---|---|
lst.before_begin(), lst.cbefore_begin() | 返回指向链表首元素之前 不存在的元素的迭代器。此迭代器不能解引用。cbefore_begin() 返回一个 const_iterator |
lst.insert_after(p, t),lst.insert_after(p,n,t),lst.insert_after(p, b, e),lst.insert_after(p, il) | 在选代器p之后(其他容器是之前)的位置插入元素。t是一个对象,n是数量;b和e是表示范围的一对迭代器(b和e不能指向lst内,不能把自己的一部分复制进自己);il是一个花括号列表。返回一个指向最后一个插入元素的迭代器(其他容器返回 第一个新插入容器的 迭代器)。如果范围为空,则返回p。若p为尾后迭代器,则函数行为未定义 |
emplace_after(p, args) | 使用args在p指定的位置之后 创建一个元素。返回一个 指向这个新元素的选代器。若p为 尾后选代器,则函数行为未定义 |
lst.erase_after§,lst.erase_after(b, e) | 删除p指向的位置之后 的元素(其他是 删除这个位置上的),或删除从b之后 直到(但不包含)e之间的元素。返回一个指向被删元素之后元素的迭代器(与其他元素相同),若不存在这样的元素,则返回尾后选代器。如果p指向lst的尾元素 或者 是一个尾后送代器,则 函数行为未定义 |
3、当在forward_list中添加或删除元素时,我们必须关注两个送代器一个指向 要处理的元素,另一个 指向其前驱
例:从list中删除奇数元素 的循环程序,将其改为从forward list中 删除元素
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); //删除它并移动curr, prev保持不变,仍指向(新)curr之前的元素
else{
prev = curr; //移动迭代器curr,指向下一个元素,prev指向curr之前的元素
++curr;
}
}
curr表示 要处理的元素,prev表示 curr的前驱
3.5 改变容器大小
1、可以用resize来增大或缩小容器,array不支持resize。如果当前大小 大于所要求的大小,容器后部的元素 会被删除:如果当前大小 小于新大小,会将新元素 添加到容器后部
list<int> ilist(10, 42); //10个int:每个的值都是42
ilist.resize(15); //将5个值为0的元素添加到ilist的末尾
ilist.resize(25, -1); //将10个值为-1的元素添加到ilist的末尾
ilist.resize(5); //从ilist末尾删除20个元素
resize操作接受一个 可选的元素值参数,用来初始化 添加到容器中的元素。如果调用者 未提供此参数,新元素进行 值初始化。如果容器保存的是 类类型元素,且resize向容器 添加新元素,则 必须提供初始值,或者 元素类型必须提供一个 默认构造函数
2、顺序容器大小操作:
函数 | 作用 |
---|---|
c.resize(n) | 调整c的大小为n个元素。若n<c.size(),则 多出的元素被丢弃。若必须 添加新元素,对新元素进行 值初始化 |
c.resize(n,t) | 调整c的大小为n个元素。任何新添加的元素都 初始化为值t |
如果resize缩小容器,则指向被删除元素的选代器、引用和指针都会失效
对vector、string 或 deque 进行resize可能导致迭代器、指针和引用失效
3、接受单个参数的resize版本对元素类型的限制:对于元素类型是 类类型,则单参数 resize 版本要求该类型 必须提供一个 默认构造函数
3.6 容器操作可能使选代器失效
1、向容器中 添加元素 和 从容器中删除元素的操作 可能会使 指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或送代器 将不再表示任何元素。使用失效的指针、引用或迭代器 是一种严重的程序设计错误,很可能引起 与使用未初始化指针一样的问题
2、在向容器添加元素后:
1)如果容器是 vector或string(顺序存储的),且存储空间 被重新分配,则指向容器的选代器、指针和引用 都会失效
如果存储空间 未重新分配,指向 插入位置 之前 的元素的迭代器、指针和引用仍有效,但 指向插入位置之后元素的送代器、指针和引用将会失效
2)对于deque,插入到 除首尾位置之外的任何位置 都会导致送代器、指针和引用失效
如果 在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效
3)对于list和forward_list,指向容器的送代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效
3、从一个容器中删除元素后,指向被删除元素的送代器、指针和引用会失效,因为这些元素都已经被销毁了。当我们删除一个元素后:
1)对于list和forward_list,指向 容器其他位置的迭代器(包括尾后迭代器 和 首前迭代器)、引用和指针仍有效
2)对于deque,如果在 首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效
如果是删除deque的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不
会受影响
3)对于vector和string,指向被删元素之前元素的迭代器、引用和指针仍有效
注意:当我们删除元素时,尾后迭代器总是会失效
4、建议:管理送代器
由于向迭代器添加元素 和从迭代器删除元素的代码 可能会使迭代器失效,因此必须保证 每次改变容器的操作之后 都正确地重新定位迭代器。这个建议对vector、string 和 deque尤为重要
5、编写改变容器的循环程序:如果循环中
调用的是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指向我们删除的元素之后的元素
}
在调用insert和erase后 都更新迭代器,因为两者都会 使迭代器失效
insert在给定位置之前 插入新元素,然后返回 指向新插入元素的迭代器
删除偶数值元素并复制奇数值元素的程序不能用于list或forward_list:
不能直接用于list或forward_list,list 和 forward_list 。与其他容器的一个不同是,迭代器 不支持加减运算,究其原因,链表中元素 并非在内存中连续存储,因此无法通过地址的加减 在元素间远距离移动。因此,应多次调用++来实现与迭代器加法相同的效果
list:(主要是 iter +=2
改成 ++iter;++iter;
)
#include <list>
#include <iostream>
using namespace std;
int main()
{
list<int> l1 = {0,1,2,3,4,5,6,7,8,9};
auto iter = l1.begin();
while(iter != l1.end())
{
if(*iter % 2)
{
iter = l1.insert(iter, *iter);
++iter;
++iter;
}else
{
iter = l1.erase(iter);
}
}
for(const auto i : l1)
cout << i << " ";
cout << endl;
return 0;
}
对于 forward_list ,由于是单向链表结构,删除元素时,需将前驱指针调整为指向下一个节点,因此需维护“前驱”、“后驱”两个迭代器:
#include <forward_list>
#include <iostream>
using namespace std;
int main()
{
forward_list<int> flst = {0,1,2,3,4,5,6,7,8,9};
auto iter = flst.begin();
auto prev = flst.before_begin();
while(iter != flst.end())
{
if(*iter % 2)
{
iter = flst.insert_after(iter, *iter);
prev = iter;
++iter;
}else
{
iter = flst.erase_after(prev);
}
}
for(const auto i : flst)
cout << i << " ";
cout << endl;
return 0;
}
语句iter = vi.insert(iter, *iter++);
不合法,insert中的参数运行顺序是未定义的,我们不知道iter运行的是iter+1的状态还是未+1的状态
6、不要保存end返回的送代器:当 添加/删除vector或string的元素后,或 在deque中首元素之外任何位置 添加/删除元素后,原来end返回的迭代器 总是会失效。添加或删除元素的循环程序 必须反复调用end,而不能在循环之前保存end返回的迭代器,一直当作容器末尾使用
例:考虑这样一个循环,它处理容器中的每个元素,在其后添加一个新元素。我们希望循环 能跳过新添加的元素,只处理 原有元素。在每步循环之后,我们将 定位迭代器,使其指向下一个原有元素。如果我们试图“优化”这个循环,在循环之前保存end() 返回的选代器,一直用作容器末尾,就会导致一场灾难:
//灾难:此循环的行为是未定义的
auto begin = v.begin(), end=v.end(); //保存尾迭代器的值是一个坏主意
while(begin!= end){
//做一些处理
//插入新值,对begin重新赋值,否则的话它就会失效
++begin;//向前移动begin,因为我们想在此元素之后插入元素
begin=v.insert(begin,42); //插入新值
++begin;//向前移动begin跳过我们刚刚加入的元素
}
问题在于我们 将end操作返回的迭代器 保存在一个名为end的局部变量中。在循环体中,我们向容器中 添加了一个元素,这个操作 使保存在end中的迭代器失效了
必须在每次插入操作后 重新调用end()
//更安全的方法:在每个循环步添加/删除元素后都重新计算end
while(begin != v.end())
//做一些处理
++begin; //向前移动begin,因为我们想在此元素之后插入元素
begin = v.insert(begin,42); //插入新值
++begin; //向前移动begin,跳过我们刚刚加入的元素
}
4、vector对象是如何增长的
1、为了支持快速随机访问,vector将元素连续存储。对于vector和string,其部分实现渗透到了接口中
假定容器中 元素是连续存储的,且容器的大小是可变的,考虑向vector或string中添加元素:
如果没有空间容纳新元素,容器不可能简单地将它添加到内存中其他位置——因为元素必须连续存储。容器必须分配新的内存空间来保存已有元素和新元素,将已有元素从旧位置移动到新空间中,然后添加新元素,释放旧存储空间
为了避免这种代价,标准库实现者采用了 可以减少容器空间重新分配次数的策略:不得不获取新的内存空间时,vector和string的实现 通常会分配 比新的空间需求更大 的内存空间
虽然vector在每次重新分配内存空间时都要移动所有元素,但使用此策略后,其扩张操作通常比list和deque还要快
2、管理容量的成员函数:vector和string类型提供了一些 成员函数,允许我们与它的实现中 内存分配部分 互动。capacity操作告诉我们 容器在不扩张内存空间的情况下 可以容纳多少个元素。reserve操作 允许我们 通知容器它应该准备保存多少个元素
容器大小管理操作:
shrink_to_fit 只适用于 vector、string和deque
capacity和reserve 只适用于 vector和string(没有deque)。list或array没有capacity成员函数:list所占的空间不是连续的;array是固定size的
函数 | 作用 |
---|---|
c.shrink_to_fit() | 将capacity() 减少为与size() 相同大小 |
c.capacity() | 不重新分配内存空间 的话,c可以 保存多少元素 |
c.reserve(n) | 分配至少能容纳n个元素的内存空间 |
reserve并不改变容器中元素的数量,它仅影响vector / string预先分配多大的内存空间
3、只有当需要的内存空间(参数n) 超过当前容量时,reserve调用才会改变vector的容量
如果需求大小 大于当前容量,reserve至少分配 与需求一样大的内存空间(可能更大)。如果需求大小 小于或等于 当前容量,reserve什么也不做。特别是,当需求大小 小于当前容量时,容器不会退回内存空间。因此,在调用reserve之后,capacity将会 大于或等于传递给reserve的参数
调用reserve永远也不会 减少容器占用的内存空间。类似的,resize成员函数 只改变容器中元素的数目,而不是容器的容量。同样不能使用resize 来减少容器预留的内存空间
在新标准库中,可以调用shrink_to_fit 来要求deque, vector 或 string 退回不需要的内存空间。调用shrink_to_fit也并不保证 一定退回内存空间
4、capacity和size的区别:容器的size 是指它已经保存的元素的数目;而capacity 则是在不分配新的内存空间的前提下 最多可以保存多少元素
一个空vector的size为0,实现中 一个空vector的capacity 也为0。向vector中添加元素时,size与添加的元素数目相等。而capacity至少与size一样大
可以预分配一些额外空间:ivec.reserve(50); // 将capacity至少设定为50,可能会更大
只要没有操作需求 超出vector的容量,vector就不能 重新分配内存空间
5、虽然不同的实现 可以采用不同的分配策略,但所有实现都应遵循一个原则:确保用push_back 向vector添加元素的操作 有高效率。从技术角度说,就是通过 在一个初始为空的vector上 调用n次push_back来创建 一个n个元素的vector,所花费的时间 不能超过n的常数倍
6、探究在你的标准实现中,vector是如何增长的
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector<int> svec;
svec.push_back(1);
svec.resize(12);
for (int i = 0; i < svec.size(); i++)
cout << svec[i] << endl;
cout << svec.size() << " " << svec.capacity() << endl;
return 0;
}
运行结果:
5、额外的string操作
string类型 提供了一些额外的操作。这些操作中的大部分 要么是提供string类和C风格字符数组之间的相互转换,要么是增加了 用下标代替送代器的版本
5.1 构造string的其他方法
1、n,len2 和 pos2 都是无符号值
构造 | 作用 |
---|---|
string s(cp, n) | s是 cp指向的数组中前n个字符的 拷贝。此数组至少应该 包含n个字符 |
string s(s2, pos2) | s是string s2从 下标pos2开始的字符的拷贝。若pos 2 > s 2.size(),构造函数的行为未定义 |
string s(s2, pos2, len2) | s是string s2从 下标pos2开始 len2个字符的拷贝。若pos2>s2.size(),构造函数的行为未定义。不管len2的值是多少,构造函数至多拷贝 s2.size()-pos2 个字符 |
这些函数 接受一个string或一个const char*参数,还接受(可选的)指定拷贝 多少个字符的参数。当我们传递给它们的是一个string时,还可以 给定一个下标来指出从哪里开始拷贝
const char *cp = "HelloWorld!!!"; //以空字符结束的数组
char noNull[] = {'H', 'i'}; //不是以空字符结束
string s1(cp); //拷贝cp中的字符 直到遇到空字符;s1 == "HelloWorld!!!"
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 == "World!!!"
string s8(s1, 16); // 抛出一个out_of_range异常,起点>s1.size()
从一个 const char*创建string时,指针指向的数组 必须以空字符结尾,拷贝操作 遇到空字符时停止。还传递给 构造函数一个计数值,数组就不必以 空字符结尾。未传递 计数值 且 数组也未以空字符结尾,或者 给定计数值 大于数组大小,则构造函数的行为是未定义的
传递了一个 计数值,则从 给定位置开始拷贝这么多个字符。不管 要求拷贝多少个字符,标准库最多拷贝到 string结尾,不会更多
附:string / const char* 的 size / sizeof
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "ashergu";
cout << str.size() << " " << str.capacity() << endl;
cout << sizeof(str) / sizeof(str[0]) << endl;
// 这两个输出不一样的原因是因为它们计算的内容不同, 见下
const char* cp = "ashergu"; // 直接转换为str不行
cout << sizeof(cp) / sizeof(cp[0]) << endl; // 后面有个空字符
string str1 = "hello world"; // 不能重复初始化
string s = str1.substr(6, 9); // 长度9超过了 也只能到结尾
cout << s;
return 0;
}
1)str.size() 返回的是字符串 str 中字符的实际数量,不包括结尾的 null 字符
2)str.capacity() 返回的是 str 字符串对象在不重新分配内存的情况下可以容纳的字符数量。它可能会比 str.size() 返回的值大,因为 std::string 会预留一定量的内存以便执行插入操作而不需要重新分配内存,这可以提高性能
3)sizeof(str) / sizeof(str[0]) 计算的是 str 对象在内存中所占的字节数,除以 str[0] 所占的字节数(通常为1),得到的结果 并不会给出字符串的实际长度,而是字符串对象本身的大小
因此,str.size() 和 str.capacity() 返回的是关于字符串实际内容和内存管理的信息,而 sizeof(str) / sizeof(str[0]) 返回的是关于字符串对象本身在内存中的大小
运行结果:
2、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的大小
开始位置加上计数值 大于string的大小,则substr会 调整计数值,只拷贝到string的末尾
字符串操作
函数 | 作用 |
---|---|
s.substr(pos, n) | 返回一个string,包含 s中从pos开始的n个字符的拷贝。pos的默认值 为0。n的默认值 为s.size()-pos,即拷贝 从pos开始的所有字符 |
编写程序,从一个vector初始化一个string:(用迭代器)string s(vc.begin(), vc.end());
5.2 改变string的其他方法
1、string类型 支持顺序容器的赋值运算符 以及 assign、insert和erase操作,还定义了 额外的insert和erase版本
string还提供了接受下标的版本。下标指出了 开始删除的位置,或是 insert到给定值之前的位置
s.insert(s.size(), 5, "!"); // 在s末尾插入5个感叹号
s.erase(s.size() - 5, 5); // 从s删除最后5个字符 参数为下标,'\0'不算
接受C风格字符数组 的insert和assign版本
const char *cp = "Stately,plump Buck";
s.assign(cp, 7); //s=="Stately"
s.insert(s.size(), cp + 7); //s=="Stately,plump Buck"
调用assign替换s的内容,要求赋值的字符数必须小于或等于cp指向的数组中的字符数(不包括结尾的空字符)
接下来在s上调用insert,将字符插入到s[size()]处(不存在的)元素 之前的位置
将cp第8个字符开始的字符串(至多到结尾空字符之前)拷贝到s中。跟之前的insert(p, t)相比 只是把迭代器 换成下标
也可以 指定将 来自其他string 或 子字符串的字符 插入到当前string中 或赋予当前string:
string s = "some string", s2 = "some other string";
s.insert(0, s2); //在s中位置0之前插入s2的拷贝
// 在s[0]之前插入s2中s2[0]开始的 s2.size()个字符
s.insert(0, s2, 0, s2.size());
2、append和replace函数:
这两个函数可以改变 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个字符并插入"5 th"
s2.replace(11, 3, "5th"); //等价方法:s==s 2
调用replace时,插入的文本 不需要与删除的文本一样长:s.replace(11, 3,"Fifth"); // s == "C++ Primer Fifth Ed."
3、修改 string操作
insert,erase下标作为参数,返回 字符串引用,迭代器作为参数,返回 迭代器
函数 | 作用 |
---|---|
s.insert(pos, args) | 在pos之前插入args指定的字符。pos可以 是一个下标或一个迭代器。接受下标的版本 返回一个 指向s的引用(不是迭代器);接受迭代器的版本返回 指向第一个插入字符的迭代器 |
s.erase(pos, len) | 删除 从位置pos开始的len个字符。如果len被省略,则删除 从pos开始直至s末尾的所有字符。返回一个 指向s的引用;接受迭代器的erase:c.erase§,c.erase(b, e) |
s.assign(args) | 将s中的字符 替换为args指定的字符。返回一个 指向s的引用 |
s.append(args) | 将args 追加到s。返回一个 指向s的引用 |
s.replace(range, args) | 删除 s中范围range内的字符,替换为args指定的字符。range 或者是 一个下标和一个长度,或者是 一对指向s的选代器。返回一个指向s的引用 |
args可以是 下列形式之一;append和assign 可以使用所有形式
str不能与s相同,迭代器b和e不能指向s
args | 解释 |
---|---|
str | 字符串str |
str, pos, len | str中从pos开始最多len个字 |
cp, len | 从cp指向的字符数组 的前(最多)len个字符 |
cp | cp指向的 以空字符结尾的字符数组 |
n, c | n个字符c |
b, e | 迭代器b和e指定的 范围内的字符 |
初始化列表 | 花括号包围的,以逗号分隔的字符列表 |
replace和insert 所允许的args形式 依赖于range和pos是如何指定的
replace(pos, len, args) | replace(b, e, args) | insert(pos, args) | insert(iter, args) | args可以是 |
---|---|---|---|---|
是 | 是 | 是 | 否 | str |
是 | 否 | 是 | 否 | str, pos, len(迭代器参数 不行) |
是 | 是 | 是 | 否 | cp, len(cp指向 以空字符结尾的字符数组)(跟str一样) |
是 | 是 | 否 | 否 | cp(insert不行) |
是 | 是 | 是 | 是 | n, c(都行) |
否 | 是 | 否 | 是 | b2, e2(参数为下标不行) |
否 | 是 | 否 | 是 | 初始化列表(参数为下标不行) |
改变 string的多种 重载函数:
replace函数提供了 两种指定删除元素范围的方式。可以通过 一个位置和一个长度来指定范围,也可以通过 一个送代器范围来指定
insert函数允许我们用 两种方式指定插入点:用一个下标 或 一个迭代器。在两种情况下,新元素都会插入到给定下标(或迭代器)之前的位置
可以用 好几种方式来指定 要添加到string中的字符。新字符可以 来自于另一个string,来自于一个字符指针(指向的字符数组),来自于一个花括号包围的字符列表,或者是一个字符和一个计数值
当字符来自于 一个string或一个字符指针时,我们可以传递 一个额外的参数来控制是拷贝部分还是全部字符
并不是每个函数都支持所有形式的参数
4、编写一个函数,接受三个string参数 是s、oldVal 和newVal。使用insert和erase函数 将s中所有oldVal替换为newVal
使用迭代器参数版本:
#include <iostream>
#include <string>
using namespace std;
void solve(string& s, const string& oldVal, const string& newVal)
{
if (oldVal.size() > s.size())
return;
for (auto it = s.begin(); it != s.end();) // 不能设it != s.end() - oldVal.size(),因为是可以跳过去的
{
if (s.end() - it < oldVal.size())
return;
string tmp = string(it, it + oldVal.size());
if (tmp == oldVal)
{
it = s.erase(it, it + oldVal.size());
it = s.insert(it, newVal.begin(), newVal.end());
// 注意erase / insert之后it会改变,如果使用下标参数返回的是 string&,用下标操作的时候循环不要使用迭代器,直接整下标
it = it + newVal.size();
}
else {
it++;
}
}
for (char c : s)
cout << c;
}
int main()
{
string s = "iuithoughiuthough";
solve(s, "though", "tho");// 直接传入"iuithoughiuthought"报错,无法用const char [19]初始化string&
return 0;
}
使用下标参数版本:
#include <iostream>
#include <string>
using namespace std;
void solve(string& s, const string& oldVal, const string& newVal)
{
if (oldVal.size() > s.size())
return;
for (int i = 0; i <= s.size() - oldVal.size(); i++)
{
if (string(s, i, oldVal.size()) == oldVal)
{
s.erase(i, oldVal.size());
s.insert(i, newVal);
// 用下标操作的时候循环不要使用迭代器,直接整下标
}
}
for (char c : s)
cout << c;
}
int main()
{
string s = "iuithoughiuthough";
solve(s, "though", "tho");
return 0;
}
5.3 string搜索操作
1、提供了6个不同的搜索函数,每个函数都有4个重载版本(见后面表格)。
每个搜索操作都 返回一个string::size type值,表示匹配发生位置的 下标。如果 搜索失败,则返回一个 名为string::npos的static成员
标准库将npos定义为一个const string::size_type类型,并初始化为值-1。由于 npos是一个unsigned类型,此初始值意味着 npos等于任何string最大的可能大小
2、string搜索函数 返回string::size_type值,该类型是一个 unsigned类型。因此,用一个int或其他带符号类型 来保存这些函数的返回值 不是一个好主意
3、find函数完成 最简单的搜索。它查找参数指定的字符串,若找到,则返回第一个匹配位置的 下标,否则返回npos:
string name("Anna Belle");
auto pos1 = name.find("Anna"); //pos 1==0, 即子字符串"Anna"在"Anna Belle"中第一次出现的下标
4、搜索(以及其他string操作)是大小写敏感的
5、查找与给定字符串中 任何一个字符 匹配的位置
例如,下面代码 定位name中的第一个数字:
string numbers("0123456789"), name("r2d2");
//返回1,即,name中第一个数字2的下标,numbers是一个查找范围的集合
auto pos = name.find_first_of(numbers);
搜索第一个 不在参数中的字符,应该调用 find_first_not_of。搜索一个string中 第一个非数字字符
string dept("03714p3");
//返回5——字符'p'的下标
auto pos = dept.find_first_not_of(numbers);
6、string搜索操作
搜索操作返回 指定字符出现的下标,如果未找到 则返回npos
函数 | 作用 |
---|---|
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中的字符 |
args必须是 以下形式之一
args参数 | 解释 |
---|---|
c, pos | 从s中 位置pos开始查找字符c。pos默认为0 |
s2, pos | 从s中 位置pos开始查找字符串s2。pos默认为0 |
cp, pos | 从s中位置pos开始查找 指针cp指向的 以空字符结尾的C风格字符串。pos默认为0 |
cp, pos, n | 从s中位置pos开始查找 指针cp指向的 数组的前n个字符。pos和n无默认值 |
7、指定在哪里开始搜索:可以传递给 find操作 一个可选的开始位置。一种常见的程序设计模式是 用这个可选参数 在字符串中循环地搜索 子字符串出现的所有位置:
string::size_type pos = 0;
// 每步循环查找name中下一个数
while ((pos = name.find_first_of(numbers, pos)) != string::npos) {
cout << "found number at index:" << pos << "element is" << name[pos] << endl;
++pos; //移动到下一个字符
}
8、逆向搜索:rfind成员函数 搜索最后一个匹配,即子字符串 最靠右的出现位置
string river("Mississippi");
auto first_pos = river.find("is"); //返回1,表示第一个"is"的位置
auto last_pos = river.rfind("is"); //返回4,表示最后一个"is"的位置
find_last_of搜索 与 给定string中任何一个字符匹配的 最后一个字符
find_last_not_of搜索 最后一个 不出现在给定string中的字符
每个操作都 接受一个可选的第二参数,可用来指出 从什么位置开始搜索
9、首先查找string"ab2c3d7R4E6"中每个数字字符,然后查找其中每个字母字符。数字 要使用find_first_of,字母 要使用find_first_not_of
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("ab2c3d7R4E6");
string num("123456789");
string::size_type pos = 0;
while ((pos = str.find_first_of(num, pos)) != string::npos)
{
cout << str[pos++] << " ";
}
cout << endl;
pos = 0;
while ((pos = str.find_first_not_of(num, pos)) != string::npos)
{
cout << str[pos++] << " ";
}
return 0;
}
5.4 compare函数
除了关系运算符外,标准库string类型 还提供了 一组compare函数,这些函数与 C标准库的strcmp函数 很相似
根据s是 等于、大于还是小于参数指定的字符串,s.compare 返回0、正数 或 负数
s.compare的几种参数形式:
参数 | 解释 |
---|---|
s2 | 比较s 和 s2 |
pos1, n1, s2 | 将s中 从pos1开始的n1个字符 与s2进行比较 |
pos1, n1, s2, pos2, n2 | 将s中 从pos1开始的n1个字符 与s2中 从pos2开始的n2个字符 进行比较 |
cp | 比较s与cp指向的 以空字符结尾的字符数组 |
pos1, n1, cp | 将s中 从pos1开始的n1个字符 与cp指向的 以空字符结尾 的字符数组进行比较 |
pos1, n1, cp, n2 | 将s中从pos1开始的n1个字符 与指针cp指向的地址 开始的n2个字符 进行比较 |
5.5 数值转换
1、数值15如果保存为16位的short类型,则其 二进制位模式为0000000000001111,而字符串"15"存为 两个Latin-1编码的char,二进制位模式为0011000100110101
可以实现数值数据 与标准库string之间的转换
int i = 42;
string s = to_string(i); //将整数i转换为字符表示形式
double d = stod(s); //将字符串s转换为浮点数
2、要转换为数值的string中 第一个非空白符 必须是 数值中可能出现的字符:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "i = -7b";
int i = stoi(s.substr(s.find_first_of("+-0123456789.")), nullptr, 16);
// 转int,std::string::substr 函数返回一个新的 std::string 对象
cout << i << endl;
string s1 = "j = -0x245abc";
string s2 = "pi = .14e-7asher";
double d = stod(s1.substr(s1.find_first_of("+-0123456789.")));// 转double
cout << d << endl;
d = stod(s2.substr(s2.find_first_of("+-0123456789.")));
cout << d;
return 0;
}
运行结果:
stod函数 读取此参数,处理其中的字符,直至遇到 不可能是数值的一部分的字符,然后它就 将找到的这个数值的字符串表示形式 转换为对应的双精度浮点值
string参数中 第一个非空白符必须是符号(+或-)或数字。它可以 以0x或0X 开头来表示十六进制数
那些将字符串 转换为浮点值的函数,string参数 也可以以小数点(.)开头,并可以 包含e或E来表示指数部分。对于那些将字符串转换为 整型值的函数,根据基数(进制数)不同,string参数可以包含字母字符,对应大于数字9的数
如果string不能转换为一个数值,这些函数抛出一个invalid argument异常;转换得到的数值无法用任何类型来表示,则抛出一个out of range异常
3、string和数值之间的转换:
函数 | 作用 |
---|---|
to_string(val) | 一组重载函数,返回数值val的string表示。val可以是 任何算术类型。对每个浮点类型和int或更大的整型,都有相应版本的to_string。与往常一样,小整型会被提升 |
stoi(s, p, b), stol(s, p, b), stoul(s, p, b), stoll(s, p, b), stoull(s, p, b) | 返回s的起始子串(表示整数内容)的数值,返回值类型 分别是int、long、unsigned long、long long、 unsigned long long。b表示转换所用的基数(指定字符串中 数值的进制),默认值为10。p是size_t指针,用来保存 s中第一个 非数值字符的下标,p默认为0,即,函数不保存下标 |
stof(s, p), stod(s, p), stold(s,p) | 返回s的 起始子串(表示浮点数内容)的数值,返回值类型分别是 float、double或long double。参数p的作用 与整数转换函数中 一样 |
4、设计一个类,它有三个unsigned成员,分别表示年、月和日。为其编写构造函数,接受一个表示日期的string参数。构造函数应该能处理不同的数据格式,如January 1,1900、1/1/1990、Jan 1 1900 等
#include <iostream>
#include <string>
#include <array>
using namespace std;
class Date {
public:
Date() = default;
Date(string ss) :s(ss) { };
unsigned getMonth();
unsigned getDay();
unsigned getYear();
private:
unsigned year = 0;
unsigned month = 0;
unsigned day = 0;
const string months = "JanFebMarAprMayJunJulAugSepOctNovDec";
string s;
};
unsigned Date::getMonth()
{
string tmp_str(s.begin(), s.begin() + 3);
if (months.find(tmp_str) != string::npos) {
month = months.find(tmp_str) / 3 + 1;
return month;
}
else {
return stoi(tmp_str);
}
//month = MonthFromName(str.substr(0, month_day_delim_pos));
//if (std::isdigit(str[0])) return std::stoi(str); // 若月份为数字
//for (size_t i = 0; i != 12; ++i) { // 若月份为英文
// if (str.find(month_names[i]) != std::string::npos) return i + 1;
//}
}
unsigned Date::getDay()
{
unsigned be = s.find_first_of(" ,/");
unsigned en = s.find_last_of(" ,/");
string tmp_str(s.begin() + be + 1, s.begin() + en);
day = stoi(tmp_str);
return day;
}
unsigned Date::getYear()
{
unsigned be = s.find_last_of(" ,/");
string tmp = s.substr(be + 1);
year = stoi(tmp);
return year;
}
void print(Date d) {
cout << d.getYear() << "-" << d.getMonth() << "-" << d.getDay() << endl;
}
int main()
{
Date d1("January 1,1900");
print(d1);
Date d2("1/1/1990");
print(d2);
Date d3("Jan 1 1900");
print(d3);
return 0;
}
运行结果:
6、容器适配器
1、除了 顺序容器外,标准库还定义了 三个顺序容器适配器:stack、queue 和 priority_queue
2、一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。例如,stack适配器接受 一个顺序容器(除array或forward_list外),并使其操作起来 像一个stack一样
3、所有容器适配器都支持的操作和类型:
操作和类型 | 解释 |
---|---|
size_type | 一种类型,足以保存当前类型的 最大对象的大小 |
value_type | 元素类型 |
container_type | 实现 适配器 的底层容器类型 |
A a; | 创建一个名为a的空适配器 |
A a(c); | 创建一个名为a的适配器,带有 容器c的一个拷贝 |
关系运算符 | 每个适配器都支持所有关系运算符:==、!=、<、<=、>和>=。这些运算符返回底层容器的比较结果 |
a.empty() | 若a包含 任何元素,返回false,否则返回true |
a.size() | 返回a中的元素数目 |
swap(a, b), a.swap(b) | 交换a和b的内容,a和b必须有 相同类型,包括 底层容器类型也必须相同 |
4、定义一个适配器:每个适配器都定义 两个构造函数:默认构造函数 创建一个空对象,接受一个容器的构造函数 拷贝该容器 来初始化适配器。例如,假定deq 是一个deque<int>
,可以用deq来 初始化 一个新的stack,如下所示:
stack<int> stk(deq); //从deq拷贝元素到stk
5、默认情况下,stack和queue 是基于deque实现的,priority_queue是 在vector之上实现的。创建一个适配器时 将一个命名的顺序容器作为 第二个类型参数,来重载 默认容器类型
//在vector上实现的空栈
stack<string, vector<string>> str_stk;
//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构造
6、栈适配器:stack类型定义在 stack头文件中
stack<int> intStack://空栈
//填满栈
for(size_t ix = 0; ix != 10; ++ix)
//intStack保存 0到9 十个数
intStack.push(ix);
while(!intStack.empty()) { //intStack中有值 就继续循环
int value = intStack.top();
//使用栈顶值的代码
intStack.pop(); //弹出栈顶元素,继续循环
}
栈除了3中列出的操作之外,其余的栈操作:栈默认基于 deque实现,也可以在 list或vector之上实现
操作 | 解释 |
---|---|
s.pop() | 删除栈顶元素,但 不返回该元素值 |
s.push(item), s.emplace(args) | 创建一个新元素 压入栈顶,该元素通过 拷贝或移动item而来,或者 由args构造 |
s.top() | 返回栈顶元素,但不将元素弹出栈 |
每个容器适配器 都基于 底层容器类型的操作 定义了自己的特殊操作。只可以使用 适配器操作,而不能 使用底层容器类型的操作:
例如:不能在一个stack上调用 push_back,而必须 使用stack自己的操作 push
7、队列适配器:queue和priority_queue适配器 定义在queue头文件中
队列除了3中 列出的操作之外,其余的 队列(queue 和 priority_queue)操作:
queue默认 基于deque实现,priority_queue默认 基于vector实现
queue 也可以 用list或vector实现,priority_queue也可以 用deque实现
操作 | 解释 |
---|---|
q.pop() | 返回queue的首元素 或priority_queue的最高优先级的元素,但不删除此元素 |
q.front() | 返回 首元素或尾元素,但 不删除此元素 |
q.back() | 只适用于 queue |
q.top() | 返回 最高优先级元素,但 不删除该元素, 只适用于priority_queue |
q.push(item), q.emplace(args) | 在queue末尾 或priority_queue中 恰当的位置 创建一个元素,其值为item,或者由args构造 |
标准库queue使用一种 先进先出的存储 和访问策略。priority_queue允许 为队列中的元素 建立优先级。新加入的元素 会排在所有优先级 比它低的已有元素之前,标准库在元素类型上 使用<运算符来 确定相对优先级
小结和术语表
1、顺序容器有公共的标准接口:如果两个顺序容器 都提供个特定的操作,那么这个操作 在两个容器中 具有相同的接口和含义
2、所有容器(除array外)都提供 高效的动态内存管理。可以 向容器中添加元素 而不必担心 元素存储在哪里。容器负责管理自身的存储
3、deque顺序容器:deque中的元素 可以通过 位置下标来访问。支持 快速的随机访问。deque各方面都 与vector类似,唯一的
差别是,deque支持在 容器头尾位置的快速插入和删除,而且 在两端插入或删除元素都不会导致重新分配空间
4、forward_list上的迭代器 不支持递减运算(–)
5、list顺序容器:表示一个 双向链表。list中的元素 只能顺序访问。从一个 给定元素 开始,为了 访问另一个元素,只能遍历两者之间的所有元素。当加入新元素后,迭代器仍然有效。当 删除元素后,只有 原来指向被删除元素的迭代器 才会失效