Loading

C++ STL 系列——序列式容器(Array、Vector、Deque、List、Forward_list)

一、什么是序列容器

序列容器即以线性排序来存储某一特定类型数据的容器,该类容器不会自动对存储的元素按照元素值的大小进行排序。

序列容器大致包含以下几类:

  • array<T,N>(数组容器):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,不能增加或删除元素,只能改变某个元素的值;
  • vector(向量容器):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(O(1)),在其它位置插入或删除元素效率较差(O(n));
  • deque(双端队列容器):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效 O(1) ,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;
  • list(链表容器):一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
  • forward_list(正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。

其实 stack 和 queue 本质上也属于序列容器,只不过它们都是在 deque 容器的基础上改头换面而成,通常更习惯称它们为容器适配器。

容器中常见的函数成员

下表展示了 array、vector 和 deque 容器的函数成员,它们中至少有两个容器实现了同样的函数成员。

函数成员 函数功能 array<T,N> vector deque
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的迭代器。
rend() 返回指向第一个元素所在位置前一个位置的迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
assign() 用新元素替换原有内容。 -
operator=() 复制同类型容器的元素,或者用初始化列表替换现有内容。
size() 返回实际元素个数。
max_size() 返回元素个数的最大值。这通常是一个很大的值,一般是 2^32-1,所以我们很少会用到这个函数。
capacity() 返回当前容量。 - -
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
resize() 改变实际元素的个数。 -
shrink _to_fit() 将内存减少到等于当前元素实际所使用的大小。 -
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
operator 使用索引访问元素。
at() 使用经过边界检査的索引访问元素。
push_back() 在序列的尾部添加一个元素。 -
insert() 在指定的位置插入一个或多个元素。 -
emplace() 在指定的位置直接生成一个元素。 -
emplace_back() 在序列尾部生成一个元素。 -
pop_back() 移出序列尾部的元素。 -
erase() 移出一个元素或一段元素。 -
clear() 移出所有的元素,容器大小变为 0。 -
swap() 交换两个容器的所有元素。
data() 返回指向容器中第一个元素的指针。 -

list 和 forward_list 容器彼此非常相似,forward_list 中包含了 list 的大部分成员函数,而未包含那些需要反向遍历的函数。下表展示
了 list 和 forward_list 的函数成员。

函数成员 函数功能 list forward_list
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器。
rbegin() 返回指向最后一个元素的迭代器。 -
rend() 返回指向第一个元素所在位置前一个位置的迭代器。 -
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
before_begin() 返回指向第一个元素前一个位置的迭代器。 -
cbefore_begin() 和 before_begin() 功能相同,只不过在其基础上,增加了 const 属性,即不能用该指针修改元素的值。 -
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 -
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 -
assign() 用新元素替换原有内容。
operator=() 复制同类型容器的元素,或者用初始化列表替换现有内容。
size() 返回实际元素个数。 -
max_size() 返回元素个数的最大值,这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
resize() 改变实际元素的个数。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
front() 返回容器中第一个元素的引用。
back() 返回容器中最后一个元素的引用。 -
push_back() 在序列的尾部添加一个元素。 -
push_front() 在序列的起始位置添加一个元素。
emplace() 在指定位置直接生成一个元素。 -
emplace_after() 在指定位置的后面直接生成一个元素。 -
emplace_back() 在序列尾部生成一个元素。 -
cmplacc_front() 在序列的起始位生成一个元索。
insert() 在指定的位置插入一个或多个元素。 -
insert_after() 在指定位置的后面插入一个或多个元素。 -
pop_back() 移除序列尾部的元素。 -
pop_front() 移除序列头部的元素。
reverse() 反转容器中某一段的元素。
erase() 移除指定位置的一个元素或一段元素。 -
erase_after() 移除指定位置后面的一个元素或一段元素。 -
remove() 移除所有和参数匹配的元素。
remove_if() 移除满足一元函数条件的所有元素。
unique() 移除所有连续重复的元素。
clear() 移除所有的元素,容器大小变为 0。
swap() 交换两个容器的所有元素。
sort() 对元素进行排序。
merge() 合并两个有序容器。
splice() 移动指定位置前面的所有元素到另一个同类型的 list 中。 -
splice_after() 移动指定位置后面的所有元素到另一个同类型的 list 中。 -

深入理解容器如何组织元素后,自然会知道哪个容器能使用哪些成员函数。

二、array 容器

array 容器是 C++ 11 标准中新增的序列容器。

  • 在 C++ 普通数组的基础上,添加了一些成员函数和全局函数
  • 使用上比普通数组更安全,且效率没有因此变差
  • 大小固定

array 容器以类模板的形式定义在 头文件,并位于命名空间 std 中,如下所示:

namespace std{
    template <typename T, size_t N>
    class array;
}

2.1 array 容器初始化

// 创建一个包含10个浮点型元素的容器,各个元素的值不确定(array 容器不做默认初始化)
std::array<double, 10> values;

// 将所有元素初始化为0.0
std::array<double, 10> values {};

// 只初始化前 4 个元素,剩余元素会被初始化为 0.0
std::array<double, 10> values {0.5, 1.0, 1.5, 2.0};

2.2 array 容器成员函数汇总

成员函数 功能
begin() 返回指向容器中第一个元素的随机访问迭代器。
end() 返回指向容器最后一个元素之后一个位置的随机访问迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的随机访问迭代器。
rend() 返回指向第一个元素之前一个位置的随机访问迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
size() 返回容器中当前元素的数量,其值始终等于初始化 array 类的第二个模板参数 N。
max_size() 返回容器可容纳元素的最大数量,其值始终等于初始化 array 类的第二个模板参数 N。
empty() 判断容器是否为空,和通过 size()==0 的判断条件功能相同,但其效率可能更快。
at(n) 返回容器中 n 位置处元素的引用,该函数自动检查 n 是否在有效的范围内,如果不是则抛出 out_of_range 异常。
front() 返回容器中第一个元素的直接引用,该函数不适用于空的 array 容器。
back() 返回容器中最后一个元素的直接应用,该函数同样不适用于空的 array 容器。
data() 返回一个指向容器首个元素的指针。利用该指针,可实现复制容器中所有元素等类似功能。
fill(val) 将 val 这个值赋值给容器中的每个元素。
array1.swap(array2) 交换 array1 和 array2 容器中的所有元素,但前提是它们具有相同的长度和类型。
  • C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 array 容器包含的 begin() 和 end() 成员函数不同的是,标准
    库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。当操作普通数组时,bengin() 返回第一个元素的指针,end()返回最后一个元素之后的指针。
  • <array> 头文件中还重载了 get() 全局函数,该重载函数的功能是访问容器中指定的元素,并返回该元素的引用。
  • 正是由于 array 容器中包含了 at()/get() 这样的成员函数,使得操作元素时比普通数组更安全。

2.3 array 随机访问迭代器

STL 为 array 容器配备了随机访问迭代器。

array 支持迭代器的成员函数

成员函数 功能
begin() 返回指向容器中第一个元素的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。
end() 返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。
rbegin() 返回指向最后一个元素的反向迭代器;如果是 const 类型容器,在该函数返回的是常量反向迭代器。
rend() 返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,在该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。
cbegin() 和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
cend() 和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。
crend() 和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。

除此之外,C++ 11 标准新增的 begin() 和 end() 函数,当操作对象为 array 容器时,其功能和表中成员函数的具体功能相同。
以上函数在实际使用时,返回值类型都可以用 auto 关键字代替,编译器可以自行判断出该迭代器的类型。

array 容器在普通数组上的升级

  • array 容器与普通数组存储数据的方式一样,存储的所有元素一定会位于连续且相邻的内存中。意味着可以通过地址访问之后的数据。
  • 容器迭代器和指针不能混用,即 a.begin() 不能代替 &a[0] 或者 a.data()。
  • at()函数能有效防止越界操作数组; fill() 函数可以实现数组的快速初始化; swap() 函数可以轻松实现两个相同数组(类型、大小相同)元素的互换。
  • 两个 array 容器满足大小、类型相同时,可以直接做赋值操作。
  • 两个 array 容器满足大小、类型相同时,且元素支持比较运算符时,可以用任何运算符直接比较两个 array 容器。
  • array 容器的代码功能实现效率、程序执行效率都比普通数组更高。

2.4 array 容器访问元素的几种方式

访问单个元素

  • 通过容器名[] 的方式
  • 通过 at() 成员函数
  • 通过 get<n> 函数模板
  • 通过 data() 成员函数
values[4] = values[3] + 2.0 * values[1];
values.at (4) = values.at(3) + 2.O*values.at(1);    
  • 建议使用 at(),除非索引没有越界。
  • array 容器重载 [] 运算符时,因为性能,没有实现便捷检查的功能。
  • get<n> 模板函数,能获取容器的第 n 个元素,该函数模板参数的实参必须是一个在编译时可以确定的常量表达式,所以不能是循环变量。
  • data() 成员函数可以得到指向容器首个元素的指针,通过该指针可以获得容器种的各个元素
// get<n> 模板函数的使用
array<string, 5> words{ "one","two","three","four","five" };
cout << get<3>(words) << endl;          // Output words[3]
cout << get<6>(words) << std::endl;     //越界,会发生编译错误

cout << *( words.data()+1);             // Output words[1]

访问多个元素

  • size() 函数能返回容器中元素的个数,所以能像下面一个逐个提取元素
double total = 0;
for(size_t i = 0; i < values.size(); ++i)
    total += values[i];

if(values.empty())
    std::cout << "The container has no elements.\n";
else
    std::cout << "The container has "<< values.size()<<"elements.\n";
  • 使用迭代器,基于范围的循环
double total = 0;
for(auto &value : values)
    total += value;

三、vector 容器

array 实现的是静态数组(容量固定),vector 实现的是动态数组,即可以进行元素的插入和删除,vector 会动态调整所占用的内存空间。

vector 常被称为向量容器,因为该容器擅长在尾部插入或删除元素,在常量时间内就可以完成,时间复杂度为 O(1);而对于在容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶 O(n)。

3.1 创建 vector 容器的几种方式

// 方式一:
std::vector<double> values;
values.reserve(20);             // 增加容器容量,如果已经 >= 20 个元素,就什么都不做

使用 reserve 增加容器容量时,之前创好的迭代器都可能失效,因为 vector 容器元素可能已经被复制或移到新的内存地址。

// 方式二:
std::vector<int> primes {2, 3, 5, 7, 11, 13, 17, 19};   // 创建含 8 个素数的 vector 容器

// 方式三:
std::vector<double> values(20);             // values 容器开始时就有 20 个容器,默认初始值都为 0
std::vector<double> values(20, 1.0);        // 默认值都是 1.0
  • 圆括号() 表示元素个数,花括号{} 表示元素的值
  • 圆括号() 种的 2 个参数,既可以是常量,也可以是变量。
// 方式四:通过其他 vector 容器创建新的 vector 容器
std::vector<char>value1(5, 'c');
std::vector<char>value2(value);

// 通过一对指针或者迭代器来指定初始化值的范围
int array[] = {1,2,3};
std::vector<int>values(array, array+2);     // 保存 {1,2}
std::vector<int>value1{1,2,3,4,5};
std::vector<int>values2(std::begin(value1),std::begin(value1)+3);   // 保存{1,2,3}

3.2 成员函数

函数成员 函数功能
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的迭代器。
rend() 返回指向第一个元素所在位置前一个位置的迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
size() 返回实际元素个数。
max_size() 返回元素个数的最大值。这通常是一个很大的值,一般是 2^32-1,所以我们很少会用到这个函数。
resize() 改变实际元素的个数。
capacity() 返回当前容量。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
reserve() 增加容器的容量。
shrink _to_fit() 将内存减少到等于当前元素实际所使用的大小。
operator[] 重载了 [] 运算符,可以向访问数组中元素那样,通过下标即可访问甚至修改 vector 容器中的元素。
at() 使用经过边界检查的索引访问元素。
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
data() 返回指向容器中第一个元素的指针。
assign() 用新元素替换原有内容。
push_back() 在序列的尾部添加一个元素。
pop_back() 移出序列尾部的元素。
insert() 在指定的位置插入一个或多个元素。
erase() 移出一个元素或一段元素。
clear() 移出所有的元素,容器大小变为 0。
swap() 交换两个容器的所有元素。
emplace() 在指定的位置直接生成一个元素。
emplace_back() 在序列尾部生成一个元素。

C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 vector 容器包含的 begin() 和 end() 成员函数不同,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。

3.3 vecotr容器迭代器

成员函数 功能
begin() 返回指向容器中第一个元素的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。
end() 返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。
rbegin() 返回指向最后一个元素的反向迭代器;如果是 const 类型容器,在该函数返回的是常量反向迭代器。
rend() 返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,在该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。
cbegin() 和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
cend() 和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。
crend() 和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。

C++ 11 新添加的 begin() 和 end() 全局函数也同样适用于 vector 容器。即当操作对象为 vector 容器时,其功能分别和表中的 begin()、end() 成员函数相同

vector 迭代器的基本用法

vector 容器迭代器最常用的功能就是遍历访问容器中存储的元素。

vector<int>values{1,2,3,4,5};
auto first = values.begin();
auto end = values.end();
// 使用全局的 begin() 和 end() 函数来从容器中获取迭代器
// auto first = std::begin(values);
// auto end = std::end(values);

while (first != end)
{
    cout << *first << " ";
    ++first;
}

在使用反向迭代器进行 ++ 或 -- 运算时,++ 指的是迭代器向左移动一位,-- 指的是迭代器向右移动一位,即这两个运算符的功能也“互换”了。

vector 容器可以随着存储元素的增加,自行申请更多的存储空间,因此创建 vector 对象时可以直接创建空的 vector 容器,但在初始化空的 vector 容器时不能使用迭代器,此时应该先通过调用 push_back() 函数。

3.4 访问 vector 容器元素的几种方式

访问单个元素

  • 通过下表索引访问
    vector<int> values{1,2,3,4,5};
    cout << values[0] << endl;        // 1
    cout << values.at(0) << endl;     // 越界保护
    values[0] = values[1] + values[2] + values[3] + values[4];
    cout << values[0] << endl;        // 14
    cout << values.at(0) << endl;     // 越界保护
    
    • vector 容器还提供 front() 和 back() 函数,即返回容器的第一个和最后一个元素的引用,可以通过引用修改容器的收尾元素。
    • data() 函数返回指向容器首个元素的指针,可以通过该指针访问/修改容器中的元素。

访问多个元素

// 1
vector<int> values{1,2,3,4,5};
for(int i = 0; i<values.size();i++)
    cout << values[i];

// 2
for(auto &value:values)
    cout << value;

// 3
for(auto first = values.begin(); first < values.end(); ++first )
    cout << *first;
// 也可以 rbegin/rend、cbegin/cend、crbegin/crent、全局 begin/end

3.5 容量capacity 和 size

vector 容器的容量(用 capacity 表示),指的是在不分配更多内存的情况下,容器可以保存的最多元素个数;而 vector 容器的大小(用 size 表示),指的是它实际所包含的元素个数。

修改容量和大小

  • reserve() 成员函数增加容量
  • resize() 改变大小

3.6 vector 容器的底层实现机制

vecotr 底层所采用的数据结构就是一段连续的线性内存空间,其使用 3 个迭代器(也可以理解成指针)来表示:

//_Alloc 表示内存分配器,此参数几乎不需要我们关心
template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
    ...
    
protected:
    pointer _Myfirst;   // 指向容器对象的起始字节位置
    pointer _Mylast;    // 指向当前最后一个元素的末尾字节
    pointer _Myend;     // 指向整个容器的末尾字节
};
  • _Myfirst 和 _Mylast 可以用来表示 vector 容器中目前已被使用的内存空间;
  • _Mylast 和 _Myend 可以用来表示 vector 容器目前空闲的内存空间;
  • _Myfirst 和 _Myend 可以用表示 vector 容器的容量。

