C++ Primer 总结索引 | 第九章:顺序容器

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, lenstr中从pos开始最多len个字
cp, len从cp指向的字符数组 的前(最多)len个字符
cpcp指向的 以空字符结尾的字符数组
n, cn个字符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中的元素 只能顺序访问。从一个 给定元素 开始,为了 访问另一个元素,只能遍历两者之间的所有元素。当加入新元素后,迭代器仍然有效。当 删除元素后,只有 原来指向被删除元素的迭代器 才会失效

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值