C++ 学习笔记(三)(标准库类型顺序容器篇)

前言:主要是自己学习过程的积累笔记,所以跳跃性比较强,建议先自学后拿来作为复习用。

文章目录

  • 0 初始化的相关知识
    • 默认初始化和显示初始化
    • 普通初始化和列表初始化
    • 拷贝初始化和直接初始化
    • 值初始化
  • 1 顺序容器概述
  • 2 容器库概述
    • 2.1 迭代器
      • 2.1.1 迭代器范围
      • 2.1.2 为什么采用左闭右开?
        • 练习
    • 2.2 begin 和 end 成员
    • 2.3 容器定义和初始化
      • 2.3.1 容器的拷贝初始化
      • 2.3.2 列表初始化
      • 2.3.3 提供容器大小的初始化
      • 2.3.4 array 容器的特性
    • 2.5 赋值和 swap
    • 2.5.1 assign(仅顺序容器)
      • 2.5.2 swap
    • 2.6 关系运算符
  • 3 顺序容器操作
    • 3.1 添加元素
    • 3.1.1 push_back
      • 3.1.2 push_front
      • 3.1.3 在特定位置插入元素
    • 3.2 访问元素
      • 3.2.1 访问成员函数返回的是引用
    • 3.3 删除元素
    • 3.4 forward_list 的特殊操作
      • 练习
    • 3.5 改变容器大小
    • 3.6 迭代器失效的情形
  • 4 容器的空间管理
    • 4.1 vector 对象是如何增长的
    • 4.2 管理容量的成员函数
  • 5 容器适配器
  • 6 总结

0 初始化的相关知识

默认初始化和显示初始化

  • 内置类型定义变量时如果没有指定初值,则变量会被默认初始化,默认值由变量类型和位置来决定。如 int 型默认值为0,string 型默认值为空串。
  • 一个例外就是,内置类型的变量未被显式初始化时,若它是定义在 函数体的内部(包括 main 函数) 将不会被默认初始化,此时对它进行拷贝或者访问将会出错。一般地,只有全局变量才会进行默认初始化(在一些特定的语句中也可以执行默认初始化)。
  • 对于类来说,其自行决定初始化对象的方式。

普通初始化和列表初始化

定义一个 int 型的变量 a 并初始化为0:

int a = 0;
int a(0);
int a = {0};
int a{0};

后两条语句即被称为“列表初始化”。其中有一个注意点:对于内置类型的变量,使用列表初始化且初始值存在丢失信息的风险时,编译器可能会报错(部分编译器只会警告,初始化依然执行)。如下:

long double a = 3.1415926536;
int b{a}, c = {a}; 	// 错误:存在丢失信息的风险,列表初始化不执行
int d(a), e = a; 	// 正确:虽然丢失了部分值,普通初始化仍然执行

拷贝初始化和直接初始化

使用等号(=)初始化一个变量即为拷贝初始化,不用则为直接初始化。如:

string a = "hi";	// 拷贝初始化
string b("hi");		// 直接初始化
string c(3, 'i');	// 直接初始化,c 的内容是 iii

值初始化

在初始化标准库容器的时候,如果不提供显示的初始值,则库会创建一个值初始化的元素初值,并把它赋给容器中的所有元素,初值由容器对象中的元素类型决定,值初始化相当于容器上的默认初始化

1 顺序容器概述

顺序容器类型
vector 可变大小数组。支持快速随机访问。在尾部之外的位置插入/删除元素可能很
deque 双端队列。支持快速随机访问。在头尾位置插入/删除速度很
list 双向链表。只支持双向顺序访问。在 list 中任何位置进行插入/删除操作速度都很
forward_list 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除速度都很
array 固定数组大小。支持快速随机访问不能添加/删除元素
string 与 vector 相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度

对上表的一些补充:

  • 除了 array 外的其他容器都提供高效、灵活的内存管理。
  • string 和 vector 将元素存储在连续的内存空间中,所以支持随机访问,但在中间位置插入/删除元素会很耗费时间。而且如果插入元素时发现连续的存储空间不足,还需要将所有的元素都移动到新的存储空间中。
  • list 和 forward_list 的插入/删除操作很迅速,但由于其元素在存储空间中是离散分配的,因此访问一个元素需要遍历整个容器时间代价很大。元素之间的先后关系需要额外的东西来标记,因此其内存开销也较其他容器更
  • deque 支持随机访问,在其中间位置插入/删除的代价可能会很高,但是在其两端插入/删除元素会很快。