通过灵活运用这 3 个迭代器,vector 容器可以轻松的实现诸如首尾标识、大小、容器、空容器判断等几乎所有的功能

template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
public:
    iterator begin() {return _Myfirst;}
    iterator end() {return _Mylast;}
    size_type size() const {return size_type(end() - begin());}
    size_type capacity() const {return size_type(_Myend - begin());}
    bool empty() const {return begin() == end();}
    reference operator[] (size_type n) {return *(begin() + n);}
    reference front() { return *begin();}
    reference back() {return *(end()-1);}
    // ...
};

vector 扩大容量的本质

当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:

  1. 完全弃用现有的内存空间,重新申请更大的内存空间;
  2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
  3. 最后将旧的内存空间释放。

vector 扩容后,其相关的指针、引用以及迭代器都可能失效。因此, vector 扩容是非常耗时的。

3.7 添加元素(push_back()/emplace_back())

vector<int> values{};
values.push_back(1);
values.push_back(2);
values.emplace_back(3);
values.emplace_back(4);

for (int i = 0; i < values.size(); i++) 
    cout << values[i] << " ";

区别:

底层实现的机制不同:

  • push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);
  • push_back() 在底层实现时,会优先选择调用移动构造函数,如果没有才会调用拷贝构造函数。
  • emplace_back() 则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。

3.8 插入元素(insert()/emplace())

insert()成员函数语法格式

语法格式 用法说明
iterator insert(pos,elem) 在迭代器 pos 指定的位置之前插入一个新元素 elem,并返回表示新插入元素位置的迭代器。
iterator insert(pos,n,elem) 在迭代器 pos 指定的位置之前插入 n 个元素 elem,并返回表示第一个新插入元素位置的迭代器。
iterator insert(pos,first,last) 在迭代器 pos 指定的位置之前,插入其他容器(不仅限于 vector)中位于 [first,last) 区域的所有元素,并返回表示第一个新插入元素位置的迭代器。
iterator insert(pos,initlist) 在迭代器 pos 指定的位置之前,插入初始化列表(用大括号{}括起来的多个元素,中间有逗号隔开)中所有的元素,并返回表示第一个新插入元素位置的迭代器。
std::vector<int> demo{1,2};

//第一种格式用法
demo.insert(demo.begin() + 1, 3);                           //{1,3,2}

//第二种格式用法
demo.insert(demo.end(), 2, 5);                              //{1,3,2,5,5}

//第三种格式用法
std::array<int,3>test{ 7,8,9 };
demo.insert(demo.end(), test.begin(), test.end());          //{1,3,2,5,5,7,8,9}

//第四种格式用法
demo.insert(demo.end(), { 10,11 });                         //{1,3,2,5,5,7,8,9,10,11}
for (int i = 0; i < demo.size(); i++)
    cout << demo[i] << " ";

emplace() 是 C++ 11 标准新增加的成员函数,用于在 vector 容器指定位置之前插入一个新的元素。

iterator emplace (const_iterator pos, args...);
  • pos 为指定插入位置的迭代器
  • args... 表示与新插入元素的构造函数相对应的多个参数
  • 函数会返回表示新插入元素位置的迭代器。

与 push_back()、emplace_back()之间的区别一样,emplace() 在插入元素时,是在容器的指定位置直接构造元素,而不是先单独生成,再将其复制(或移动)到容器中。

3.9 删除元素的几种方式

函数 功能
pop_back() 删除 vector 容器中最后一个元素,该容器的大小(size)会减 1,但容量(capacity)不会发生改变。
erase(pos) 删除 vector 容器中 pos 迭代器指定位置处的元素,并返回指向被删除元素下一个位置元素的迭代器。该容器的大小(size)会减 1,但容量(capacity)不会发生改变。
swap(beg)、pop_back() 先调用 swap() 函数交换要删除的目标元素和容器最后一个元素的位置,然后使用 pop_back() 删除该目标元素。
erase(beg,end) 删除 vector 容器中位于迭代器 [beg,end)指定区域内的所有元素,并返回指向被删除区域下一个位置元素的迭代器。该容器的大小(size)会减小,但容量(capacity)不会发生改变。
remove() 删除容器中所有和指定元素值相等的元素,并返回指向最后一个元素下一个位置的迭代器。值得一提的是,调用该函数不会改变容器的大小和容量。
clear() 删除 vector 容器中所有的元素,使其变成空的 vector 容器。该函数会改变 vector 的大小(变为 0),但不是改变其容量。

3.10 避免 vector 容器进行不必要扩容

在不超出 vector 最大容量限制(max_size() 成员方法的返回值)的前提下,vector 容器可以自行扩充容量来满足用户存储更多元素的需求。

vector 容器扩容的整个过程,和 realloc() 函数的实现方法类似,大致分为以下 4 个步骤:

  1. 分配一块大小是当前 vector 容量几倍的新存储空间。注意,多数 STL 版本中的 vector 容器,其容器都会以 2 的倍数增长,也就是说,每次 vector 容器扩容,它们的容量都会提高到之前的 2 倍;
  2. 将 vector 容器存储的所有元素,依照原有次序从旧的存储空间复制到新的存储空间中;
  3. 析构掉旧存储空间中存储的所有元素;
  4. 释放旧的存储空间。