通常来讲,vector 是最好的顺序容器选择,除非有更好的理由选择其他容器。以下是一些选择时的基本原则:

  • 如果程序有许多小的元素,且对于空间开销使用很谨慎,不要使用 list 和 forward_list。
  • 如果要随机访问元素选用 vector 或 deque。
  • 如果要在容器中间插入/删除元素,选用 list 和 forward_list。
  • 如果仅在容器头尾插入/删除元素,选用 deque。
  • 如果程序在读取输入时在容器中间插入,之后需要随机访问容器:
  1. 全都插入 vector 中,再利用 sort 函数进行排序
  2. 在输入阶段使用 list,然后拷贝到 vector 中。
  • 如果程序既需要随机访问元素,又需要在容器中间插入/删除元素,则依据哪种操作占据主导地位来选择。

2 容器库概述

容器类型上的操作大致可以分为以下三类:

  • 所有容器类型都提供的操作。
  • 仅针对顺序容器、关联容器、或无序容器的操作。
  • 只适用于一小部分容器的操作。

顺序容器可以保存任意类型的元素,甚至说元素也是容器,但某些容器操作对于元素的类型是有限制的。下面是常见的容器操作(args 是一个用逗号分隔的一个或多个参数的列表):

类型别名
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
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,否则返回 true
添加/删除元素(不适用于array) 在不同容器中,这些操作的接口都不同
c.insert(args) 将 args 中的元素拷贝进 c
c.emplace(inits) 使用 inits 构造 c 中的一个元素
c.erase(args) 删除 args 指定的元素
c.clear() 删除 c 中的所有元素,返回 void
关系运算符
==,!= 所有容器都支持相等(不等)运算符
<,<=,>,>= 关系运算符(无序关联容器不支持)
获取迭代器
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 迭代器

标准容器迭代器支持的所有操作:

迭代器的操作
*iter 返回迭代器 iter 所指元素的引用
iter -> mem 解引用 iter 并获取该元素的名为 mem 的成员,等价于 (*iter).mem(括号不能少)
++iter 令 iter 指向容器(或 string 对象)的下一个元素(字符)
–iter 令 iter 指向容器(或 string 对象)的上一个元素(字符)
iter1 == iter2 两个迭代器指向同一个元素(字符),或者都是同一个容器的尾后迭代器时相等,返回 true
iter1 != iter2 上面的返回结果取反

string、vector、deque、array 迭代器支持的运算(forward_list 迭代器不支持减运算符 “- -”):

迭代器的运算
iter + n 或 iter += n 迭代器向前移动 n 个元素(字符),指向容器(或 string 对象)的另一个元素(字符),或者变成尾迭代器
iter - n 或 iter -= n 迭代器向后移动 n 个元素(习惯),指向容器(或 string 对象)的另一个元素(字符),或者变成尾迭代器
iter1 - iter2 两个迭代器相减的结果是它们之间的距离(是一个数),这两个迭代器必须指向同一个容器
>,>=,<,<= 迭代器的关系运算符,这两个迭代器必须指向同一个容器

2.1.1 迭代器范围

一个迭代器范围由一对迭代器(begin 和 end)表示。两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置,标记了容器中元素的一个范围。它是一个左闭右开的范围,即 [begin, end)。begin 和 end 必须指向同一个容器,也可以指向容器中的相同位置,但 end 一定不能指向 begin 之前的位置。

2.1.2 为什么采用左闭右开?

主要有三个理由:

  • 如果 begin 等于 end,则范围为空。
  • 如果 begin 不等于 end,则范围内至少有一个元素,且 begin 指向范围中的第一个元素,也因此可以安全的解引用 begin。
  • 对 begin 一直递增会使得 begin 最终等于 end。

练习

编写函数,接受一对指向 vector 的迭代器和一个 int 值。在两个迭代器指定的范围中查找给定的值,返回一个布尔值来指出是否找到。

一定要注意给 iterator 加上完整的作用域运算符。就类似于指针,用 vector::iterator 指明该对象是迭代器,用 < int> 表明迭代器指向的是整型数据。事实上,准确的说,vector< int> 是一个类,所以它的迭代器就必须加上类的作用域运算符:

#include
#include
using namespace std;

bool Search(vector<int>::const_iterator begin, vector<int>::const_iterator end, int x)
{	// 迭代器所指元素不修改,所以传入 const_iterator
    while (begin != end)    // 循环在两个迭代器相等时结束
        if (*begin++ == x)  // 如果 begin 所指元素等于给定值 x 返回 true
            return true;	// 执行完判断后 begin 自增
    return false;           // 循环正常结束说明没找到给定的值,返回 false
}

int main()
{
    vector<int> vec{1, 2, 3, 4, 5};
    int x;
    cin >> x;
    bool flag = Search(vec.cbegin(), vec.cend(), x);
    if (flag)                           // 依据 flag 的值来输出不同的情况
        cout << "found" << endl;
    else
        cout << "not found" << endl;

    return 0;
}

2.2 begin 和 end 成员

begin 和 end 成员函数分别返回指向容器中第一个元素尾元素之后位置的迭代器。它们有多个版本,带 r 的版本返回反向迭代器,以 c 开头的版本返回 const 迭代器,其中不以 c 开头的成员函数(begin、rbegin、end、rend)都是被重载过的。因为这些函数实际上都有一个同名的成员函数,非常量对象调用这些成员函数时返回 iterator 类型,常量对象调用这些函数时返回 const_iterator 类型。与 const 指针和引用类似,可以将一个普通的 iterator 转换为对应的 const_iterator,反之则不行。以 c 开头的版本是 C++ 新标准引入的,就是用来支持 auto 与 begin 和 end 函数结合使用的,这样程序员就不用去费心思思考 it 的类型了:

auto it = a.begin();	// a 是 const 时,返回 const_iterator;反之返回 iterator

2.3 容器定义和初始化

所有容器都有默认构造函数,除 array 之外的所有容器默认构造函数都会创建一个指定类型的空容器,且都能够接受指定容器大小和元素初始值的参数。

容器定义和初始化
C c; 默认构造函数。如果 C 是一个 array,则 c 中元素按默认方式初始化;否则 c 为空
C c1(c2) c1 初始化为 c2 的拷贝。c1 和 c2 必须是相同类型(即,它们必须是相同的容器类型,且保存的是相同的元素类型;对于 array 类型,两者还必须具有相同大小)
C c1 = c2 等价于上句
C c{a, b, c…} c 初始化为列表中元素的拷贝。列表中元素的类型必须与 C 的元素类型相容。对于 array 类型,列表中元素数目必须等于或小于 array 的大小,任何遗漏的元素都进行值初始化
C c = {a, b, c…} 等价于上句
C c(b, e) c 初始化为迭代器 b 和 e 指定范围中的元素的拷贝。范围中元素的类型必须与 C 的元素类型相容(array 不适用)
C seq(n) seq 包含 n 个元素,这些元素进行了值初始化,此构造函数是 explicit 的(srting 不是用)
C seg(n, t) seq 包含 n 个初始化为值 t 的元素

2.3.1 容器的拷贝初始化

两个方法:直接拷贝整个容器,或者拷贝由一对迭代器指定的范围内的元素。直接拷贝整个容器时,要求容器类型元素类型都必须相同;使用迭代器范围来拷贝元素时就不需要容器类型相同了,只要求原容器中的元素类型可以转换成新容器的类型即可:

// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"}l

list<string> list2(authors);		// 正确:类型匹配
deque<string> authList(authors);	// 错误:容器类型不匹配
vector<string> words(articles);		// 错误:容器类型不匹配
// 正确:可以将 const char* 类型转换为 string
forward_list<string> words(articles.begin(), atricles.end());

2.3.2 列表初始化

可以对一个容器进行列表初始化,除 array 之外,容器的初始大小和初始值一样多:

// 每个容器有三个元素,用给定的初始值进行初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};

2.3.3 提供容器大小的初始化