vector 容器的扩容过程是非常耗时的,并且当容器进行扩容后,之前和该容器相关的所有指针、迭代器以及引用都会失效。因此在使用 vector 容器过程中,我们应尽量避免执行不必要的扩容操作。

vector 模板类中功能类似的成员方法

成员方法 功能
size() 告诉我们当前 vector 容器中已经存有多少个元素,但仅通过此方法,无法得知 vector 容器有多少存储空间。
capacity() 告诉我们当前 vector 容器总共可以容纳多少个元素。如果想知道当前 vector 容器有多少未被使用的存储空间,可以通过 capacity()-size() 得知。注意,如果 size() 和 capacity() 返回的值相同,则表明当前 vector 容器中没有可用存储空间了,这意味着,下一次向 vector 容器中添加新元素,将导致 vector 容器扩容。
resize(n) 强制 vector 容器必须存储 n 个元素,注意,如果 n 比 size() 的返回值小,则容器尾部多出的元素将会被析构(删除);如果 n 比 size() 大,则 vector 会借助默认构造函数创建出更多的默认值元素,并将它们存储到容器末尾;如果 n 比 capacity() 的返回值还要大,则 vector 会先扩增,在添加一些默认值元素。
reserve(n) 强制 vector 容器的容量至少为 n。注意,如果 n 比当前 vector 容器的容量小,则该方法什么也不会做;反之如果 n 比当前 vector 容器的容量大,则 vector 容器就会扩容。

只要有新元素要添加到 vector 容器中而恰好此时 vector 容器的容量不足时,该容器就会自动扩容。因此,避免 vector 容器执行不必要的扩容操作的关键在于,在使用 vector 容器初期,就要将其容量设为足够大的值。换句话说,在vector 容器刚刚构造出来的那一刻,就应该借助 reserve() 成员方法为其扩充足够大的容量。

vector<int>myvector;
for (int i = 1; i <= 1000; i++) {
    myvector.push_back(i);
}

上述代码在整个循环过程中,会自动扩容 2~10 次。在上面程序基础上,可以使用 reserve() 方法尽量避免不必要的扩容操作:

vector<int>myvector;
myvector.reserve(1000);
cout << myvector.capacity();
for (int i = 1; i <= 1000; i++)
    myvector.push_back(i);

关于怎样去除 vector 容器多余的容量,可以借助该容器模板类提供的 shrink_to_fit() 成员方法,另外还可以使用 swap() 成员方法去除 vector 容器多余的容量。

3.11 swap()成员方法去除多余容量

vector 模板类中提供 shrink_to_fit() 成员方法,该方法的功能是将当前 vector 容器的容量缩减至和实际存储元素的个数相等。

swap() 成员方法的基础功能是交换 2 个相同类型的 vector 容器(交换容量和存储的所有元素),也能用于去除 vector 容器多余的容量。

当用 swap() 成员方法去除当前 vector 容器多余的容量时,可以套用如下的语法格式:

vector<T>(x).swap(x);    // x 指当前要操作的容器,T 为该容器存储元素的类型
vector<int>myvector;

// 手动为 myvector 扩容
myvector.reserve(1000);                 // 容量1000

//利用 myvector 容器存储 10 个元素
for (int i = 1; i <= 10; i++)
    myvector.push_back(i);

//将 myvector 容量缩减至 10
vector<int>(myvector).swap(myvector);   // 容量 10

swap 修改容器容量分为 3 步:

  1. 先执行 vector(myvector),此表达式会调用 vector 模板类中的拷贝构造函数,从而创建出一个临时的 vector 容器(后续称其为 tempvector)。tempvector 临时容器并不为空,因为我们将 myvector 作为参数传递给了复制构造函数,该函数会将 myvector 容器中的所有元素拷贝一份,并存储到 tempvector 临时容器中。注意,vector 模板类中的拷贝构造函数只会为拷贝的元素分配存储空间。换句话说,tempvector 临时容器中没有空闲的存储空间,其容量等于存储元素的个数。
  2. 借助 swap() 成员方法对 tempvector 临时容器和 myvector 容器进行调换,此过程不仅会交换 2 个容器存储的元素,还会交换它们的容量。换句话说经过 swap() 操作,myvetor 容器具有了 tempvector 临时容器存储的所有元素和容量,同时 tempvector 也具有了原 myvector 容器存储的所有元素和容量。
  3. 当整条语句执行结束时,临时的 tempvector 容器会被销毁,其占据的存储空间都会被释放。注意,这里释放的其实是原 myvector 容器占用的存储空间。

利用 swap() 方法清空 vector 容量

vector().swap(x);

vector<int>myvector;

//手动为 myvector 扩容
myvector.reserve(1000);

//利用 myvector 容器存储 10 个元素
for (int i = 1; i <= 10; i++)
    myvector.push_back(i);

//清空 myvector 容器
vector<int>().swap(myvector);

3.12 vector<bool>

vector 不完全满足 C++ 标准中对容器的要求,尽量避免在实际场景中使用它!

不推荐使用 vector 的原因有以下 2 个:

  1. 严格意义上讲,vector 并不是一个 STL 容器;
  2. vector 底层存储的并不是 bool 类型值。

对于是否为 STL 容器,C++ 标准库中有明确的判断条件,其中一个条件是:如果 cont 是包含对象 T 的 STL 容器,且该容器中重载了 [ ] 运算符(即支持 operator[]),则以下代码必须能够被编译:

vector<bool>cont{0,1};
T *p =  &cont[0];

但实际上此段代码不能通过,原因在于 vector 底层采用了独特的存储机制。

  • 为了节省空间,vector 底层在存储各个 bool 类型值时,每个 bool 值都只使用一个比特位(二进制位)来存储。也就是说在 vector 底层,一个字节可以存储 8 个 bool 类型值。在这种存储机制的影响下,operator[ ] 势必就需要返回一个指向单个比特位的引用,但显然这样的引用是不存在的。
  • C++ 标准中解决这个问题的方案是,令 operator[] 返回一个代理对象(proxy object)。

四、deque 容器

deque 是 double-ended queue 的缩写,又称双端队列容器。

  • deque 容器擅长在序列头/尾部添加或删除元素(时间复杂度为 O(1)),不擅长在序列中间添加或删除元素。
  • deque 容器可以根据需要修改自身的容量和大小。
  • deque 容器不能保证所有元素都存储到连续的内存空间中。