顺序容器(array 除外)提供另一个构造函数:接受一个容器大小和一个元素初始值。不提供元素初始值的话,编译器会对容器执行值初始化:

vector<int> vec1(10, -1);		// 10个 int 元素,每个都初始化为-1
list<string> vec2(10, "hi");	// 10个 string,每个都初始化为 “hi”
forward_lis<int> vec3(10);		// 10个 int 元素,每个都值初始化为0
deque<string> vec4(10);			// 10个 string,每个都是空串

2.3.4 array 容器的特性

定义 array 时必须同时指定容器的大小,也因此所有默认构造的 array 都是非空的,所有的元素都被默认初始化,相当于一个固定了大小的 vector。如果采用列表初始化 array,初始值的数目必须小于等于容器大小,n 个初始值只能用来初始化前 n 个元素,剩余的都执行值初始化

array<int, 5> arr1;						// 5个默认初始化
array<int, 5> arr2 = {0, 1, 2, 3, 4};	// 列表初始化
array<int, 5> arr3 = {52};				// arr3[0] 为52,剩余元素为0

内置数组类型不能拷贝或赋值,但 array 并无此限制,因此不要将 array 和内置数组等同。进行拷贝或赋值时,array 要求元素类型和容器大小都得相同:

int a[5] = {0, 1, 2, 3, 4};
int b[5] = digs;			// 错误:内置数组不能拷贝或赋值
array<int, 5> arr1 = {0, 1, 2, 3, 4};
array<int, 5> arr2 = a;		// 正确:后两条均正确,只要数组类型匹配就合法

2.5 赋值和 swap

注意区分赋值和初始化的区别。赋值是一个运算,拷贝是具体的操作。

assign 操作不适用于关联容器和 array。

容器赋值运算
c1 = c2 将 c1 中的元素替换为 c2 中元素的拷贝。c1 和 c2 必须具有相同的类型
c1 = {a, b, c} 将 c1 中元素替换为列表中元素的拷贝(array 不适用)
swap(c1, c2) 交换 c1 和 c2 中的元素。c1 和 c2 必须具有相同的类型
c1.swap(c2) swap 通常比从 c2 向 c1拷贝元素快得多
seq.assign(b, e) 将 seq 中的元素替换为迭代器 b 和 e 所表示范围中的元素。迭代器 b 和 e 不能指向 seq 中的元素
seq.assign(list) 将 seq 中的元素替换为列表 list 中的元素
seq.assign(n, t) 将 seq 中的元素替换为 n 个值为 t 的元素

上表中的赋值运算适用于所有容器。赋值运算符将其左边容器中的全部元素替换为右边容器中元素的拷贝。如果两个容器原来大小不同,赋值运算后左边的容器会和右边容器一样大,哪怕是小容器赋值给大容器也是如此。也因此 array 只有在两侧类型和大小都一样时才能使用赋值运算,也不允许用花括号来进行赋值(列表初始化 ≠ 列表赋值)。

还有一点要注意,赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而 swap 操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(容器类型为 array 和 string 的情况除外)。

2.5.1 assign(仅顺序容器)

顺序容器的 assign 成员允许它们从一个不同的容器类型进行赋值,或者仅仅是其中的某一子序列。assign 用参数来指定范围进行拷贝:

list<string> lst;
vector<const char*> vec;
lst = vec;		// 错误:容器类型不匹配
// 正确:assign 可以将 const char* 转换为 string
lst.assign(vec.cbegin(), vec.end());

由于旧元素没有了,假设 X 容器调用了 assign,就不能将指向 X 的迭代器传递给 assign:

lst.assign(lst.cbegin(), lst.end());	// 错误:不能传递指向容器本身的迭代器

assign 的第二个版本接受一个整型值和一个元素值:

list<string> lst(1);	// 1个元素,为空 string
list.assign(10, "hi");	// 10个元素,每个都是 "hi"
// 上面两条语句等价于下面两条
list.clear();
list.insert(list,begin(), 10, "hi");

2.5.2 swap

swap 用于交换两个同类型容器的元素:

vector<string> vec1(10);	// 10个空 string 的 vector
vector<string> vec2(20);	// 20个空 string 的 vector
swap(vec1, vec2);	// 调用非成员 swap
vec1.swap(vec2);	// 调用成员 swap

上述语句执行后,vec1 有20个元素,而 vec2 有10个元素。实际上,swap 只是交换了两个容器的内部数据结构,并不进行任何元素的拷贝、删除和插入。因此指向容器的迭代器、引用和指针在 swap 执行之后都不会失效(string 对象除外)。例如 iter 指向的是 vec1[1],在 swap 之后指向的就是 vec2[1] 了。可以粗浅地理解为,swap 只不过是互换了两个容器的名字(实际不止这样)。

但是,swap 两个 array 会真正地交换它们的元素,也因此交换 array 的时间(与 array 的大小成正比)要高于交换其它容器。swap 之后,指针、引用和迭代器绑定的元素不会变,但是元素值已经是被交换过了。新标准里,容器既提供成员函数版本的 swap,也提供非成员版本的。建议统一使用非成员版本。

2.6 关系运算符

注意几点:

  • 所有容器都支持相等运算符(== 和 !=)。
  • 除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。
  • 关系运算符两侧必须是同容器类型和元素类型

容器之间的大小比较类似于 string 或者 C 语言字符串的大小比较。

有下面一个例子,读者不要混淆了:

vector<int> vec1 = {1, 3, 6};	// 6 > 5,所以 vec1 > vec2
vector<int> vec2 = {1, 3, 5, 7, 9}
string s1 = "abce";				// e > d,所以 s1 > s2
string s2 = "abcdefg";

比较后你会发现其实是 vec1 > vec2,s1 > s2。长的不一定更大,比较大小还是得看元素的大小。

3 顺序容器操作

在 [目录2] 中介绍的是所有容器都支持的操作,下面是顺序容器特有的操作。

3.1 添加元素

所有顺序容器都提供灵活的内存管理(array 除外):运行时可以动态添加或删除元素。

顺序容器添加元素
c.push_back(t) 在 c 的尾部创建一个值为 t 或由 args 创建的元素。返回 void
c.emplace_back(args) 等价函数
c,push_front(t) 在 c 的头部创建一个值为 t 或由 args 创建的元素。返回 void
c.emplace_front(args) 等价函数
c.insert(p, t) 迭代器 p 指向的元素之前创建一个值为 t 或由 args 创建的元素。返回指向新添加元素的迭代器
c.emplace(p, args) 等价函数
c.insert(p, n, t) 迭代器 p 指向的元素之前创建 n 个值为 t 的元素。返回指向新添加的第一个元素的迭代器
c.insert(p, b, e) 迭代器 b 和 e 指定的范围内的元素插入到迭代器 p 指向的元素之前。b 和 e 不能指向 c 中的元素。返回指向新添加的第一个元素的迭代器;若范围为空,则返回 p
c.insert(p, il) il 是一个花括号包围的元素值列表。将这些给定值插入到迭代器 p 指向的元素之前。返回指向新添加的第一个元素的迭代器;若列表为空,则返回 p

有几点要注意:

  • forwar_list 有自己专用版本的 insert 和 emplace,且不支持 push_back 和emplace_back。
  • vector 和 string 不支持 push_front 和 emplace_front,即不能在头部插入元素。
  • 向一个 vector、string 或 deque 中插入元素会使所有指向容器的迭代器、指针和引用失效。

3.1.1 push_back

除了 array 和 forward_list 之外,每个顺序容器(包括 string)都支持 push_back:

Typename tail;
while (cin >> tail)		// 从标准输入读取数据,将每个元素放到容器末尾
	container.push_back(tail);

容器元素都是拷贝的:这句话表明,当我们用一个对象来初始化容器,或将一个对象插入到容器中时,实际上放入的是对象值的一个拷贝,而非对象本身。拷贝完成后,容器中的元素与对象之间就没有任何关联了。

3.1.2 push_front

list、forward_list 和 deque 容器还支持名为 push_front 操作,此操作将单个元素插入到容器头部。因为只有双链表、单链表和双端队列在头部插入元素比较方便,都只花费常数时间。

3.1.3 在特定位置插入元素