4.1 创建 deque 容器的几种方式

  • 创建空 deque 容器

    std::deque<int> d;
    

    和空 array 容器不同,空的 deque 容器在创建之后可以做添加或删除元素的操作。

  • 创建 n 个元素的 deque 容器

    std::deque<int> d(10);
    
  • 创建 n 个元素的 deque 容器,指定初始值

    std::deque<int> d(10,5);
    
  • 通过已有 deque 容器创建新的 deque 容器

    std::deque<int> d1(5);
    std::deque<int> d2(d1);
    

    这种方式新旧容器存储的元素类型需一致。

  • 拷贝其他类型容器中指定区域内的元素(也可以是普通数组),可以创建一个新容器

    int a[] = {1,2,3,4,5};
    std::deque<int>d(a,a+5);
    
    std::array<int,5>arr{11,12,13,14,15};
    std::deque<int>d(arr.begin()+2,arr.end());    // ;//拷贝 arr 容器中的{13,14,15}
    

4.2 成员函数

函数成员 函数功能
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的迭代器。
rend() 返回指向第一个元素所在位置前一个位置的迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
size() 返回实际元素个数。
max_size() 返回容器所能容纳元素个数的最大值。这通常是一个很大的值,一般是 2^32-1,我们很少会用到这个函数。
resize() 改变实际元素的个数。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
shrink _to_fit() 将内存减少到等于当前元素实际所使用的大小。
at() 使用经过边界检查的索引访问元素。
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
assign() 用新元素替换原有内容。
push_back() 在序列的尾部添加一个元素。
push_front() 在序列的头部添加一个元素。
pop_back() 移除容器尾部的元素。
pop_front() 移除容器头部的元素。
insert() 在指定的位置插入一个或多个元素。
erase() 移除一个元素或一段元素。
clear() 移出所有的元素,容器大小变为 0。
swap() 交换两个容器的所有元素。
emplace() 在指定的位置直接生成一个元素。
emplace_front() 在容器头部生成一个元素。和 push_front() 的区别是,该函数直接在容器头部构造元素,省去了复制移动元素的过程。
emplace_back() 在容器尾部生成一个元素。和 push_back() 的区别是,该函数直接在容器尾部构造元素,省去了复制移动元素的过程。
  • 和 vector 相比,额外增加了实现在容器头部添加和删除元素的成员函数,同时删除了 capacity()、reserve() 和 data() 成员函数。
  • 和 array、vector 相同,C++ 11 标准库新增的 begin() 和 end() 这 2 个全局函数也适用于 deque 容器。

4.3 deque 容器迭代器

deque 支持迭代器的成员函数

成员函数 功能
begin() 返回指向容器中第一个元素的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。
end() 返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。
rbegin() 返回指向最后一个元素的反向迭代器;如果是 const 类型容器,在该函数返回的是常量反向迭代器。
rend() 返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,在该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。
cbegin() 和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
cend() 和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。
crend() 和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。
// 遍历 deque 容器
deque<int>d{1,2,3,4,5};
for(auto i = d.begin(); i<d.end();i++)
    cout << *i << " ";

// 全局 begin/end 函数
for (auto i = begin(d); i < end(d); i++) 
    cout << *i << " ";

deque 迭代器注意事项

  • 在遍历的同时可以访问(甚至修改)容器中的元素,但迭代器不能用来初始化空的 deque 容器。
  • 对于空的 deque 容器来说,可以通过 push_back()、push_front() 或者 resize() 成员函数实现向(空)deque 容器中添加元素。
  • 当向 deque 容器添加元素时,可能会使之前创建的迭代器失效。

4.4 deque 容器底层实现原理

存储结构

deque 容器存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于在内存的不同区域。

为了管理这些连续空间,deque 容器用数组(数组名假设为 map)存储着各个连续空间的首地址。也就是说,map 数组中存储的都是指针,指向那些真正用来存储数据的各个连续空间(如图所示)。

通过建立 map 数组,deque 容器申请的这些分段的连续空间就能实现“整体连续”的效果。当需要在头部或尾部增加存储空间时,它会申请一段新的连续空间,再在 map 数组开头或结尾添加指向该空间的指针。如果 map 数组满了,重新申请一块更大的连续空间供 map 数组使用,将原有数据拷贝到新的 map 数组中。

deque 迭代器底层实现

由于 deque 容器底层将序列中的元素分别存储到了不同段的连续空间中,因此要想实现迭代器的功能,必须先解决如下 2 个问题:

  1. 迭代器在遍历 deque 容器时,必须能够确认各个连续空间在 map 数组中的位置;
  2. 迭代器在遍历某个具体的连续空间时,必须能够判断自己是否已经处于空间的边缘位置。如果是,则一旦前进或者后退,就需要跳跃到上一个或者下一个连续空间中。

为了实现遍历 deque 容器的功能,deque 迭代器定义了如下的结构:

template<class T,...>
struct __deque_iterator{
    // ...
    T* cur;                 // 指向当前正在遍历的元素;
    T* first;               // 指向当前连续空间的首地址;
    T* last;                // 指向当前连续空间的末尾地址;
    map_pointer node;       //map_pointer 等价于 T**,二级指针,用于指向 map 数组中存储的指向当前连续空间的指针。
}
//当迭代器处于当前连续空间边缘的位置时,如果继续遍历,就需要跳跃到其它的连续空间中,该函数可用来实现此功能
void set_node(map_pointer new_node){
    node = new_node;//记录新的连续空间在 map 数组中的位置
    first = *new_node; //更新 first 指针

    //更新 last 指针,difference_type(buffer_size())表示每段连续空间的长度
    last = first + difference_type(buffer_size());
}

//重载 * 运算符
reference operator*() const{ 
    return *cur; 
}

pointer operator->() const{ 
    return &(operator *()); 
}

//重载前置 ++ 运算符
self & operator++(){
    ++cur;
    
    //处理 cur 处于连续空间边缘的特殊情况
    if(cur == last){
        //调用该函数,将迭代器跳跃到下一个连续空间中
        set_node(node+1);
        //对 cur 重新赋值
        cur = first;
    }
    return *this;
}

//重置前置 -- 运算符
self& operator--(){
    // 如果 cur 位于连续空间边缘,则先将迭代器跳跃到前一个连续空间中
    if(cur == first){
        set_node(node-1);
        cur == last;
    }
    --cur;
    return *this;
}

deque 容器底层实现

deque 容器除了维护 map 数组,还需维护 start、finish 2 个 deque 迭代器。

// deque 容器的定义

//_Alloc 为内存分配器
template<class _Ty,class _Alloc = allocator<_Ty>>
class deque{
    // ...
protected:
    iterator start;     // 记录 map 数组中首个连续空间的信息, 迭代器中cur 指针指向的是连续空间中首个元素
    iterator finish;    // 记录 map 数组中最后一个连续空间的信息,迭代器中 cur 指针指向的是连续空间最后一个元素的下一个位置
    map_pointer map;
    // ...
};

底层实现如图所示

4.5 deque 容器访问元素的 4 种方法

  • 采用普通数组访问存储元素
    deque<int> d{1,2,3,4};
    cout << d[1] << endl;
    
  • at()函数
    deque<int> d{1,2,3,4};
    cout << d.at(1) << endl;
    
  • front()/back() 函数
    deque<int> d{1,2,3,4,5};
    cout << d.front() << endl;        // 访问首元素
    cout << d.back() << endl;         // 访问尾元素
    