一般用 insert 成员在容器中的任意位置插入0个或多个元素。vector、deque、list 和 string 都支持 insert,而 forward_list 提供了特殊版本的 insert。每个 insert 函数都接受一个迭代器作为其第一个参数,并将元素插入到迭代器所指位置之前。因此对于不支持 push_front 操作的容器(vector 和 string),可以利用 insert 将元素插到它们的头部,只是说会很耗时罢了

insert 函数还能接受更多的参数,类似于容器的构造函数:

  • 接受一个元素数目和一个值,将指定数量的值初始化过的元素添加到指定位置之前
  • 接受一对迭代器或一个初始化列表的 insert 将给定范围中的元素插入到指定位置之前
vector<string> vec = {"a", "b", "c", "d", "e"};
// 将 vec 的最后两个元素添加到 list 的开始位置
list.insert(list.begin(), vec.end() - 2, vec.end());

上述两个版本的 insert 返回指向第一个新加入元素的迭代器。如果范围为空则不插入任何元素,insert 返回第一个参数。

3.2 访问元素

下表是在顺序容器访问元素的操作(如果容器中没有元素,访问操作的结果是未定义的):

访问元素的操作
c.back() 返回 c 中尾元素引用。若 c 为空,函数行为未定义
c.front() 返回 c 中首元素引用。若 c 为空,函数行为未定义
c[n] 返回 c 中下标为 n 的元素的引用,n 是一个无符号整数。若 n >= c.size(),则函数行为未定义
c.at(n) 返回下标为 n 的元素的引用。如果下标越界,则抛出 out_of_range 异常

所有顺序容器都有一个 front 成员函数,除了 forward_list 之外的所有顺序容器都有一个 back 成员函数,它俩分别返回首元素尾元素引用

// 在解引用一个迭代器或调用 front 或 back 之前先检查是否有元素
if (!c.empty())
{
	// val1 和 val2 是 c 中第一个元素值的拷贝
	auto val1 = *c.begin(), val2 = c.front();
	// val3 和 val4 是 c 中最后一个元素值的拷贝
	auto last = c.end();
	auto val3 = *(--last);	// 正确,但是要注意不能递减 forward_list 的迭代器
	auto val4 = c.back();	// 错误:forward_list 没有 back 成员函数
}

3.2.1 访问成员函数返回的是引用

在容器中访问元素的成员函数(front、back、下标和 at)返回的都是引用。如果容器是一个 const 对象,返回的是 const 的引用,否则是普通引用。因此如果想改变返回的值,必须要定义引用来接受:

if (!c.empty)
{
    c.front() = 32;         // 将32赋予 c 中的第一个元素
    auto &v = c.back();     // 获得指向最后一个元素的引用
    v = 1024;               // 改变 c 中的元素
    auto v2 = c.back();     // v2 不是一个引用,它是 c.back() 的一个拷贝
    v2 = 0;                 // 未改变 c 中的值
}

3.3 删除元素

下表是顺序容器(array 除外)删除元素的多种操作:

删除元素的操作
c.pop_back() 删除 c 中的尾元素。若 c 为空,则函数行为未定义。返回 void
c.pop_front() 删除 c 中的首元素。若 c 为空,则函数行为未定义。返回 void
c.erase( p) 删除迭代器 p 所指的元素,返回一个指向被删元素之后元素的迭代器,若 p 指向尾元素,则返回尾后迭代器。若 p 是尾后迭代器,则函数行为未定义
c.erase(b, e) 删除迭代器 b 和 e 所指范围内的元素。返回一个指向最后一个被删除元素之后元素的迭代器,若 e 本身就是尾后迭代器,则函数也返回尾后迭代器
c.clear() 删除 c 中的所有元素。返回 void

有几点要注意:

  • forward_list 有特殊版本的 erase。
  • forward_list 不支持 pop_back;vector 和 string 不支持 pop_front。
  • 删除 deque 中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。
  • 指向 vector 和 string 中删除点之后位置的迭代器、引用和指针都会失效。
  • 删除元素的成员函数并不检查其参数,因此在删除之前必须先检查待元素是否存在。

3.4 forward_list 的特殊操作