deque 容器没有 data() 成员函数

添加和删除元素

成员函数 功能
push_back() 在容器现有元素的尾部添加一个元素,和 emplace_back() 不同,该函数添加新元素的过程是,先构造元素,然后再将该元素移动或复制到容器的尾部。
pop_back() 移除容器尾部的一个元素。
push_front() 在容器现有元素的头部添加一个元素,和 emplace_back() 不同,该函数添加新元素的过程是,先构造元素,然后再将该元素移动或复制到容器的头部。
pop_front() 移除容器尾部的一个元素。
emplace_back() C++ 11 新添加的成员函数,其功能是在容器尾部生成一个元素。和 push_back() 不同,该函数直接在容器头部构造元素,省去了复制或移动元素的过程。
emplace_front() C++ 11 新添加的成员函数,其功能是在容器头部生成一个元素。和 push_front() 不同,该函数直接在容器头部构造元素,省去了复制或移动元素的过程。
insert() 在指定的位置直接生成一个元素。和 emplace() 不同的是,该函数添加新元素的过程是,先构造元素,然后再将该元素移动或复制到容器的指定位置。
emplace() C++ 11 新添加的成员函数,其功能是 insert() 相同,即在指定的位置直接生成一个元素。和 insert() 不同的是,emplace() 直接在容器指定位置构造元素,省去了复制或移动元素的过程。
erase() 移除一个元素或某一区域内的多个元素。
clear() 删除容器中所有的元素。

insert() 成员函数语法格式

语法格式 功能
iterator insert(pos,elem) 在迭代器 pos 指定的位置之前插入一个新元素 elem,并返回表示新插入元素位置的迭代器。
iterator insert(pos,n,elem) 在迭代器 pos 指定的位置之前插入 n 个元素 elem,并返回表示第一个新插入元素位置的迭代器。
iterator insert(pos,first,last) 在迭代器 pos 指定的位置之前,插入其他容器(不仅限于 vector)中位于 [first,last) 区域的所有元素,并返回表示第一个新插入元素位置的迭代器。
iterator insert(pos,initlist) 在迭代器 pos 指定的位置之前,插入初始化列表(用大括号{}括起来的多个元素,中间有逗号隔开)中所有的元素,并返回表示第一个新插入元素位置的迭代器。

五、list 容器

list 容器,又称双向链表容器,底层是以双向链表实现的。这意味着,list 容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间中。

  • 优点:基于双向链表的数据结构,list 可以在序列已知的任何位置快速插入或删除元素(O(1)),且移动元素时效率比其他容器也高。
  • 缺点:不能直接访问元素,需要从首/尾开始遍历。

如果需要对序列进行大量添加或删除元素的操作,而直接访问元素的需求却很少时,建议使用 list 容器存储序列。

5.1 list 容器的创建

  • 创建空 list 容器
    std::list<int> values;
    
  • 创建含 n 个元素的 list 容器
    std::list<int> values(10);
    
  • 为元素指定初始值
    std::list<int> values(10,5);  // 创建一个含10个元素,且初始值都为5的容器
    
  • 通过已有 list 容器创建新容器
    // 新旧容器存储的元素类型必须一致
    std::list<int> value1(10);
    std::list<int> value2(value1);
    
  • 拷贝其他类型容器(或数组)中指定区域内元素创建新的 list 容器
    //拷贝普通数组,创建 list 容器
    int a[] = { 1,2,3,4,5 };
    std::list<int> values(a, a+5);
    
    //拷贝其它类型的容器,创建 list 容器
    std::array<int, 5>arr{ 11,12,13,14,15 };
    std::list<int>values(arr.begin()+2, arr.end());       //拷贝 arr 容器中的{13,14,15}
    

5.2 list 容器成员函数

成员函数 功能
begin() 返回指向容器中第一个元素的双向迭代器。
end() 返回指向容器中最后一个元素所在位置的下一个位置的双向迭代器。
rbegin() 返回指向最后一个元素的反向双向迭代器。
rend() 返回指向第一个元素所在位置前一个位置的反向双向迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
size() 返回当前容器实际包含的元素个数。
max_size() 返回容器所能包含元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
assign() 用新元素替换容器中原有内容。
emplace_front() 在容器头部生成一个元素。该函数和 push_front() 的功能相同,但效率更高。
push_front() 在容器头部插入一个元素。
pop_front() 删除容器头部的一个元素。
emplace_back() 在容器尾部直接生成一个元素。该函数和 push_back() 的功能相同,但效率更高。
push_back() 在容器尾部插入一个元素。
pop_back() 删除容器尾部的一个元素。
emplace() 在容器中的指定位置插入元素。该函数和 insert() 功能相同,但效率更高。
insert() 在容器中的指定位置插入元素。
erase() 删除容器中一个或某区域内的元素。
swap() 交换两个容器中的元素,必须保证这两个容器中存储的元素类型是相同的。
resize() 调整容器的大小。
clear() 删除容器存储的所有元素。
splice() 将一个 list 容器中的元素插入到另一个容器的指定位置。
remove(val) 删除容器中所有等于 val 的元素。
remove_if() 删除容器中满足条件的元素。
unique() 删除容器中相邻的重复元素,只保留一个。
merge() 合并两个事先已排好序的 list 容器,并且合并之后的 list 容器依然是有序的。
sort() 通过更改容器中元素的位置,将它们进行排序。
reverse() 反转容器中元素的顺序。

除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 list 容器包含的 begin() 和 end() 成员函数不同,标准库提供
的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。

5.3 list 迭代器及用法

迭代器函数 功能
begin() 返回指向容器中第一个元素的双向迭代器(正向迭代器)。
end() 返回指向容器中最后一个元素所在位置的下一个位置的双向迭代器。(正向迭代器)。
rbegin() 返回指向最后一个元素的反向双向迭代器。
rend() 返回指向第一个元素所在位置前一个位置的反向双向迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,正向迭代器增加了 const 属性,即不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,正向迭代器增加了 const 属性,即不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,反向迭代器增加了 const 属性,即不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,反向迭代器增加了 const 属性,即不能用于修改元素。

C++ 11 新添加的 begin() 和 end() 全局函数也同样适用于 list 容器。即当操作对象为 list 容器时,其功能分别和表中的 begin()、end() 成员函数相同。

假设 p1 和 p2 都是双向迭代器,则它们支持使用 ++p1、 p1++、 p1--、 p1++、 *p1、 p1==p2 以及 p1!=p2 运算符,但不支持以下操作(其中 i 为整数):

  • p1[i]:不能通过下标访问 list 容器中指定位置处的元素。
  • p1-=i、 p1+=i、 p1+i 、p1-i:双向迭代器 p1 不支持使用 -=、+=、+、- 运算符。
  • p1<p2、 p1>p2、 p1<=p2、 p1>=p2:双向迭代器 p1、p2 不支持使用 <、 >、 <=、 >= 比较运算符。
// 遍历 list 容器
std::list<char> values{'h','t','t','p',':','/','/','c','.','b','i','a','n','c','h','e','n','g','.','n','e','t'};
//使用 begin()/end()迭代器函数对输出 list 容器中的元素
for (std::list<char>::iterator it = values.begin(); it != values.end(); ++it) 
    std::cout << *it;

//使用 rbegin()/rend()迭代器函数输出 lsit 容器中的元素
for (std::list<char>::reverse_iterator it = values.rbegin(); it != values.rend();++it) 
    std::cout << *it;

list 容器在进行插入(insert())、接合(splice())等操作时,都不会造成原有的 list 迭代器失效,甚至进行删除操作,而只有指向被删除元素的迭代器失效,其他迭代器不受任何影响。

5.4 list 容器底层存储结构


list 容器底层是用双向链表实现的,甚至有些 STL 版本使用的是双向循环链表。

template<typename T,...>
struct __List_node{
    //...
    __list_node<T>* prev;   // 指向前一个节点
    __list_node<T>* next;   // 指向后一个节点
    T myval;                // 存储当前元素的值
    //...
}

list 容器迭代器的实现代码如下:

template<tyepname T,...>
struct __list_iterator{
    __list_node<T>* node;
    //...
    //重载 == 运算符
    bool operator==(const __list_iterator& x){return node == x.node;}

    //重载 != 运算符
    bool operator!=(const __list_iterator& x){return node != x.node;}
    
    //重载 * 运算符,返回引用类型
    T* operator *() const {return *(node).myval;}

    //重载前置 ++ 运算符
    __list_iterator<T>& operator ++(){
        node = (*node).next;
        return *this;
    }

    //重载后置 ++ 运算符
    __list_iterator<T>& operator ++(int){
        __list_iterator<T> tmp = *this;
        ++(*this);
        return tmp;
    }
    
    //重载前置 -- 运算符
    __list_iterator<T>& operator--(){
        node = (*node).prev;
        return *this;
    }

    //重载后置 -- 运算符
    __list_iterator<T> operator--(int){
        __list_iterator<T> tmp = *this;
        --(*this);
        return tmp;
    }
    //...
}

不同版本的 STL 标准库中,list 容器的底层实现并不完全一致,但原理基本相同。

如下是 SGI STL 标准库中对 list 容器的定义:

template <class T,...>
class list{
    //...
    //指向链表的头节点,并不存放数据
    __list_node<T>* node;
    //...以下还有 list 容器的构造函数以及很多操作函数
    list() { empty_initialize(); }
    // 用于空链表的建立
    void empty_initialize()
    {
        node = get_node();//初始化节点
        node->next = node; // 前置节点指向自己
        node->prev = node; // 后置节点指向自己
    }
};

list 容器实际上就是一个带有头节点的双向循环链表。

5.5 list 访问元素的几种方法

list 容器不支持随机访问,未提供下标操作符 [] 和 at() 成员函数,也没有提供 data() 成员函数。

通过 front() 和 back() 成员函数,可以分别获得 list 容器中第一个元素和最后一个元素的引用形式。

std::list<int> mylist{ 1,2,3,4 };
int &first = mylist.front();
int &last = mylist.back();

first = 10;
last = 20;
cout << mylist.front() << " " << mylist.back() << endl;     // 10 20

除此之外,如果想访问 list 容存储的其他元素,就只能使用 list 容器的迭代器。

const std::list<int> mylist{1,2,3,4,5};
auto it = mylist.begin();
cout << *it << " ";
++it;
while (it!=mylist.end())
{
    cout << *it << " ";
    ++it;
}

5.6 list 插入元素

list 模板类中,与“添加或插入新元素”相关的成员方法有如下几个:

  • push_front():向 list 容器首个元素前添加新元素;
  • push_back():向 list 容器最后一个元素后添加新元素;
  • emplace_front():在容器首个元素前直接生成新的元素;
  • emplace_back():在容器最后一个元素后直接生成新的元素;
  • emplace():在容器的指定位置直接生成新的元素;
  • insert():在指定位置插入新元素;
  • splice():将其他 list 容器存储的多个元素添加到当前 list 容器的指定位置处。

insert() 成员方法

语法格式 用法说明
iterator insert(pos,elem) 在迭代器 pos 指定的位置之前插入一个新元素 elem,并返回表示新插入元素位置的迭代器。
iterator insert(pos,n,elem) 在迭代器 pos 指定的位置之前插入 n 个元素 elem,并返回表示第一个新插入元素位置的迭代器。
iterator insert(pos,first,last) 在迭代器 pos 指定的位置之前,插入其他容器(例如 array、vector、deque 等)中位于 [first,last) 区域的所有元素,并返回表示第一个新插入元素位置的迭代器。
iterator insert(pos,initlist) 在迭代器 pos 指定的位置之前,插入初始化列表(用大括号 { } 括起来的多个元素,中间有逗号隔开)中所有的元素,并返回表示第一个新插入元素位置的迭代器。
std::list<int> values{ 1,2 };
//第一种格式用法
values.insert(values.begin() , 3);      //{3,1,2}

//第二种格式用法
values.insert(values.end(), 2, 5);      //{3,1,2,5,5}

//第三种格式用法
std::array<int, 3>test{ 7,8,9 };
values.insert(values.end(), test.begin(), test.end());      //{3,1,2,5,5,7,8,9}

//第四种格式用法
values.insert(values.end(), { 10,11 });                     //{3,1,2,5,5,7,8,9,10,11}

实现同样的功能,emplace 系列方法的执行效率更高。

splice() 成员方法

splice() 成员方法的作用对象是其它 list 容器,其功能是将其它 list 容器中的元素添加到当前 list 容器中指定位置处。

splice() 成员方法的用法

语法格式 功能
void splice (iterator position, list& x); position 为迭代器,用于指明插入位置;x 为另一个 list 容器。此格式的 splice() 方法的功能是,将 x 容器中存储的所有元素全部移动当前 list 容器中position 指明的位置处。
void splice (iterator position, list& x,iterator i); position 为迭代器,用于指明插入位置;x 为另一个 list 容器;i 也是一个迭代器,用于指向 x 容器中某个元素。此格式的 splice() 方法的功能是将 x 容器中 i 指向的元素移动到当前容器中 position指明的位置处。
void splice (iterator position, list& x, iterator first, iterator last); position 为迭代器,用于指明插入位置;x 为另一个 list 容器;first 和 last 都是迭代器,[fist,last) 用于指定 x 容器中的某个区域。此格式的 splice() 方法的功能是将 x 容器 [first, last) 范围内所有的元素移动到当前容器 position 指明的位置处。

splice() 成员方法移动元素的方式是,将存储该元素的节点从 list 容器底层的链表中摘除,然后再链接到当前 list 容器底层的链表中。这意味着,当使用 splice() 成员方法将 x 容器中的元素添加到当前容器的同时,该元素会从 x 容器中删除。

//创建并初始化 2 个 list 容器
list<int> mylist1{ 1,2,3,4 }, mylist2{10,20,30};
list<int>::iterator it = ++mylist1.begin();     //指向 mylist1 容器中的元素 2