作为单向链表,我们无法直接访问其中某个结点的直接前驱,因此它的删除和插入操作都有别于其它容器:

插入或删除元素的操作
lst.befor_begin() 返回指向链表首元素之前不存在的元素的迭代器。此迭代器不能解引用
lst.cbefor_begin() 返回一个 const_iterator
lst.insert_after(p, t) 在迭代器 p 之后的位置插入元素。t 是一个对象,n 是数量
lst.insert_after(p, n, t) 同上
lst.insert_after(p, b, e) b 和 e 是表示范围的一对迭代器(b 和 e 不能指向 lst 内)
lst.insert_after(p, il) il 是一个花括号列表。返回一个指向最后一个插入元素迭代器。如果列表为空,则返回 p。若 p 为尾后迭代器,则函数行为未定义。
emplace_after(p, args) 使用 args 在 p 指向的位置之后创建一个元素。返回一个指向这个新元素迭代器。若 p 为尾后迭代器,则函数行为未定义
lst.erass_after§ 删除 p 指向的位置之后的元素,或删除从 b 之后直到(但不包含)e 之间的元素(接下行)
lst.erase_after(b, e) 返回一个指向被删除元素之后元素的迭代器。如果 p 指向 lst 的尾元素或者是一个尾后迭代器,则函数行为未定义

练习

编写函数,接受一个 forward_list< string> 和两个 string 共三个参数。函数应在链表中查找第一个 string,并将第二个 string 插入到紧接着第一个 string 之后的位置。若第一个 string 未在链表中,则将第二个 string 插入到链表末尾。

在 forward_list 中添加或删除元素时,必须关注两个迭代器:一个指向当前处理的,另一个指向前驱。就类似于遍历单链表的过程,需要两个指针分别指向当前结点和其前驱结点。

#include
#include
#include
using namespace std;

void inserString(forward_list<string> &flst, const string s1, const string s2)
{
    // curr 指向当前要处理的元素,prev 指向其前驱
    auto curr = flst.begin(), prev = flst.before_begin();
    while (curr != flst.end())  // curr 成为尾后迭代器时退出循环
    {
        if (*curr == s1)        // 如果当前元素是 s1 则直接在其后插入 s2
        {
            flst.insert_after(curr, s2);
            return;             // 退出函数
        }
        prev = curr++;          // 当前元素不是 s1,两个迭代器均往后移继续查找
    }

    // flst 中没有 s1,prev 此时指向链表的最后一个元素,在其后插入 s2 即可
    flst.insert_after(prev, s2);
}

int main()
{
    forward_list<string> flst;
    flst.insert_after(flst.before_begin(), { "a", "b", "c", "d" });

    string s1, s2;
    cin >> s1 >> s2;
    inserString(flst, s1, s2);

    for (const auto i : flst)
        cout << i << " ";

    return 0;
}

3.5 改变容器大小

容器的大小可以改变,如下表所示:

顺序容器大小操作
c.resize(n) 调整 c 的大小为 n 个元素。若 n < c.size(),则多出的元素被丢弃。若必须添加新元素,对新元素进行值初始化
c.resize(n, t) 调整 c 的大小为 n 个元素。任何新添加的元素都初始化为 t

如果 resize 用于缩小容器,则指向被删除元素的迭代器、引用或指针都会失效;对 vector、string 或 deque 进行 resize 可能导致迭代器、引用和指针失效。array 不支持 resize。

3.6 迭代器失效的情形

指向容器的迭代器、引用或指针在添加或删除元素之后可能会发生失效的情况,具体有以下的情形。

向容器添加元素后:

  • 对于 vector 或 string,如果存储空间被重新分配,则全都失效。如果未重新分配,插入元素之前的仍有效,之后的失效。
  • 对于 deque,将元素插入到除首尾之外的任何位置都会导致失效。如果插入到首尾位置,迭代器会失效,但指向存在的元素的引用和指针不会失效。
  • 对于 list 和 forward_list,不会失效。

从容器删除元素后:

  • 对于 vector 或 string,被删除元素之前的仍有效,删除之后的失效。注意:删除元素时,尾后迭代器总是会失效
  • 对于 deque,将除首尾之外的任何位置上的元素删除都会导致失效。如果删除尾元素,则尾后迭代器也会失效,其他不受影响;删除首元素所有都不会影响。
  • 对于 list 和 forward_list,不会失效。