//调用第一种语法格式
mylist1.splice(it, mylist2);        // mylist1: 1 10 20 30 2 3 4
                                    // mylist2: it 迭代器仍然指向元素 2,只不过容器变为了 mylist1

//调用第二种语法格式,将 it 指向的元素 2 移动到 mylist2.begin() 位置处
mylist2.splice(mylist2.begin(), mylist1, it);       // mylist1: 1 10 20 30 3 4
                                                    // mylist2: it 仍然指向元素 2

//调用第三种语法格式,将 [mylist1.begin(),mylist1.end())范围内的元素移动到 mylist.begin() 位置处
mylist2.splice(mylist2.begin(), mylist1, mylist1.begin(), mylist1.end());       //mylist1:
                                                                                //mylist2:1 10 20 30 3 4 2

5.7 empty() 和 size() 比较

只要其模板类中提供了 empty() 成员方法,使用此方法都可以保证在O(1) 时间复杂度内完成对“容器是否为空”的判断;但对于 list 容器来说,使用 size() 成员方法判断“容器是否为空”,可能要消耗O(n) 的时间复杂度。

这个结论不仅适用于 vector、deque 和 list 容器,还适用于很多其他容器。

如果需要将 size() 方法执行效率达到 O(1),最直接实现方式是设置一个专门用于统计存储元素数量的 size 变量。而 list 容器最大的特点是不需要拷贝可以直接加减元素,整个过程消耗 O(1)。但如果要将 size 和 splice 复杂度都设为 O(1),这将产生矛盾,无法同时实现,因此大部分版本的 size() 执行效率为 O(n)。

如何需要判断当前容器是否为空,优先使用 empty()。

5.8 list 删除元素

实现 list 容器删除元素的成员函数

成员函数 功能
pop_front() 删除位于 list 容器头部的一个元素。
pop_back() 删除位于 list 容器尾部的一个元素。
erase() 该成员函数既可以删除 list 容器中指定位置处的元素,也可以删除容器中某个区域内的多个元素。
clear() 删除 list 容器存储的所有元素。
remove(val) 删除容器中所有等于 val 的元素。
unique() 删除容器中相邻的重复元素,只保留一份。
remove_if() 删除容器中满足条件的元素。

erase() 成员函数有以下 2 种语法格式:

  • iterator erase (iterator position);
  • iterator erase (iterator first, iterator last);
list<int>values{ 1,2,3,4,5 };
auto del = values.begin();      // 指向元素 1 的迭代器
++del;                          // 迭代器右移,改为指向元素 2
values.erase(del);              //{1,3,4,5}


list<int>values{ 1,2,3,4,5 };
auto first = values.begin();
++first;                        //指向元素 2
auto last = values.end();       //指向删除区域的右边界
--last;                         //指向元素 5
values.erase(first, last);  //删除 2、3 和 4,注意不包括 last

erase() 成员函数是按照被删除元素所在的位置来执行删除操作,如果想根据元素的值来执行删除操作,可以使用 remove() 成员函数。

六、forward_list 容器

forward_list 是 C++ 11 新添加的一类容器,其底层实现和 list 容器一样,采用的也是链表结构,只不过 forward_list 使用的是单链表。

forward_list 容器具有和 list 容器相同的特性,即擅长在序列的任何位置进行插入元素或删除元素的操作,但对于访问存储的元素,没有其它容器(如 array、vector)的效率高。

效率高是选用 forward_list 而弃用 list 容器最主要的原因,换句话说,只要是 list 容器和 forward_list 容器都能实现的操作,应优先选择 forward_list 容器。

6.1 创建 forward_list 容器

  • 创建一个没有任何元素的空 forward_list 容器
    std::forward_list<int> values;
    
  • 创建一个包含 n 个元素的 forward_list 容器
    std::forward_list<int> values(10);
    
  • 创建一个包含 n 个元素的 forward_list 容器,并为每个元素指定初始值。
    std::forward_list<int> values(10,5);
    
  • 通过拷贝其他类型容器(或者普通数组)中指定区域内的元素,可以创建新的 forward_list 容器。
    // 拷贝普通数组,创建 forward_list 容器
    int a[] = { 1,2,3,4,5 };
    std::forward_list<int> values(a, a+5);
    // 拷贝其它类型的容器,创建 forward_list 容器
    std::array<int, 5>arr{ 11,12,13,14,15 };
    std::forward_list<int>values(arr.begin()+2, arr.end());//拷贝 arr 容器中的{13,14,15}
    

6.2 成员函数

成员函数 功能
before_begin() 返回一个前向迭代器,其指向容器中第一个元素之前的位置。
begin() 返回一个前向迭代器,其指向容器中第一个元素的位置。
end() 返回一个前向迭代器,其指向容器中最后一个元素之后的位置。
cbefore_begin() 和 before_begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
max_size() 返回容器所能包含元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
front() 返回第一个元素的引用。
assign() 用新元素替换容器中原有内容。
push_front() 在容器头部插入一个元素。
emplace_front() 在容器头部生成一个元素。该函数和 push_front() 的功能相同,但效率更高。
pop_front() 删除容器头部的一个元素。
emplace_after() 在指定位置之后插入一个新元素,并返回一个指向新元素的迭代器。和 insert_after() 的功能相同,但效率更高。
insert_after() 在指定位置之后插入一个新元素,并返回一个指向新元素的迭代器。
erase_after() 删除容器中某个指定位置或区域内的所有元素。
swap() 交换两个容器中的元素,必须保证这两个容器中存储的元素类型是相同的。
resize() 调整容器的大小。
clear() 删除容器存储的所有元素。
splice_after() 将某个 forward_list 容器中指定位置或区域内的元素插入到另一个容器的指定位置之后。
remove(val) 删除容器中所有等于 val 的元素。
remove_if() 删除容器中满足条件的元素。
unique() 删除容器中相邻的重复元素,只保留一个。
merge() 合并两个事先已排好序的 forward_list 容器,并且合并之后的 forward_list 容器依然是有序的。
sort() 通过更改容器中元素的位置,将它们进行排序。
reverse() 反转容器中元素的顺序。

forward_list 容器还有一个 std::swap(x , y)非成员函数(其中 x 和 y 是存储相同类型元素的 forward_list 容器),它和 swap() 成员函数的功能完全相同,仅使用语法上有差异。

forward_list 容器中是不提供 size() 函数的,但如果想要获取 forward_list 容器中存储元素的个数,可以使用头文件 中的 distance() 函数。

std::forward_list<int> my_words{1,2,3,4};
int count = std::distance(std::begin(my_words), std::end(my_words));
cout << count;          // 4

forward_list 容器迭代器的移动除了使用 ++ 运算符单步移动,还能使用 advance() 函数

std::forward_list<int> values{1,2,3,4};
auto it = values.begin();
advance(it, 2);
while (it!=values.end())
{
    cout << *it << " ";             // 3  4
    ++it;
}
posted @ 2021-11-13 17:01  锦瑟,无端  阅读(628)  评论(0编辑  收藏  举报