必须保证每次改变容器的操作之后都正确地重新定位迭代器,对于 vector、string 和 deque 尤为重要。

4 容器的空间管理

4.1 vector 对象是如何增长的

为了支持快速随机访问,vector 像数组一样将元素存储在连续的内存空间中。在添加新元素时,如果空间不足,vector 会重新分配一个比当前更大的内存空间。然后将之前的所有元素重新移动到这个新空间中。也因此之前的迭代器、引用或指针都会失效。

4.2 管理容量的成员函数

容器的 size 是指它已经保存的元素的数目;而 capacity 则是容器当前最多可保存元素的数目。

容器大小管理操作
c.shrink_to_fit() 请将 capacity() 减少为与 size() 相同大小
c.capacity() 不重新分配内存空间的话,c 可以保存多少元素
c.reserve(n) 分配至少能容纳 n 个元素的内存空间

有以下几点需要注意:

  • shrink_to_fit 只适用于 vector、string 和 deque。
  • capacity 和 reserve 只适用于 vector 和 string。

只有当需求的空间大于容器当前容量时,调用 reserve 才会至少分配与需求一样大,甚至更大的内存空间;如果需求小于容量,reserve 函数什么也不做,也不会出现减少容器占用内存空间的情况。同样的,resize 只改变容器中元素的数目,而不是容器的容量,同样不能用 resize 来减少容器预留的内存空间。只有 shrink_to_fit 才能完成此操作。

5 容器适配器

适配器是标准库中的一个通用概念。容器。迭代器和函数都有适配器。本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。例如,stack 适配器接受一个顺序容器(array 和 forward_list 除外),并使其操作起来像一个 stack 一样。下表是所有容器适配器都支持的操作和类型。

操作和类型
size_type 一种类型,足以保存当前类型的最大对象的大小
value_type 元素类型
container_type 实现适配器的底层容器类型
A a; 创建一个名为 a 的空适配器
A a©; 创建一个名为 a 的适配器,带有一个容器 c 的拷贝
关系运算符 每个适配器都支持所有关系运算符:==、!=、<、<=、> 和 >=。这些运算符返回底层容器的比较结果
a.empty() 若 a 包含任何元素,返回 false,否则返回 true
a.size() 返回 a 中的元素数目
swap(a, b) 交换 a 和 b 的内容,a 和 b 必须有相同类型,包括底层容器类型也必须相同
a.swap(b) 等价语句

每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。例如,假定 deq 是一个 deque< int>,可以用 deq 来初始化一个新的 stack:

stack<int> stk(deq);	// 从 deq 拷贝元素到 stk

6 总结

标准库容器是模板类型,用来保存给定类型的对象。在一个顺序容器中,元素是按顺序存放的,通过位置来访问。顺序容器有公共的标准接口:如果两个顺序容器都提供一个特定的操作,那么这个操作在两个容器中具有相同的接口和含义。

所有容器(除array外)都提供高效的动态内存管理。我们可以向容器中添加元素,而不必担心元素存储在哪里。容器负责管理自身的存储。vector 和 string 都提供更细致的内存管理控制,这是通过它们的 reserve 和 capacity 成员函数来实现的。

很大程度上,容器只定义了极少的操作。每个容器都定义了构造函数、添加和删除元素的操作、确定容器大小的操作以及返回指向特定元素的迭代器的操作。其他一些有用的操作,如排序或搜索,并不是由容器类型定义的,而是由标准库算法实现的。

当我们使用添加和删除元素的容器操作时,必须注意这些操作可能使指向容器中元素的迭代器、指针或引用失效。很多会使迭代器失效的操作,如 insert 和 erase,都会返回一个新的迭代器,来帮助程序员维护容器中的位置。如果循环程序中使用了改变容器大小的操作,就要尤其小心其中迭代器、指针和引用的使用。


希望本篇博客能对你有所帮助,也希望看官能动动小手点个赞哟~~。

你可能感兴趣的:(学习总结,c++,学习,开发语言,标准库容器,笔记)