第2章 C++ STL序列式容器总结
本章主要讲解STL标准库中所有的序列式容器,包括 array、vector、string、deque、list 和 forward_list 容器。(个人:也就是说,c++中的序列容器,主要是:array(定长数组),vector(变长数组),deque(双端队列),list(双向列表),forward_list(单向正向列表)) 。 所谓STL序列式容器,其共同的特点是不会对存储的元素进行排序,元素排列的顺序取决于存储它们的顺序。
本章主要介绍:
- 各个序列容器的特性和功能,适用场景,
- 系统讲解各个容器的具体用法,
- 包括如何创建容器,(个人:也就是初始化)
- 以及如何实现对容器中元素的增删改查等。(个人:也就是创建后具体的使用操作,增删改查)
C++ STL容器是什么?
简单的理解容器,它就是一些模板类的集合,但和普通模板类不同的是,容器中封装的是组织数据的方法(也就是数据结构)。
STL提供有3类标准容器,分别是序列容器、排序容器和哈希容器,其中后两类容器有时也统称为关联容器。它们各自的含义如表 1 所示。(个人:顺序容器:array(定长数组),vector(变长数组),list(双向链表),forward_list(单向前向链表),deque(双端队列),string,元素在顺序容器中的顺序与其加入容器时的位置相对应;关联容器,主要是集合,映射,关联容器中元素的位置由元素相关联的关键字值决定,分为有序关联容器(底层红黑树实现)set,multiset,map,multimap和无序的哈希表容器(底层哈希表实现)unordered_set,unordered_multiset,unordered_map,unordered_multimap)
容器种类 | 功能 |
---|---|
序列容器 | 主要包括 array,vector 向量容器、list 双向链表容器、forward_list单向链表以及 deque 双端队列容器,string。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。 |
排序容器 | 包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。 |
哈希容器 | C++ 11 新加入 4 种关联式容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。 |
迭代器是什么,C++ STL迭代器(iterator)用法详解
- 它除了要具有对容器进行遍历读写数据的能力之外,
- 还要能对外隐藏容器的内部差异,
- 从而以统一的界面向算法传送数据。
这是泛型思维发展的必然结果,于是迭代器就产生了。
简单来讲,迭代器和 C++ 的指针非常类似,它可以是需要的任意类型,(个人:各个容器会在其内部定义对应的迭代器,例如,vector<int>::iterator,不同类型的容器,内部定义的迭代器各不相同,但是使用时,是以一种统一的方式使用的,在外界看不出什么不同),通过迭代器可以指向容器中的某个元素,如果需要,还可以对该元素进行读/写操作。
迭代器类别:
STL 标准库为每一种标准容器定义了一种迭代器类型,这意味着,不同容器的迭代器也不同,其功能强弱也有所不同。 容器的迭代器的功能强弱,决定了该容器是否支持 STL 中的某种算法。
常用的迭代器按功能强弱分为5 种:
- 输入迭代器、
- 输出迭代器、
- 前向迭代器、
- 双向迭代器、
- 随机访问迭代器 。
本节主要介绍后面的这 3 种迭代器。(个人:前向迭代器,双向迭代器,随机访问迭代器,这几种迭代器,按功能是逐步增强的)
- 前向迭代器(forward iterator):假设 p 是一个前向迭代器,则 p 支持 ++p,p++,*p 操作,还可以被复制或赋值,可以用 == 和 != 运算符进行比较。此外,两个正向迭代器可以互相赋值。(个人:只能one step前进,不能后退,支持赋值操作,是否相等操作,多遍扫描)
- 双向迭代器(bidirectional iterator):双向迭代器具有前向迭代器的全部功能,除此之外,假设 p 是一个双向迭代器,则还可以进行 --p 或者 p-- 操作(即一次向后移动一个位置)。(个人:one step前进后退,支持赋值操作,是否相等比较操作,多遍扫描)。
- 。除此之外,假设 p 是一个随机访问迭代器,
- ,则 p 还支持以下操作:
- p+=i:使得 p 往后移动 i 个元素。
- p-=i:使得 p 往前移动 i 个元素。
- p+i:返回 p 后面第 i 个元素的迭代器。
- p-i:返回 p 前面第 i 个元素的迭代器。
- p[i]:返回 p 后面第 i 个元素的引用
此外,两个随机访问迭代器 p1、p2 还可以用 <、>、<=、>= 运算符进行比较。另外,表达式 p2-p1 也是有定义的,其返回值表示 p2 所指向元素和 p1 所指向元素的序号之差(也可以说是 p2 和 p1 之间(左右闭区间)的元素个数减一)。
- (个人:任意step前进后退,赋值操作,下标运算,是否相等比较操作,大小比较运算,两个随机迭代器offset的直接作差运算,多遍扫描)
表 1 所示,是 C++ 11 标准中,不同容器指定使用的迭代器类型。(个人:也就是不同的容器,内部定义的是不同的迭代器类型)
容器 | 对应的迭代器类型 |
---|---|
array | 随机访问迭代器 |
vector | 随机访问迭代器 |
deque | 随机访问迭代器 |
list |
双向迭代器(个人:也就是双向 链表只能双向一个一个地遍历, 不能双向跳着遍历,所以其内部 定义的为双向迭代器) |
set / multiset | 双向迭代器 |
map / multimap |
双向迭代器(个人:关联容器红黑树, 双向迭代器,不能正反跳着遍历) |
forward_list | 前向迭代器 |
unordered_map / unordered_multimap | 前向迭代器 |
unordered_set / unordered_multiset |
前向迭代器(个人:关联容器哈希表, 前向迭代器,只能单向one step遍历) |
stack | 不支持迭代器 |
queue |
不支持迭代器(个人:栈和队列不支持 迭代器,而是通过成员函数进行遍历,例如 top(),pop()等) |
注意,容器适配器 stack 和 queue 没有迭代器,它们包含有一些成员函数,可以用来对元素进行访问。
迭代器)
迭代器定义方式 | 具体格式 |
---|---|
正向迭代器 | 容器类名::iterator 迭代器名; |
常量正向迭代器 | 容器类名::const_iterator 迭代器名; |
反向迭代器 | 容器类名::reverse_iterator 迭代器名; |
常量反向迭代器 | 容器类名::const_reverse_iterator 迭代器名; |
注:表 2 中的反向迭代器全称为 "反向迭代器适配器",
- 通过定义以上几种迭代器,就可以读取它指向的元素,*迭代器名就表示迭代器指向的元素。
- 其中,常量迭代器和非常量迭代器的分别在于,通过非常量迭代器还能修改其指向的元素。
- 另外,反向迭代器和正向迭代器的区别在于:
- 对正向迭代器进行 ++ 操作时,迭代器会指向容器中的后一个元素;
- 而对反向迭代器进行 ++ 操作时,迭代器会指向容器中的前一个元素。
注意,以上 4 种定义迭代器的方式,并不是每个容器都适用。
- 有一部分容器同时支持以上 4 种方式(个人:容器的迭代器类型为随机访问迭代器或者双向迭代器,则这4种定义方式都支持;容器的迭代器类型为forward iterator,则仅仅支持正向迭代器和常量正向迭代器这两种方式,不支持其他定义方式),
- 而有些容器只支持其中部分的定义方式,例如 forward_list 容器只支持定义正向迭代器,不支持定义反向迭代器。(个人:这是很显然的,forward_list是单向链表,通过next指针遍历下一个元素,如果它可以定义反向迭代器,则++时是指向容器中的前一个元素,这是办不到的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | //遍历 vector 容器。 #include <iostream> //需要引入 vector 头文件 #include <vector> using namespace std; int main() { vector< int > v{ 1,2,3,4,5,6,7,8,9,10 }; //v被初始化成有10个元素 cout << "第一种遍历方法:" << endl; //size返回元素个数 for ( int i = 0; i < v.size(); ++i) cout << v[i] << " " ; //像普通数组一样使用vector容器 cout << endl << "第二种遍历方法:" << endl; //创建一个正向迭代器,当然,vector也支持其他 3 种定义迭代器的方式 vector< int >::iterator i; //用 != 比较两个迭代器 for (i = v.begin(); i != v.end(); ++i) cout << *i << " " ; cout << endl << "第三种遍历方法:" << endl; for (i = v.begin(); i < v.end(); ++i) //用 < 比较两个迭代器 cout << *i << " " ; cout << endl << "第四种遍历方法:" << endl; i = v.begin(); while (i < v.end()) { //间隔一个输出 cout << *i << " " ; i += 2; // 随机访问迭代器支持 "+= 整数" 的操作 } } |
再举一个例子,我们知道,list 容器的迭代器类型是双向迭代器。假设 v 和 i 的定义如下:
1 2 3 4 | //创建一个 v list容器 list< int > v; //创建一个常量正向迭代器,同样,list也支持其他三种定义迭代器的方式。 list< int >::const_iterator i; |
- 则以下代码是合法的:(个人:对于比较,双向迭代器仅仅支持是否相等比较,不支持大小比较)
1 2 | for (i = v.begin(); i != v.end(); ++i) cout << *i; |
- 以下代码则不合法,因为双向迭代器不支持用“<”进行比较:
1 2 | for (i = v.begin(); i < v.end(); ++i) cout << *i; |
- 以下代码也不合法,因为双向迭代器不支持用下标随机访问元素:
1 2 | for ( int i=0; i<v.size(); ++i) cout << v[i]; |
其实在 C++ 中,数组也是容器。数组的迭代器就是指针,而且是随机访问迭代器。例如,对于数组 int a[10],int * 类型的指针就是其迭代器。则 a、a+1、a+2 都是 a 的迭代器。
C++序列式容器(STL序列式容器)是什么
所谓序列容器,即以线性排列(类似普通数组的存储方式)来存储某一指定类型(例如 int、double 等)的数据,需要特殊说明的是,该类容器并不会自动对存储的元素按照值的大小进行排序。
- array<T,N>(固定数组):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值;
- vector<T>(可变大小数组):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数);
- deque<T>(双端队列):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;
- list<T>(双向链表):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意第几个元素的速度要比前三种容器慢,这是因为 list<T> 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
- forward_list<T>(单向链表):和 list 容器非常类似,只不过它以单向链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比list快、更节省内存的容器。
- 注意,其实除此之外,stack<T> 和 queue<T> 本质上也属于序列容器,只不过它们都是在 deque 容器的基础上改头换面而成,通常更习惯称它们为容器适配器,(个人:也就是说,在C++中我们在数据结构中比较常用的队列queue,栈stack,都是C++中双端队列deque容器的容器适配器)
C++ array(STL array)容器
array 容器是 C++ 11 标准中新增的序列容器,
- 简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全,且效率并没有因此变差。
- 和其它容器不同,array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法借由增加或移除元素而改变其大小,它只允许访问或者替换存储的元素。
array容器以类模板的形式定义在 <array> 头文件,并位于命名空间 std 中,如下所示:
1 2 3 4 | namespace std{ template < typename T, size_t N> class array; } |
因此,在使用该容器之前,代码中需引入 <array> 头文件,并默认使用 std 命令空间,如下所示:
1 2 | #include <array> using namespace std; |
在 array<T,N> 类模板中,T 用于指明容器中的存储的具体数据类型,N 用于指明容器的大小,需要注意的是,这里的 N 必须是常量,不能用变量表示。(个人:这里N为constexpr类型)
(个人:讲解一个容器的用法时,顺序按照先讲解如何构造,初始化,再讲解增删改查这些基本的操作)
array 容器初始化
array 容器有多种初始化方式,
-
1
std::array<
double
, 10> values;
但是,由于未显式指定这10个元素的值,因此各个元素的值是不确定的(array 容器不会做默认初始化操作)。
1 2 3 4 5 6 7 8 9 10 11 12 | #include <iostream> #include<array> using namespace std; int main(){ array< float ,10> x; for ( auto i:x){ cout<<i<< " " ; } return 0; } |
这一点和普通的数组一样,
1 2 3 4 5 6 7 8 9 | #include <iostream> using namespace std; int main() { float a[5]; for ( auto x:a) cout<<x<< " " ; return 0; } |
- 通过如下创建 array 容器的方式,可以将所有的元素初始化为 0 或者和默认元素类型等效的值: (个人:列表初始化,没有列出初始值,调用的是默认构造函数,如果没有默认构造函数,不使用列表形式时,基本类型没有初始化,是随机值,使用列表形式时,基本类型初始化为0)
123456789101112
#include <iostream>
#include<array>
using
namespace
std;
int
main(){
array<
float
,10> values{};
for
(
auto
i:values){
cout<<i<<
" "
;
}
return
0;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include <iostream> #include<array> using namespace std; class A { public : int x; int y; string s; A() :x(2), y(3),s( "hello world!" ) {}; }; int main() { A a; cout << a.x << " " << a.y << " " <<a.s<< endl; cout << "=======================" << endl; array<A, 2> values{}; for ( auto i : values) { cout << i.x << " " << i.y << " " <<i.s<<endl; } return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include <iostream> #include<array> using namespace std; class A { public : int x; int y; string s; }; int main() { A a; cout << a.x << " " << a.y << " " <<a.s<< endl; cout << "=======================" << endl; array<A, 2> values{}; for ( auto i : values) { cout << i.x << " " << i.y << " " <<i.s<<endl; } return 0; } |
- 当然,在创建 array 容器的实例时,也可以像创建常规数组那样对元素进行初始化:(个人:列表初始化提供列表初始值)
1 | std::array< double , 10> values {0.5,1.0,1.5,,2.0}; |
可以看到,这里只初始化了前 4 个元素,剩余的元素都会被初始化为 0.0。
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 异常。正是由于 array 容器中包含了 at() 这样的成员函数,使得操作元素时比普通数组更安全。 |
front() | 返回容器中第一个元素的直接引用,该函数不适用于空的 array 容器。 |
back() | 返回容器中最后一个元素的直接应用,该函数同样不适用于空的 array 容器。 |
data() | 返回一个指向容器首个元素的指针。利用该指针,可实现复制容器中所有元素等类似功能。 |
fill(val) | 将 val 这个值赋值给容器中的每个元素。(个人:填充成员函数) |
array1.swap(array2) | 交换 array1 和 array2 容器中的所有元素,但前提是它们具有相同的长度和类型。(个人:swap交换成员函数) |
- 除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 array 容器包含的 begin() 和 end() 成员函数不同的是,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。
- 当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;
- 如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。
- 另外,在<array>头文件中还重载了 get() 全局函数,该重载函数的功能是访问容器中指定的元素,并返回该元素的引用。(个人:这是一个函数模板,
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include <iostream> //需要引入 array 头文件 #include <array> using namespace std; int main() { std::array< int , 4> values{}; //初始化 values 容器为 {0,1,2,3} for ( int i = 0; i < values.size(); i++) { values.at(i) = i; } //使用 get() 重载函数输出指定位置元素 cout << get<3>(values) << endl; //如果容器不为空,则输出容器中所有的元素 if (!values.empty()) { for ( auto val = values.begin(); val < values.end(); val++) { cout << *val << " " ; } } } |
array随机访问迭代器
array 容器模板类中的 begin()和end() 成员函数返回的都是正向迭代器,(个人:array里面定义的迭代器类型为随机访问迭代器,所以这里得到的正向迭代器为随机访问迭代器),它们分别指向「首元素」和「尾元素+1」 的位置。在实际使用时,我们可以利用它们实现初始化容器或者遍历容器中元素的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #include <iostream> //需要引入 array 头文件 #include <array> using namespace std; int main() { array< int , 5>values; int h = 1; auto first = values.begin(); auto last = values.end(); //初始化 values 容器为{1,2,3,4,5} while (first != last) { *first = h; ++first; h++; } first = values.begin(); while (first != last) { cout << *first << " " ; ++first; } return 0; } |
与此同时,还可以使用全局的 begin() 和end() 函数来从容器中获取迭代器,因为当操作对象为array容器时,它们和 begin()/end() 成员函数是通用的。所以上面代码中,first 和 last 还可以像下面这样定义:
1 2 | auto first = std::begin(values); auto last = std::end (values); |
array 模板类中还提供了 rbegin()/rend() 和 crbegin()/crend() 成员函数,它们每对都可以分别得到指向最一个元素和第一个元素前一个位置的随机访问迭代器,又称它们为反向迭代器,在使用反向迭代器进行 ++ 或 -- 运算时,++ 指的是迭代器向左移动一位,-- 指的是迭代器向右移动一位,即这两个运算符的功能也“互换”了。在反向迭代器上使用 ++ 递增运算符,会让迭代器用一种和普通正向迭代器移动方向相反的方式移动。
array容器访问元素的几种方式
- 可以通过容器名[]的方式直接访问和使用容器中的元素,这和C++标准数组访问元素的方式相同,(个人:传统的下标访问方式)
1 | values[4] = values[3] + 2.O*values[1]; |
需要注意的是,使用如上这样方式,由于没有做任何边界检查,所以即便使用越界的索引值去访问或存储元素,也不会被检测到。
- 为了能够有效地避免越界访问的情况,可以使用 array 容器提供的 at() 成员函数,(个人:有安全检查的下标成员函数),例如 :
1 | values.at (4) = values.at(3) + 2.O*values.at(1); |
当传给 at() 的索引是一个越界值时,程序会抛出 std::out_of_range 异常。因此当需要访问容器中某个指定元素时,建议大家使用at(),除非确定索引没有越界。
为什么 array 容器在重载 [] 运算符时,没有实现边界检查的功能呢?答案很简单,因为性能。如果每次访问元素,都去检查索引值,无疑会产生很多开销。当不存在越界访问的可能时,就能避免这种开销。
- 除此之外,还提供了全局的get<n> 模板函数,它是一个辅助函数,能够获取到容器的第 n 个元素。需要注意的是,该模板函数中,参数的实参必须是一个在编译时可以确定的常量表达式,
1 2 3 4 5 6 7 8 9 10 11 | #include <iostream> #include <array> #include <string> using namespace std; int main() { array<string, 5> words{ "one" , "two" , "three" , "four" , "five" }; cout << get<3>(words) << endl; // Output words[3] //cout << get<6>(words) << std::endl; //越界,会发生编译错误 return 0; } |
- 另外,array容器提供了data() 成员函数,通过调用该函数可以得到指向容器首个元素的指针。通过该指针,我们可以获得容器中的各个元素,
1 2 3 4 5 6 7 8 9 | #include <iostream> #include <array> using namespace std; int main() { array< int , 5> words{1,2,3,4,5}; cout << *( words.data()+1); //输出2 return 0; } |
array 容器提供的 size() 函数能够返回容器中元素的个数,函数返回值为 size_t类型,array 容器之所以提供 empty() 成员函数的原因,对于其他元素个数可变或者元素可删除的容器(例如 vector、deque 等)来说,它们使用 empty() 时的机制是一样的,因此为它们提供了一个一致性的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #include <iostream> #include <iomanip> #include <array> using namespace std; int main() { array< int , 5> values1; array< int , 5> values2; //初始化 values1 为 {0,1,2,3,4} for ( size_t i = 0; i < values1.size(); ++i) { values1.at(i) = i; } cout << "values1[0] is : " << values1[0] << endl; cout << "values1[1] is : " << values1.at(1) << endl; cout << "values1[2] is : " << get<2>(values1) << endl; //初始化 values2 为{10,11,12,13,14} int initvalue = 10; for ( auto & value : values2) { value = initvalue; initvalue++; } cout << "Values1 is : " ; for ( auto i = values1.begin(); i < values1.end(); i++) { cout << *i << " " ; } cout << endl << "Values2 is : " ; for ( auto i = values2.begin(); i < values2.end(); i++) { cout << *i << " " ; } return 0; } |
array容器:普通数组的“升级版”
和C++ 普通数组存储数据的方式一样,C++ 标准库保证使用 array 容器存储的所有元素一定会位于连续且相邻的内存中,通过如下代码也可以验证这一点:
1 2 3 4 5 6 7 8 9 | #include <iostream> #include <array> using namespace std; int main() { array< int , 5>a{ 1,2,3 }; cout << &a[2] << " " << &a[0] + 2 << endl; return 0; } |
因此在实际编程过程中,我们完全有理由去尝试,
- 在原本使用普通数组的位置,改由 array 容器去实现。
- 用 array 容器替换普通数组的好处是,array 模板类中已经封装好了大量实用的方法,在提高开发效率的同时,代码的运行效率也会大幅提高。
举个例子,我们完全可以使用 array 容器去存储 char* 或 const char* 类型的字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <iostream> #include <array> #include <cstring> using namespace std; int main() { array< char , 50>a{1,2,3}; strcpy (&a[0], "http://www.baidu.com" ); printf ( "%s" , &a[0]); cout<< "\n===============================" <<endl; for ( auto x:a){ cout<<x<< " " ; } return 0; } |
注意,array容器的大小必须保证能够容纳复制进来的数据,而且如果是存储字符串的话,还要保证在存储整个字符串的同时,在其最后放置一个\0作为字符串的结束符。此程序中,strcpy() 在拷贝字符串的同时,会自动在最后添加\0。
前面提到,使用 array 容器代替普通数组,最直接的好处就是 array 模板类中已经为我们写好了很多实用的方法,可以大大提高我们编码效率。例如,
- array容器提供的at() 成员函数,可以有效防止越界操纵数组的情况;
- fill() 函数可以实现数组的快速初始化;
- swap() 函数可以轻松实现两个相同数组(类型相同,大小相同)中元素的互换。
- 另外,当两个 array 容器满足大小相同并且保存元素的类型相同时,两个array容器可以直接直接做赋值操作。(个人:因为array是一个类模板,内部定义有赋值运算符,所以可以直接对两个array进行赋值操作,而传统的数组不可以直接赋值)
- 不仅如此,在满足以上2个条件的基础上,如果其保存的元素也支持比较运算符,就可以用任何比较运算符直接比较两个 array 容器。两个容器比较大小的原理,和两个字符串比较大小是一样的,即从头开始,逐个取两容器中的元素进行大小比较(根据 ASCII 码表),直到遇到两个不相同的元素,那个元素的值大,则该容器就大。
1 2 3 4 5 6 7 8 9 10 11 12 | #include <iostream> #include <array> using namespace std; int main() { array< char , 50>addr1{ "http://www.baidu.com" }; array< char , 50>addr2{ "http://www.python.cn.com" }; addr1.swap(addr2); printf ( "addr1 is:%s\n" , addr1.data()); printf ( "addr2 is:%s\n" , addr2.data()); return 0; } |
1 2 3 4 5 6 7 8 9 10 11 | #include <iostream> #include <array> using namespace std; int main() { array< char , 50>addr1{ "http://www.baidu.com" }; array< char , 50>addr2{ "http://www.python.cn" }; addr1 = addr2; printf ( "%s" , addr1.data()); return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <iostream> #include <array> using namespace std; int main() { array< char , 50>addr1{ "http://www.baidu.com" }; array< char , 50>addr2{ "http://www.java.com.cn" }; if (addr1 == addr2) { std::cout << "addr1 == addr2" << std::endl; } if (addr1 < addr2) { std::cout << "addr1 < addr2" << std::endl; } if (addr1 > addr2) { std::cout << "addr1 > addr2" << std::endl; } return 0; } |
总之,可以这样认为,array 容器就是普通数组的“升级版”,使用普通数组能实现的,使用 array 容器都可以实现,而且无论是代码功能的实现效率,还是程序执行效率,都比普通数组更高。
C++ STL vector容器
vector 容器是 STL 中最常用的容器之一,它和 array 容器非常类似,都可以看做是对 C++ 普通数组的“升级版”。不同之处在于,
- array 实现的是静态数组(容量固定的数组),
- 而 vector 实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector 会动态调整所占用的内存空间,整个过程无需人工干预。
vector 常被称为向量容器,
- 因为该容器擅长在尾部插入或删除元素,在常量时间内就可以完成,时间复杂度为O(1);
- 而对于在容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)。
vector 容器以类模板 vector<T>( T 表示存储元素的类型)的形式定义在 <vector> 头文件中,并位于 std 命名空间中。因此,在创建该容器之前,代码中需包含如下内容:
1 2 | #include <vector> using namespace std; |
创建vector容器的几种方式
-
1
std::vector<
double
> values;
这是一个空的vector容器,因为容器中没有元素,所以没有为其分配空间。
当添加第一个元素(比如使用 push_back() 函数)时,vector 会自动分配内存。可以通过调用 reserve() 成员函数来增加容器的容量:
1 | values.reserve(20); |
这样就设置了容器的内存分配,即至少可以容纳 20 个元素。注意,如果vector的容量在执行此语句之前,已经大于或等于 20 个元素,那么这条语句什么也不做;另外,调用 reserve() 不会影响已存储的元素,也不会生成任何元素。还需注意的是,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector<T> 容器的元素可能已经被复制或移到了新的内存地址。所以后续再使用这些迭代器时,最好重新生成一下。
- 除了创建空 vector 容器外,还可以在创建的同时指定初始值,比如:(个人:这里使用的是列表初始化的形式)
1 | std::vector< int > primes {2, 3, 5, 7, 11, 13, 17, 19}; |
- 在创建 vector 容器时,也可以指定元素个数:(个人:调用参数为元素个数的构造函数)
1 | std::vector< double > values(20); |
如此,values 容器开始时就有 20 个元素,它们的默认初始值都为 0。
注意,圆括号 () 和大括号 {} 是有区别的,前者(例如 (20) )表示元素的个数,而后者(例如 {20} ) 则表示 vector 容器中只有一个元素 20。
- 如果不想用 0 作为默认值,也可以指定一个其它值,例如:(个人:调用参数为元素个数,元素默认值的构造函数)
1 | std::vector< double > values(20, 1.0); |
第二个参数指定了所有元素的初始值,因此这 20 个元素的值都是 1.0。
值得一提的是,圆括号 () 中的 2 个参数,既可以是常量,也可以用变量来表示,例如:
1 2 3 | int num=20; double value =1.0; std::vector< double > values(num, value); |
- 通过存储元素类型相同的其它 vector 容器,也可以创建新的 vector 容器,例如:(个人:这里调用的是拷贝构造函数)
1 2 | std::vector< char >value1(5, 'c' ); std::vector< char >value2(value1); |
- 如果不想复制其它容器中所有的元素,可以用一对指针或者迭代器来指定初始值的范围,例如:(个人:通过指定一个迭代器范围,通过一个同类型的其它容器的一部分范围内的元素进行初始化操作)
1 2 3 4 | int array[]={1,2,3}; std::vector< int >values(array, array+2); //values 将保存{1,2} std::vector< int >value1{1,2,3,4,5}; std::vector< int >value2(std::begin(value1),std::begin(value1)+3); //value2保存{1,2,3} |
vector容器包含的成员函数
函数成员 | 函数功能 |
---|---|
begin() | 返回指向容器中第一个元素的迭代器。 |
end() | 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。 |
rbegin() | 返回指向最后一个元素的迭代器。 |
rend() | 返回指向第一个元素所在位置前一个位置的迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
size() | 返回实际元素个数。 |
max_size() | 返回元素个数的最大值。这通常是一个很大的值,一般是 232-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()(个人:/ɪmˈpleɪs/,安放 ) |
在指定的位置直接生成一个元素。 |
emplace_back() | 在序列尾部直接生成一个元素。 |
- 除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 vector 容器包含的 begin() 和 end() 成员函数不同,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。
- 还有一个全局的std::swap(x , y) 非成员函数(其中 x 和 y 是存储相同类型元素的 vector 容器),它和 swap() 成员函数的功能完全相同,仅使用语法上有差异。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include <iostream> #include <vector> using namespace std; int main() { //初始化一个空vector容量 vector< char >value; //向value容器中的尾部依次添加 S、T、L 字符 value.push_back( 'S' ); value.push_back( 'T' ); value.push_back( 'L' ); //调用 size() 成员函数容器中的元素个数 printf ( "元素个数为:%d\n" , value.size()); //使用迭代器遍历容器 for ( auto i = value.begin(); i < value.end(); i++) { cout << *i << " " ; } cout << endl; //向容器开头插入字符 value.insert(value.begin(), 'C' ); cout << "首个元素为:" << value.at(0) << endl; return 0; } |
vector容器迭代器用法
成员函数 | 功能 |
---|---|
begin() | 返回指向容器中第一个元素的正向迭代器;如果是const类型容器,在该函数返回的是常量正向迭代器。 |
end() | 返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。 |
rbegin() | 返回指向最后一个元素的反向迭代器;如果是 const 类型容器,在该函数返回的是常量反向迭代器。 |
rend() | 返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,在该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。 |
cbegin() | 和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。 |
除此之外,C++ 11 新添加的 begin() 和 end() 全局函数也同样适用于 vector 容器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <iostream> //需要引入 vector 头文件 #include <vector> using namespace std; int main() { vector< int >values{1,2,3,4,5}; auto first = values.rbegin(); auto end = values.rend(); while (first != end) { cout << *first << " " ; ++first; } return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <iostream> #include <vector> using namespace std; int main() { vector< int >values; int val = 1; for ( auto first = values.begin(); first < values.end(); ++first, val++) { *first = val; //初始化的同时输出值 cout << *first; } return 0; } |
运行程序可以看到,什么也没有输出。这是因为,对于空的 vector 容器来说,begin() 和 end() 成员函数返回的迭代器是相等的,即它们指向的是同一个位置。 所以,对于空的 vector 容器来说,可以通过调用 push_back() 或者借助 resize() 成员函数实现初始化容器的目的。
除此之外,vector容器在申请更多内存的同时,容器中的所有元素可能会被复制或移动到新的内存地址,这会导致之前创建的迭代器失效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <iostream> #include <vector> using namespace std; int main() { vector< int >values{1,2,3}; cout << "values 容器首个元素的地址:" << values.data() << endl; auto first = values.begin(); auto end = values.end(); //增加 values 的容量 values.reserve(20); cout << "values 容器首个元素的地址:" << values.data() << endl; while (first != end) { cout << *first<< " " ; ++first; } cout<< "\n重新获取迭代器:\n" ; auto i = values.begin(); auto j = values.end(); while (i != j) { cout << *i<< " " ; ++i; } return 0; } |
可以看到,values 容器在增加容量之后,首个元素的存储地址发生了改变,此时再使用先前创建的迭代器,显然是错误的。因此,为了保险起见,每当 vector 容器的容量发生变化时,我们都要对之前创建的迭代器重新初始化一遍。
vector容器访问元素的几种方式
- vector 的索引从 0 开始,这和普通数组一样。通过使用索引,总是可以访问到 vector 容器中现有的元素。(个人:下标访问元素方式)
1 2 | vector< int > values{1,2,3,4,5}; values[0] = values[1] + values[2] + values[3] + values[4]; |
值得一提的是,容器名[n]这种获取元素的方式,需要确保下标 n 的值不会超过容器的容量(可以通过 capacity() 成员函数获取),否则会发生越界访问的错误。
- 和 array 容器一样,vector 容器也提供了 at() 成员函数,当传给 at() 的索引会造成越界时,会抛出std::out_of_range异常。(个人:通过at()成员函数,这个安全的下标访问方式)
1 | values.at(0) = values.at(1) + values.at(2) + values.at(3) + values.at(4); |
- 除此之外,vector 容器还提供了 2 个成员函数,即 front() 和 back(),它们分别返回 vector 容器中第一个和最后一个元素的引用,通过利用这 2 个函数返回的引用,可以访问(甚至修改)容器中的首尾元素。
- 另外,vector 容器还提供了 data() 成员函数,该函数的功能是返回指向容器中首个元素的指针。通过该指针也可以访问甚至修改容器中的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <iostream> #include <vector> using namespace std; int main() { vector< int > values{1,2,3,4,5}; //输出容器中第 3 个元素的值 cout << *(values.data() + 2) << endl; //修改容器中第 2 个元素的值 *(values.data() + 1) = 10; cout << *(values.data() + 1) << endl; return 0; } |
vector容量(capacity)和大小(size)的区别
- vector 容器的容量(用 capacity 表示),指的是在不分配更多内存的情况下,容器可以保存的最多元素个数;而 vector 容器的大小(用 size 表示),指的是它实际所包含的元素个数。
- 显然,vector 容器的大小不能超出它的容量,在大小等于容量的基础上,只要增加一个元素,就必须分配更多的内存。注意,这里的“更多”并不是 1 个。换句话说,当 vector 容器的大小和容量相等时,如果再向其添加(或者插入)一个元素,vector 往往会申请多个存储空间,而不仅仅只申请 1 个。具体多少,取决于底层算法的实现。这样做的好处是,可以很大程度上减少 vector 申请空间的次数,当后续再添加元素时,就可以节省申请空间耗费的时间。 因此,对于 vector 容器而言,当增加新的元素时,有可能很快完成(即直接存在预留空间中);也有可能会慢一些(扩容之后再放新元素)。
- 一旦vector容器的内存被重新分配,则和 vector 容器中元素相关的所有引用、指针以及迭代器,都可能会失效,最稳妥的方法就是重新生成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #include <iostream> #include <vector> using namespace std; int main() { vector< int >value{ 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47 }; cout << "value 容量是:" << value.capacity() << endl; cout << "value 大小是:" << value.size() << endl; printf ( "value首地址:%p\n" , value.data()); value.push_back(53); cout << "value 容量是(2):" << value.capacity() << endl; cout << "value 大小是(2):" << value.size() << endl; printf ( "value首地址: %p" , value.data()); return 0; } |
- 可以调用 reserve() 成员函数来增加容器的容量(但并不会改变存储元素的个数);
- 而通过调用成员函数 resize() 可以改变容器的大小,并且该函数也可能会导致 vector 容器容量的增加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #include <iostream> #include <vector> using namespace std; int main() { vector< int >value{ 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47 }; cout << "value 容量是:" << value.capacity() << endl; cout << "value 大小是:" << value.size() << endl; value.reserve(20); cout << "value 容量是(2):" << value.capacity() << endl; cout << "value 大小是(2):" << value.size() << endl; //将元素个数改变为 21 个,所以会增加 6 个默认初始化的元素 value.resize(21); //将元素个数改变为 21 个,新增加的 6 个元素默认值为 99。 //value.resize(21,99); //当需要减小容器的大小时,会移除多余的元素。 //value.resize(20); cout << "value 容量是(3):" << value.capacity() << endl; cout << "value 大小是(3):" << value.size() << endl; for ( auto x:value){ cout<<x<< " " ; } return 0; } |
可以看到,
- 仅通过 reserve() 成员函数增加 value 容器的容量,其大小并没有改变;
- 但通过 resize() 成员函数改变 value 容器的大小,它的容量可能会发生改变。
- 另外需要注意的是,通过resize() 成员函数减少容器的大小(多余的元素会直接被删除),不会影响容器的容量。
在实际场景中,我们可能需要将容器的容量和大小保存在变量中,要知道 vector<T> 对象的容量和大小类型都是 vector<T>::size_type 类型。(个人:这个类型是在vector这个类模板中定义的类型别名),因此,当定义一个变量去保存这些值时,可以如下所示:
1 2 | vector< int >::size_type cap = value.capacity(); vector< int >::size_type size = value.size(); |
size_type 类型是定义在由 vector 类模板生成的 vecotr 类中的,它表示的真实类型和操作系统有关,在 32 位架构下普遍表示的是 unsigned int 类型,而在 64 位架构下普通表示 unsigned long 类型。
当然,我们还可以使用 auto 关键字代替 vector<int>::size_type,比如:
1 2 | auto cap = value.capacity(); auto size = value.size(); |
深度剖析C++ vector容器的底层实现机制
STL 众多容器中,vector 是最常用的容器之一,其底层所采用的数据结构非常简单,就只是一段连续的线性内存空间。
通过分析vector 容器的源代码不难发现,它就是使用3个迭代器(可以理解成指针)来表示的:
1 2 3 4 5 6 7 8 9 | //_Alloc 表示内存分配器,此参数几乎不需要我们关心 template < class _Ty, class _Alloc = allocator<_Ty>> class vector{ ... protected : pointer _Myfirst; pointer _Mylast; pointer _Myend; }; |
其中,
- _Myfirst 指向的是 vector 容器对象的起始字节位置;
- _Mylast 指向当前最后一个元素的末尾字节;
- _myend 指向整个 vector 容器所占用内存空间的末尾字节。
如图1所示,通过这3个迭代器,就可以表示出一个已容纳2个元素,容量为5的 vector 容器。
在此基础上,将3个迭代器两两结合,还可以表达不同的含义,例如:
- _Myfirst 和 _Mylast可以用来表示vector容器中目前已被使用的内存空间;
- _Mylast 和 _Myend可以用来表示vector容器目前空闲的内存空间;
- _Myfirst 和 _Myend可以用表示vector容器的容量。
对于空的vector容器,由于没有任何元素的空间分配,因此_Myfirst、_Mylast和_Myend均为null。
通过灵活运用这 3 个迭代器,vector 容器可以轻松的实现诸如首尾标识、大小、容量、空容器判断等几乎所有的功能,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:
- 完全弃用现有的内存空间,重新申请更大的内存空间;
- 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
- 最后将旧的内存空间释放。
这也就解释了,为什么vector容器在进行扩容后,与其相关的指针、引用以及迭代器可能会失效的原因。
由此可见,vector 扩容是非常耗时的。为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。 vector 容器扩容时,不同的编译器申请更多内存空间的量是不同的。以 VS 为例,它会扩容现有容器容量的 50%。
vector添加元素(push_back()和emplace_back())
emplace_back() 该函数是 C++ 11 新增加的,其功能和push_back() 相同,都是在 vector 容器的尾部添加一个元素。
emplace_back()和push_back() 的区别,就在于底层实现的机制不同。
- push_back() 向容器尾部添加元素时,首先这个元素得已经存在,(个人:也就是以添加的元素作为函数参数,是已经存在的),然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);
- 而emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | #include <vector> #include <iostream> using namespace std; class testDemo { public : testDemo( int num):num(num){ std::cout << "调用构造函数" << endl; } testDemo( const testDemo& other) :num(other.num) { std::cout << "调用拷贝构造函数" << endl; } testDemo(testDemo&& other) :num(other.num) { std::cout << "调用移动构造函数" << endl; } ~testDemo(){ cout<< "调用析构函数" <<endl; } private : int num; }; int main() { cout << "emplace_back:" << endl; std::vector<testDemo> demo1; demo1.emplace_back(2); cout << "push_back:" << endl; std::vector<testDemo> demo2; demo2.push_back(2); cout<< "====================================\n" ; } |
在此基础上,将 testDemo 类中的移动构造函数注释掉,再运行程序会发现,运行结果变为:
由此可以看出,push_back() 在底层实现时,会优先选择调用移动构造函数,如果没有才会调用拷贝构造函数。 显然完成同样的操作,push_back() 的底层实现过程比 emplace_back() 更繁琐,换句话说,emplace_back() 的执行效率比 push_back() 高。因此,在实际使用时,建议大家优先选用 emplace_back()。
vector插入元素(insert()和emplace())
语法格式 | 用法说明 |
---|---|
iterator insert(pos,elem) (个人:single) |
在迭代器 pos 指定的位置之前插入一个新元素elem,并返回表示新插入元素位置的迭代器。 |
iterator insert(pos,n,elem) (个人:fill) |
在迭代器 pos 指定的位置之前插入 n 个元素 elem,并返回表示第一个新插入元素位置的迭代器。 |
iterator insert(pos,first,last) (个人:range) |
在迭代器 pos 指定的位置之前,插入其他容器(不仅限于vector)中位于 [first,last) 区域的所有元素,并返回表示第一个新插入元素位置的迭代器。 |
iterator insert(pos,initlist) | 在迭代器 pos 指定的位置之前,插入初始化列表(用大括号{}括起来的多个元素,中间有逗号隔开)中所有的元素,并返回表示第一个新插入元素位置的迭代器。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <iostream> #include <vector> #include <array> using namespace std; int main() { 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] << " " ; } return 0; } |
emplace() 是C++ 11标准新增加的成员函数,用于在 vector 容器指定位置之前插入一个新的元素。 再次强调,emplace() 每次只能插入一个元素,而不是多个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | #include <vector> #include <iostream> using namespace std; class testDemo { public : testDemo( int num) :num(num) { std::cout << "调用构造函数" << ",the num is:" <<num<< endl; } testDemo( const testDemo& other) :num(other.num) { std::cout << "调用拷贝构造函数" << ",the num is:" <<num<< endl; } testDemo(testDemo&& other) :num(other.num) { std::cout << "调用移动构造函数" << ",the num is:" <<num<< endl; } testDemo& operator=( const testDemo& other); ~testDemo(){ cout<< "调用析构函数" << ",the num is:" <<num<< endl; } private : int num; }; testDemo& testDemo::operator=( const testDemo& other) { cout<< "调用赋值运算符," <<other.num<< "---->" <<num<< endl; this ->num = other.num; return * this ; } int main() { cout << "emplace:" << endl; std::vector<testDemo> demo1; demo1.reserve(50); demo1.emplace_back( 1); demo1.emplace_back( 2); demo1.emplace_back( 3); demo1.emplace_back( 4); demo1.emplace_back( 5); cout<< "****************************" <<endl; demo1.emplace(demo1.begin()+1, 6); cout<< "============================\n" ; return 0; } |
vector删除元素的几种方式
基于不同场景的需要,删除 vecotr 容器的元素,可以使用表1中所示的函数(或者函数组合)。
函数 | 功能 |
---|---|
pop_back() | 删除 vector 容器中最后一个元素,该容器的大小(size)会减 1,但容量(capacity)不会发生改变。 |
erase(pos) | 删除 vector 容器中 pos 迭代器指定位置处的元素,并返回指向被删除元素下一个位置元素的迭代器。该容器的大小(size)会减 1,但容量(capacity)不会发生改变。 |
swap()、pop_back() | 先调用 swap() 函数交换要删除的目标元素和容器最后一个元素的位置,然后使用 pop_back() 删除该目标元素。 |
erase(beg,end) | 删除 vector 容器中位于迭代器 [beg,end)指定区域内的所有元素,并返回指向被删除区域下一个位置元素的迭代器。该容器的大小(size)会减小,但容量(capacity)不会发生改变。 |
|
|
clear() | 删除 vector 容器中所有的元素,使其变成空的 vector 容器。该函数会改变 vector 的大小(变为 0),但不是改变其容量。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <vector> #include <iostream> using namespace std; int main() { vector< int >demo{ 1,2,3,4,5 }; auto iter = demo.erase(demo.begin() + 1); //删除元素 2 //输出 dmeo 容器新的size cout << "size is :" << demo.size() << endl; //输出 demo 容器新的容量 cout << "capacity is :" << demo.capacity() << endl; for ( int i = 0; i < demo.size(); i++) { cout << demo[i] << " " ; } //iter迭代器指向元素 3 cout << endl << *iter << endl; return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <vector> #include <iostream> //swap() 函数在头文件 <algorithm> 和 <utility> 中都有定义,使用时引入其中一个即可。 #include <algorithm> using namespace std; int main() { vector< int >demo{ 1,2,3,4,5 }; //交换要删除元素和最后一个元素的位置 swap(*(std::begin(demo)+1),*(std::end(demo)-1)); //等同于 swap(demo[1],demo[4]) //交换位置后的demo容器 for ( int i = 0; i < demo.size(); i++) { cout << demo[i] << " " ; } demo.pop_back(); cout << endl << "size is :" << demo.size() << endl; cout << "capacity is :" << demo.capacity() << endl; //输出demo 容器中剩余的元素 for ( int i = 0; i < demo.size(); i++) { cout << demo[i] << " " ; } return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include <vector> #include <iostream> using namespace std; int main() { std::vector< int > demo{ 1,2,3,4,5 }; //删除 2、3 auto iter = demo.erase(demo.begin()+1, demo.end() - 2); cout << "size is :" << demo.size() << endl; cout << "capacity is :" << demo.capacity() << endl; for ( int i = 0; i < demo.size(); i++) { cout << demo[i] << " " ; } return 0; } |
如何避免vector容器进行不必要的扩容?
vector 容器扩容的整个过程,和realloc() 函数的实现方法类似,大致分为以下 4 个步骤:
- 分配一块大小是当前vector 容量几倍的新存储空间。注意,多数 STL 版本中的 vector 容器,其容器都会以 2 的倍数增长,也就是说,每次 vector 容器扩容,它们的容量都会提高到之前的 2 倍;
- 将 vector 容器存储的所有元素,依照原有次序从旧的存储空间复制到新的存储空间中;
- 析构掉旧存储空间中存储的所有元素;
- 释放旧的存储空间。
通过以上分析不难看出,vector 容器的扩容过程是非常耗时的,并且当容器进行扩容后,之前和该容器相关的所有指针、迭代器以及引用都会失效。因此在使用vector 容器过程中,我们应尽量避免执行不必要的扩容操作。要实现这个目标,可以借助 vector 模板类中提供的 reserve() 成员方法。
成员方法 | 功能 |
---|---|
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 容器刚刚构造出来的那一刻,就应该借助 reserve() 成员方法为其扩充足够大的容量。 举个例子,假设我们想创建一个包含 1~1000 的 vector<int>,通常会这样实现:
1 2 3 4 | vector< int >myvector; for ( int i = 1; i <= 1000; i++) { myvector.push_back(i); } |
值得一提的是,上面代码的整个循环过程中,vector 容器会进行 2~10 次自动扩容(多数的 STL 标准库版本中,vector 容器通常会扩容至当前容量的 2 倍,而这里 ),程序的执行效率可想而知。 在上面程序的基础上,下面代码演示了如何使用 reserve() 成员方法尽量避免 vector 容器执行不必要的扩容操作:
1 2 3 4 5 6 | vector< int >myvector; myvector.reserve(1000); cout << myvector.capacity(); for ( int i = 1; i <= 1000; i++) { myvector.push_back(i); } |
当然在实际场景中,我们可能并不知道 vector 容器到底要存储多少个元素。这种情况下,可以先预留出足够大的空间,当所有元素都存储到 vector 容器中之后,再去除多余的容量。
vector swap()成员方法
vector 模板类中提供有一个 shrink_to_fit() 成员方法,该方法的功能是将当前 vector 容器的容量缩减至和实际存储元素的个数相等。
1 2 | myvector.shrink_to_fit(); cout << "7、当前 myvector 拥有 " << myvector.size() << " 个元素,容量为 " << myvector.capacity() << endl; |
vector模板类中还提供有swap()成员方法,该方法的基础功能是交换2个相同类型的vector容器(交换容量和存储的所有元素),但其也能用于去除 vector 容器多余的容量。 如果想用 swap() 成员方法去除当前 vector 容器多余的容量时,可以套用如下的语法格式:
1 | vector<T>(x).swap(x); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <iostream> #include <vector> using namespace std; int main() { vector< int >myvector; //手动为 myvector 扩容 myvector.reserve(1000); cout << "1、当前 myvector 拥有 " << myvector.size() << " 个元素,容量为 " << myvector.capacity() << endl; //利用 myvector 容器存储 10 个元素 for ( int i = 1; i <= 10; i++) { myvector.push_back(i); } //将 myvector 容量缩减至 10 vector< int >(myvector).swap(myvector); cout << "2、当前 myvector 拥有 " << myvector.size() << " 个元素,容量为 " << myvector.capacity() << endl; return 0; } |
显然,第 16 行代码成功将 myvector 容器的容量 1000 修改为 10,此行代码的执行流程可细分为以下 3 步:
- 先执行 vector<int>(myvector),此表达式会调用 vector 模板类中的拷贝构造函数,从而创建出一个临时的 vector 容器(后续称其为 tempvector)。 值得一提的是,tempvector 临时容器并不为空,因为我们将 myvector 作为参数传递给了复制构造函数,该函数会将 myvector 容器中的所有元素拷贝一份,并存储到 tempvector 临时容器中。 注意,vector 模板类中的拷贝构造函数只会为拷贝的元素分配存储空间。换句话说,tempvector 临时容器中没有空闲的存储空间,其容量等于存储元素的个数。
- 然后借助 swap() 成员方法对 tempvector 临时容器和 myvector 容器进行调换,此过程不仅会交换 2 个容器存储的元素,还会交换它们的容量。换句话说经过 swap() 操作,myvetor 容器具有了tempvector临时容器存储的所有元素和容量,同时 tempvector 也具有了原 myvector 容器存储的所有元素和容量。
- 当整条语句执行结束时,临时的tempvector 容器会被销毁,其占据的存储空间都会被释放。注意,这里释放的其实是原 myvector 容器占用的存储空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include <iostream> #include <vector> using namespace std; int main() { cout<< "before swap:" <<endl; vector< int >x1{1,2,3}; x1.reserve(20); cout << "x1拥有 " << x1.size() << " 个元素,容量为 " << x1.capacity() << endl; vector< int >x2{7,8,9,10,11,12}; x2.reserve(50); cout << "x2拥有 " << x2.size() << " 个元素,容量为 " << x2.capacity() << endl; cout<< "x1[0]地址为:" <<&x1[0]<< ",x2[0]地址为:" <<&x2[0]<<endl; cout<< "after swap:" <<endl; x1.swap(x2); cout<< "====================================================" <<endl; cout << "x1拥有 " << x1.size() << " 个元素,容量为 " << x1.capacity() << endl; cout << "x2拥有 " << x2.size() << " 个元素,容量为 " << x2.capacity() << endl; cout<< "**********************************" <<endl; cout<< "x1[0]地址为:" <<&x1[0]<< ",x2[0]地址为:" <<&x2[0]<<endl; return 0; } |
swap() 方法还可以用来清空 vector 容器。当 swap() 成员方法用于清空 vector 容器时,可以套用如下的语法格式:
1 | vector<T>().swap(x); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <iostream> #include <vector> using namespace std; int main() { vector< int >myvector; //手动为 myvector 扩容 myvector.reserve(1000); cout << "1、当前 myvector 拥有 " << myvector.size() << " 个元素,容量为 " << myvector.capacity() << endl; //利用 myvector 容器存储 10 个元素 for ( int i = 1; i <= 10; i++) { myvector.push_back(i); } //清空 myvector 容器 vector< int >().swap(myvector); cout << "2、当前 myvector 拥有 " << myvector.size() << " 个元素,容量为 " << myvector.capacity() << endl; return 0; } |
C++ STL deque容器
deque 是 double-ended queue 的缩写,又称双端队列容器。
- deque 容器也擅长在序列尾部添加或删除元素(时间复杂度为O(1)),而不擅长在序列中间添加或删除元素。
- 和 vector 不同的是,deque 还擅长在序列头部添加或删除元素,所耗费的时间复杂度也为常数阶O(1)。
- 并且更重要的一点是,deque容器中存储元素并不能保证所有元素都存储到连续的内存空间中。
- 当需要向序列两端频繁的添加或删除元素时,应首选deque容器。
deque 容器以模板类 deque<T>(T 为存储元素的类型)的形式在 <deque> 头文件中,并位于 std 命名空间中。因此,在使用该容器之前,代码中需要包含下面两行代码:
1 2 | #include <deque> using namespace std; |
创建deque容器的几种方式
- 创建一个没有任何元素的空 deque 容器:
1 | std::deque< int > d; |
- 创建一个具有 n 个元素的 deque 容器,其中每个元素都采用对应类型的默认值:(个人:调用参数为元素个数的构造函数)
1 | std::deque< int > d(10); |
此行代码创建一个具有 10 个元素(默认都为 0)的 deque 容器。
- 创建一个具有 n 个元素的 deque 容器,并为每个元素都指定初始值,例如:(个人:调用参数为元素个数,元素默认值的构造函数)
1 | std::deque< int > d(10, 5) |
如此就创建了一个包含 10 个元素(值都为 5)的 deque 容器。
- 在已有 deque 容器的情况下,可以通过拷贝该容器创建一个新的 deque 容器,例如:(个人:调用拷贝构造函数)
1 2 | std::deque< int > d1(5); std::deque< int > d2(d1); |
注意,采用此方式,必须保证新旧容器存储的元素类型一致。
- 通过拷贝其他类型容器中指定区域内的元素(也可以是普通数组),可以创建一个新容器,例如:(个人:通过指定其它的容器的一段范围进行初始化操作,这个其它的容器可以不是deque类型)
1 2 3 4 5 6 | //拷贝普通数组,创建deque容器 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} |
deque容器可利用的成员函数
基于deque双端队列的特点,该容器包含一些 array、vector 容器都没有的成员函数。 表 1 中罗列了 deque 容器提供的所有成员函数。
函数成员 | 函数功能 |
---|---|
begin() | 返回指向容器中第一个元素的迭代器。 |
end() | 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。 |
rbegin() | 返回指向最后一个元素的迭代器。 |
rend() | 返回指向第一个元素所在位置前一个位置的迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
size() | 返回实际元素个数。 |
max_size() | 返回容器所能容纳元素个数的最大值。这通常是一个很大的值,一般是 232-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 容器。这 2 个函数的操作对象既可以是容器,也可以是普通数组。
- 当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;
- 如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。
- deque 容器还有一个std::swap(x , y) 非成员函数(其中 x 和 y 是存储相同类型元素的 deque 容器),它和 swap() 成员函数的功能完全相同,仅使用语法上有差异。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include <iostream> #include <deque> using namespace std; int main() { //初始化一个空deque容量 deque< int >d; //向d容器中的尾部依次添加 1,2,3 d.push_back(1); //{1} d.push_back(2); //{1,2} d.push_back(3); //{1,2,3} //向d容器的头部添加 0 d.push_front(0); //{0,1,2,3} //调用 size() 成员函数输出该容器存储的字符个数。 printf ( "元素个数为:%d\n" , d.size()); //使用迭代器遍历容器 for ( auto i = d.begin(); i < d.end(); i++) { cout << *i << " " ; } cout << endl; return 0; } |
deque容器迭代器
deque 容器迭代器的类型为随机访问迭代器,
成员函数 | 功能 |
---|---|
begin() | 返回指向容器中第一个元素的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。 |
end() | 返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。 |
rbegin() | 返回指向最后一个元素的反向迭代器;如果是 const 类型容器,在该函数返回的是常量反向迭代器。 |
rend() | 返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,在该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。 |
cbegin() | 和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。 |
C++ 11新添加的 begin() 和 end() 全局函数也同样适用于 deque 容器。即当操作对象为 deque 容器时,其功能分别和表 1 中的 begin()、end() 成员函数相同,
deque 模板类中还提供了 rbegin() 和 rend() 成员函数,它们分别表示指向最后一个元素和第一个元素前一个位置的随机访问迭代器,又常称为反向迭代器。 需要注意的是,在使用反向迭代器进行 ++ 或 -- 运算时,++ 指的是迭代器向左移动一位,-- 指的是迭代器向右移动一位,即这两个运算符的功能也“互换”了。
当向 deque 容器添加元素时,deque 容器会申请更多的内存空间,同时其包含的所有元素可能会被复制或移动到新的内存地址(原来占用的内存会释放),这会导致之前创建的迭代器失效。在对容器做添加元素的操作之后,如果仍需要使用之前以创建好的迭代器,为了保险起见,一定要重新生成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <iostream> #include <deque> using namespace std; int main() { deque< int >d; d.push_back(1); auto first = d.begin(); cout << *first << endl; //添加元素,会导致 first 失效 d.push_back(9); cout << *first << endl; return 0; } |
deque容器底层实现原理
deque容器的存储结构:
和vector容器采用连续的线性空间不同,deque容器存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于在内存的不同区域。为了管理这些连续空间,deque 容器用数组(数组名假设为 map)存储着各个连续空间的首地址。也就是说,map数组中存储的都是指针,指向那些真正用来存储数据的各个连续空间(如图 1 所示)。
通过建立map数组,deque容器申请的这些分段的连续空间就能实现“整体连续”的效果。
- 换句话说,当 deque 容器需要在头部或尾部增加存储空间时,它会申请一段新的连续空间,同时在 map 数组的开头或结尾添加指向该空间的指针(个人:猜测应该是map数组一开始主要是在中间保存指针,map数组的开头和结尾有大部分空白闲置),由此该空间就串接到了 deque 容器的头部或尾部。
- 如果 map 数组满了怎么办?很简单,再申请一块更大的连续空间供 map 数组使用,将原有数据(很多指针)拷贝到新的 map 数组中,然后释放旧的空间。(个人:这里只是map这个数组满了,申请的是一个更大的map数组,原来的一段一段的连续内存不需要变动,保持不变,因为毕竟map数组中保存的只是指向这些一段一段连续内存的指针)
deque 容器的分段存储结构,提高了在序列两端添加或删除元素的效率,但也使该容器迭代器的底层实现变得更复杂。
deque容器迭代器的底层实现:
由于deque容器底层将序列中的元素分别存储到了不同段的连续空间中,因此要想实现迭代器的功能,必须先解决如下 2 个问题:(个人:逻辑上deque是一个顺序容器,它的迭代器是一个随机访问迭代器,而我们使用迭代器的方式是一致的,但是deque的底层是将序列中的元素分别存储到了不同段的连续空间中,所以deque的用来描述这个顺序容器中逻辑顺序序列的位置的迭代器,需要包含以下信息)
- 迭代器在遍历deque容器时,必须能够确认它所在的连续空间在 map 数组中的位置;
- 迭代器在遍历某个具体的连续空间时,必须能够判断自己是否已经处于空间的边缘位置(个人:两个边缘位置,左边缘,右边缘)。如果是,则一旦前进或者后退,就需要跳跃到上一个或者下一个连续空间中。
为了实现遍历 deque 容器的功能,deque 迭代器定义了如下的结构:
1 2 3 4 5 6 7 8 | template < class T,...> struct __deque_iterator{ ... T* cur; T* first; T* last; map_pointer node; //map_pointer 等价于 T** } |
可以看到,迭代器内部包含 4 个指针,它们各自的作用为:
- cur:指向当前正在遍历的元素;
- first:指向当前连续空间的首地址;
- last:指向当前连续空间的末尾地址;
- node:它是一个二级指针,用于指向 map 数组中存储的指向当前连续空间的指针。
借助这4个指针,deque 迭代器对随机访问迭代器支持的各种运算符进行了重载,能够对 deque 分段连续空间中存储的元素进行遍历。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | //当迭代器处于当前连续空间边缘的位置时,如果继续遍历,就需要跳跃到其它的连续空间中,该函数可用来实现此功能 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 容器底层存储序列的结构,
- 以及 deque 容器迭代器的内部结构之后,
接下来看看 deque 容器究竟是如何实现的。 deque 容器除了维护先前讲过的 map 数组,还需要维护 start、finish 这 2 个 deque 迭代器。以下为 deque 容器的定义:
1 2 3 4 5 6 7 8 9 10 11 | //_Alloc为内存分配器 template < class _Ty, class _Alloc = allocator<_Ty>> class deque{ ... protected : iterator start; iterator finish; map_pointer map; ... } |
其中,
- start迭代器记录着map数组中首个连续空间的信息,finish 迭代器记录着 map 数组中最后一个连续空间的信息。(个人:也就是说,map数组我们刚开始使用时使用的主要是中间部分,首尾部分是处于闲置状态的),
- 另外需要注意的是,和普通deque迭代器不同,start迭代器中的cur指针指向的是连续空间中首个元素;而 finish 迭代器中的 cur 指针指向的是连续空间最后一个元素的下一个位置。
因此,deque容器的底层实现如图2所示。
借助start和finish,以及 deque 迭代器中重载的诸多运算符,就可以实现deque容器提供的大部分成员函数,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //begin() 成员函数 iterator begin() { return start;} //end() 成员函数 iterator end() { return finish;} //front() 成员函数 reference front(){ return *start;} //back() 成员函数 reference back(){ iterator tmp = finish; --tmp; return *tmp; } //size() 成员函数 size_type size() const { return finish - start;} //deque迭代器重载了 - 运算符 //enpty() 成员函数 bool empty() const { return finish == start;} |
deque容器访问元素的方法
- 容器名[n]的这种方式,不仅可以访问容器中的元素,还可以对其进行修改。但需要注意的是,使用此方法需确保下标 n 的值不会超过容器中存储元素的个数,否则会发生越界访问的错误。
- 如果想有效地避免越界访问,可以使用 deque 模板类提供的 at() 成员函数,由于该函数会返回容器中指定位置处元素的引用形式,因此利用该函数的返回值,既可以访问指定位置处的元素,如果需要还可以对其进行修改。 不仅如此,at() 成员函数会自行判定访问位置是否越界,如果越界则抛出std::out_of_range异常。
- 除此之外,deque 容器还提供了 2 个成员函数,即 front() 和 back(),它们分别返回 deque容器中第一个和最后一个元素的引用,通过利用它们的返回值,可以访问(甚至修改)容器中的首尾元素。
- 注意,和 vector 容器不同,deque 容器没有提供 data() 成员函数,同时 deque 容器在存储元素时,也无法保证其会将元素存储在连续的内存空间中,因此尝试使用指针去访问 deque 容器中指定位置处的元素,是非常危险的。(个人:也就是deque的底层是采用的一段一段的连续空间来存储元素的,如果提供data,则完全没有意义,没有用处,不像array和vector的底层实现是数组,是一个连续的内存空间,借助于data函数,可以依次访问各个元素,对于deque,这种用法不支持)
- 另外,结合 deque 模板类中和迭代器相关的成员函数,可以实现遍历 deque 容器中指定区域元素的方法。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <iostream> #include <deque> using namespace std; int main() { deque< int > d{ 1,2,3,4,5 }; //从元素 2 开始遍历 auto first = d.begin() + 1; //遍历至 5 结束(不包括 5) auto end = d.end() - 1; while (first < end) { cout << *first << " " ; ++first; } return 0; } |
deque容器添加和删除元素方法
成员函数 | 功能 |
---|---|
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() | 删除容器中所有的元素。 |
在实际应用中,常用 emplace()、emplace_front() 和 emplace_back() 分别代替 insert()、push_front() 和 push_back()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #include <deque> #include <iostream> using namespace std; int main() { deque< int >d; //调用push_back()向容器尾部添加数据。 d.push_back(2); //{2} //调用pop_back()移除容器尾部的一个数据。 d.pop_back(); //{} //调用push_front()向容器头部添加数据。 d.push_front(2); //{2} //调用pop_front()移除容器头部的一个数据。 d.pop_front(); //{} //调用 emplace 系列函数,向容器中直接生成数据。 d.emplace_back(2); //{2} d.emplace_front(3); //{3,2} //emplace() 需要 2 个参数,第一个为指定插入位置的迭代器,第二个是插入的值。 d.emplace(d.begin() + 1, 4); //{3,4,2} for ( auto i : d) { cout << i << " " ; } //erase()可以接受一个迭代器表示要删除元素所在位置 //也可以接受 2 个迭代器,表示要删除元素所在的区域。 d.erase(d.begin()); //{4,2} d.erase(d.begin(), d.end()); //{},等同于 d.clear() return 0; } |
语法格式 | 功能 |
---|---|
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 指定的位置之前,插入初始化列表(用大括号{}括起来的多个元素,中间有逗号隔开)中所有的元素,并返回表示第一个新插入元素位置的迭代器。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <iostream> #include <deque> #include <array> using namespace std; int main() { std::deque< int > d{ 1,2 }; //第一种格式用法 d.insert(d.begin() + 1, 3); //{1,3,2} //第二种格式用法 d.insert(d.end(), 2, 5); //{1,3,2,5,5} //第三种格式用法 std::array< int , 3>test{ 7,8,9 }; d.insert(d.end(), test.begin(), test.end()); //{1,3,2,5,5,7,8,9} //第四种格式用法 d.insert(d.end(), { 10,11 }); //{1,3,2,5,5,7,8,9,10,11} for ( int i = 0; i < d.size(); i++) { cout << d[i] << " " ; } return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | #include <deque> #include <iostream> using namespace std; class testDemo { public : testDemo( int num) :num(num) { std::cout << "调用构造函数" << ",the num is:" <<num<< endl; } testDemo( const testDemo& other) :num(other.num) { std::cout << "调用拷贝构造函数" << ",the num is:" <<num<< endl; } testDemo(testDemo&& other) :num(other.num) { std::cout << "调用移动构造函数" << ",the num is:" <<num<< endl; } testDemo& operator=( const testDemo& other); ~testDemo(){ cout<< "调用析构函数" << ",the num is:" <<num<< endl; } private : int num; }; testDemo& testDemo::operator=( const testDemo& other) { cout<< "调用赋值运算符," <<other.num<< "---->" <<num<< endl; this ->num = other.num; return * this ; } int main() { cout << "emplace:" << endl; std::deque<testDemo> demo1; demo1.emplace_back( 1); demo1.emplace_back( 2); demo1.emplace_back( 3); demo1.emplace_back( 4); demo1.emplace_back( 5); cout<< "****************************" <<endl; demo1.emplace(demo1.begin()+1, 6); cout<< "============================\n" ; return 0; } |
C++ STL list容器
STL list 容器,又称双向链表容器,即该容器的底层是以双向链表的形式实现的。这意味着,list 容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间中。图1展示了 list 双向链表容器是如何存储元素的。
可以看到,list 容器中各个元素的前后顺序是靠指针来维系的,每个元素都配备了 2 个指针,分别指向它的前一个元素和后一个元素。其中第一个元素的前向指针总为 null,因为它前面没有元素;同样,尾部元素的后向指针也总为 null。
- 基于这样的存储结构,list 容器具有一些其它容器(array、vector 和 deque)所不具备的优势,即它可以在序列已知的任何位置快速插入或删除元素(时间复杂度为O(1))。
- 使用 list 容器的缺点是,它不能像 array 和 vector 那样,通过位置直接访问元素。举个例子,如果要访问 list 容器中的第 6 个元素,它不支持容器对象名[6]这种语法格式,正确的做法是从容器中第一个元素或最后一个元素开始遍历容器,直到找到该位置。
- 实际场景中,如何需要对序列进行大量添加或删除元素的操作,而直接访问元素的需求却很少,这种情况建议使用list 容器存储序列。
list 容器以模板类 list<T>(T 为存储元素的类型)的形式在<list>头文件中,并位于std命名空间中。因此,在使用该容器之前,代码中需要包含下面两行代码:
1 2 | #include <list> using namespace std; |
list容器的创建
- 创建一个没有任何元素的空 list 容器:
1 | std::list< int > values; |
- 创建一个包含 n 个元素的 list 容器:
1 | std::list< int > values(10); |
通过此方式创建 values 容器,其中包含 10 个元素,每个元素的值都为相应类型的默认值(int类型的默认值为 0)。
- 创建一个包含 n 个元素的 list 容器,并为每个元素指定初始值。例如:
1 | std::list< int > values(10, 5); |
如此就创建了一个包含 10 个元素并且值都为 5 个 values 容器。
- 在已有 list 容器的情况下,通过拷贝该容器可以创建新的 list 容器。例如:
1 2 | std::list< int > value1(10); std::list< int > value2(value1); |
注意,采用此方式,必须保证新旧容器存储的元素类型一致。
- 通过拷贝其他类型容器(或者普通数组)中指定区域内的元素,可以创建新的 list 容器。例如:
1 2 3 4 5 6 7 | //拷贝普通数组,创建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}啊 |
(个人:可以看到,这几个顺序容器拥有的构造函数是一致的,能够使用的初始化方式是相同的)
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() (个人:针对双向链表的功能util函数,反转链表) |
反转容器中元素的顺序。 |
- 除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 list 容器包含的 begin() 和 end() 成员函数不同,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。
- 当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;
- 如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。
- list 容器还有一个std::swap(x , y)非成员函数(其中 x 和 y 是存储相同类型元素的 list 容器),它和 swap() 成员函数的功能完全相同,仅使用语法上有差异。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include <iostream> #include <list> using namespace std; int main() { //创建空的 list 容器 std::list< double > values; //向容器中添加元素 values.push_back(3.1); values.push_back(2.2); values.push_back(2.9); cout << "values size:" << values.size() << endl; //对容器中的元素进行排序 values.sort(); //使用迭代器输出list容器中的元素 for (std::list< double >::iterator it = values.begin(); it != values.end(); ++it) { std::cout << *it << " " ; } return 0; } |
list迭代器
迭代器函数 | 功能 |
---|---|
begin() | 返回指向容器中第一个元素的双向迭代器。 |
end() | 返回指向容器中最后一个元素所在位置的下一个位置的双向迭代器。(正向迭代器)。 |
rbegin() | 返回指向最后一个元素的反向双向迭代器。 |
rend() | 返回指向第一个元素所在位置前一个位置的反向双向迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,正向迭代器增加了 const 属性,即不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过在其基础上,正向迭代器增加了 const 属性,即不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,反向迭代器增加了 const 属性,即不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过在其基础上,反向迭代器增加了 const 属性,即不能用于修改元素。 |
前面章节已经详细介绍了 array、vector、deque 容器的迭代器,和它们相比,list 容器迭代器最大的不同在于,其配备的迭代器类型为双向迭代器,而不再是随机访问迭代器。 这意味着,假设 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 不支持使用 <、 >、 <=、 >= 比较运算符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <iostream> #include <list> using namespace std; int main() { //创建 list 容器 std::list< char > values{ 'h' , 't' , 't' , 'p' , ':' , '/' , '/' , 'w' , 'w' , 'w' , '.' , 'b' , 'a' , 'i' , 'd' , 'u' , '.' , 'c' , 'o' , 'm' }; //使用begin()/end()迭代器函数对输出list容器中的元素 for (std::list< char >::iterator it = values.begin(); it != values.end(); ++it) { std::cout << *it; } cout << endl; //使用 rbegin()/rend()迭代器函数输出 lsit 容器中的元素 for (std::list< char >::reverse_iterator it = values.rbegin(); it != values.rend();++it) { std::cout << *it; } return 0; } |
注意,程序中比较迭代器之间的关系,用的是 != 运算符,因为它不支持 < 等运算符。另外在实际场景中,所有迭代器函数的返回值都可以传给使用 auto 关键字定义的变量,因为编译器可以自行判断出该迭代器的类型。
值得一提的是,list容器在进行插入(insert())、接合(splice())等操作时,都不会造成原有的list迭代器失效,甚至进行删除操作,而只有指向被删除元素的迭代器失效,其他迭代器不受任何影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include <iostream> #include <list> using namespace std; int main() { //创建 list 容器 std::list< char > values{ 'h' , 't' , 't' , 'p' , ':' , '/' , '/' , 'w' , 'w' , 'w' , '.' , 'b' , 'a' , 'u' , '.' , 'c' , 'o' , 'm' ,}; //创建 begin 和 end 迭代器 std::list< char >::iterator begin = values.begin(); std::list< char >::iterator end = values.end(); //头部和尾部插入字符 '1' values.insert(begin, '1' ); values.insert(end, '1' ); while (begin != end) { std::cout << *begin; ++begin; } return 0; } |
可以看到,在进行插入操作之后,仍使用先前创建的迭代器遍历容器,虽然程序不会出错,但由于插入位置的不同,可能会遗漏新插入的元素。
list容器底层存储结构
list容器的底层是用双向链表实现的,甚至一些 STL 版本中(比如 SGI STL),list 容器的底层实现使用的是双向循环链表。
如图 1 所示,使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针(图 1 以箭头表示)来维持。
list 容器节点结构:
双向链表的各个节点中存储的不仅仅是元素的值,还应包含 2 个指针,分别指向前一个元素和后一个元素。 通过查看 list 容器的源码实现,其对节点的定义如下:
1 2 3 4 5 6 7 8 | template < typename T,...> struct __List_node{ //... __list_node<T>* prev; __list_node<T>* next; T myval; //... } |
list容器迭代器的底层实现:
和 array、vector 这些容器迭代器的实现方式不同,由于 list 容器的元素并不是连续存储的,所以该容器迭代器中,必须包含一个可以指向 list 容器结点的指针,并且该指针还可以借助重载的 *、++、--、==、!= 等运算符,实现迭代器正确的递增、递减、取值等操作。 因此,list 容器迭代器的实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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; } //... } |
可以看到,迭代器的移动就是通过操作节点的指针实现的。(个人:链表就是通过节点之间的指针来体现线性关系的)
list容器的底层实现:
本节开头提到,不同版本的 STL 标准库中,list 容器的底层实现并不完全一致,但原理基本相同。这里以 SGI STL 中的 list 容器为例,讲解该容器的具体实现过程。 SGI STL 标准库中,list 容器的底层实现为双向循环链表,相比双向链表结构的好处是在构建 list 容器时,只需借助一个指针即可轻松表示 list 容器的首尾元素。 如下是 SGI STL 标准库中对 list 容器的定义:
1 2 3 4 5 6 7 8 | template < class T,...> class list { //... //指向链表的头节点,并不存放数据 __list_node<T>* node; //...以下还有list 容器的构造函数以及很多操作函数 } |
另外,为了更方便的实现 list 模板类提供的函数,该模板类在构建容器时,会刻意在容器链表中添加一个空白节点,并作为 list 链表的首个节点(又称头节点)。使用双向链表实现的 list 容器,其内部通常包含 2 个指针,并分别指向链表中头部的空白节点和尾部的空白节点(也就是说,其包含 2 个空白节点)。
我们经常构造空的 list 容器,其用到的构造函数如下所示:(个人:这里的实现是双向循环链表实现,只需要一个头结点)
1 2 3 4 5 6 7 8 | list() { empty_initialize(); } // 用于空链表的建立 void empty_initialize() { node = get_node(); //初始化节点 node->next = node; // 前置节点指向自己 node->prev = node; // 后置节点指向自己 } |
显然,即便是创建空的 list 容器,它的内部实际上也包含有 1 个节点。
除此之外,list 模板类中还提供有带参的构造函数,它们的实现过程大致分为以下 2 步:
- 调用 empty_initialize() 函数,构造带有头节点的空 list 容器链表;(个人:头结点指针为node)
- 将各个参数按照次序插入到空的 list 容器链表中。
由此可以总结出,list容器实际上就是一个带有头节点的双向循环链表。如图 2 所示,此为存有 2 个元素的 list 容器:
在此基础上,通过借助 node 头节点,就可以实现 list 容器中的所有成员函数,比如:
1 2 3 4 5 6 7 8 9 10 11 | //begin()成员函数 __list_iterator<T> begin(){ return (*node).next;} //end()成员函数 __list_iterator<T> end(){ return node;} //empty()成员函数 bool empty() const { return (*node).next == node;} //front()成员函数 T& front() { return *begin();} //back()成员函数 T& back() { return *(--end();)} //... |
list 访问元素的几种方法
不同于之前学过的 STL 容器,访问 list 容器中存储元素的方式很有限,
- 即要么使用 front() 和 back() 成员函数,(个人:通过双向链表的底层实现,例如,双向循环链表实现,可以通过头结点node直接得到首尾节点,所以list很显然的有front()和back()成员函数),
- 要么使用 list 容器迭代器。
- list 容器不支持随机访问,未提供下标操作符 [] 和 at() 成员函数,也没有提供 data() 成员函数。
list添加(插入)元素方法
list 模板类中,与“添加或插入新元素”相关的成员方法有如下几个:
- push_front():向 list 容器首个元素前添加新元素;
- push_back():向 list 容器最后一个元素后添加新元素;
- emplace_front():在容器首个元素前直接生成新的元素;
- emplace_back():在容器最后一个元素后直接生成新的元素;
- emplace():在容器的指定位置直接生成新的元素;
- insert():在指定位置插入新元素;
- splice():将其他 list 容器存储的多个元素添加到当前 list 容器的指定位置处。
以上这些成员方法中,除了 insert() 和 splice() 方法有多种语法格式外,其它成员方法都仅有 1 种语法格式。
list insert()成员方法:
insert() 成员方法的语法格式有 4 种,如表 1 所示。
语法格式 | 用法说明 |
---|---|
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 指定的位置之前,插入初始化列表(用大括号 { } 括起来的多个元素,中间有逗号隔开)中所有的元素,并返回表示第一个新插入元素位置的迭代器。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #include <iostream> #include <list> #include <array> using namespace std; int main() { 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} for ( auto p = values.begin(); p != values.end(); ++p) { cout << *p << " " ; } return 0; } |
同样是实现插入元素的功能,无论是push_front()、push_back() 还是 insert(),都有以 emplace 为名且功能和前者相同的成员函数。这是因为,后者是 C++ 11 标准新添加的,在大多数场景中,都可以完全替代前者实现同样的功能。更重要的是,实现同样的功能,emplace 系列方法的执行效率更高。
list splice()成员方法:
和 insert() 成员方法相比,splice() 成员方法的作用对象是其它 list 容器,其功能是将其它 list 容器中的元素添加到当前 list 容器中指定位置处。 splice() 成员方法的语法格式有 3 种,如表 2 所示。(个人:其他插入方法是值的复制,会生成新的节点,而splice是节点的转移,会改变原来节点的连接,不会生成新的节点)
1 2 3 4 5 6 7 8 9 | //entire list (1) void splice (const_iterator position, list& x); void splice (const_iterator position, list&& x); //single element (2) void splice (const_iterator position, list& x, const_iterator i); void splice (const_iterator position, list&& x, const_iterator i); //element range (3) void splice (const_iterator position,list& x,const_iterator first, const_iterator last); void splice (const_iterator position,list&& x,const_iterator first, const_iterator last); |
语法格式 | 功能 |
---|---|
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 指明的位置处。 |
我们知道,list 容器底层使用的是链表存储结构,splice() 成员方法移动元素的方式是,将存储该元素的节点从 list 容器底层的链表中摘除,然后再链接到当前 list 容器底层的链表中。这意味着,当使用 splice() 成员方法将 x 容器中的元素添加到当前容器的同时,该元素会从 x 容器中删除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include <iostream> #include <list> using namespace std; int main() { //创建并初始化 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, //调用第二种语法格式,将 it 指向的元素 2 移动到 mylist2.begin() 位置处 mylist2.splice(mylist2.begin(), mylist1, it); // mylist1: 1 10 20 30 3 4 // mylist2: 2 // 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 cout << "mylist1 包含 " << mylist1.size() << "个元素" << endl; cout << "mylist2 包含 " << mylist2.size() << "个元素" << endl; //输出 mylist2 容器中存储的数据 cout << "mylist2:" ; for ( auto iter = mylist2.begin(); iter != mylist2.end(); ++iter) { cout << *iter << " " ; } return 0; } |
empty()和size()都可以判断容器是否为空,谁更好?
到目前为止,我们已经了解了 C++ STL 标准库中 vector、deque 和 list 这 3 个容器的功能和具体用法。学习过程中,读者是否想过一个问题,即这些容器的模板类中都提供了 empty() 成员方法和 size() 成员方法,它们都可以用来判断容器是否为空。那么,在实际场景中,到底应该使用哪一种呢? 建议使用 empty() 成员方法。理由很简单,无论是哪种容器,只要其模板类中提供了 empty() 成员方法,使用此方法都可以保证在O(1)时间复杂度内完成对“容器是否为空”的判断;但对于list容器来说,使用 size() 成员方法判断“容器是否为空”,可能要消耗 O(n) 的时间复杂度。 注意,这个结论不仅适用于 vector、deque 和 list 容器,后续还会讲解更多容器的用法,该结论也依然适用。那么,为什么 list 容器这么特殊呢?这和 list 模板类提供了独有的 splice() 成员方法有关。如果将 size() 设计为 O(1) 时间复杂度(个人:也就是内部存储一个一个变量,用来表示当前的节点个数,如果想知道当前list的节点个数,只需要访问这个变量就可以了,这可以在O(1)时间内做到,但是为了维持这个变量,当list的节点个数发生改变时,要时刻更新这个变量的值),则由于 splice() 成员方法会修改 list 容器存储元素的个数,因此该方法中就需要添加更新 size 变量的代码(更新方式无疑是通过遍历链表来实现),这也就意味着 splice() 成员方法的执行效率将无法达到 O(1);反之,如果将 splice() 成员方法的执行效率提高到 O(1),则 size() 成员方法将无法实现 O(1) 的时间复杂度。 也就是说,list 容器中的 size() 和 splice() 总有一个要做出让步,即只能实现其中一个方法的执行效率达到O(1)。但不论怎样,选用empty()判断容器是否为空,效率总是最高的。所以,如果程序中需要判断当前容器是否为空,应优先考虑使用 empty()。
list删除元素
成员函数 | 功能 |
---|---|
pop_front() | 删除位于 list 容器头部的一个元素。 |
pop_back() | 删除位于 list 容器尾部的一个元素。 |
erase() | 该成员函数既可以删除 list 容器中指定位置处的元素,也可以删除容器中某个区域内的多个元素。 |
clear() | 删除 list 容器存储的所有元素。 |
remove(val) | 删除容器中所有等于 val 的元素。 |
unique() | 删除容器中相邻的重复元素,只保留一份。 |
remove_if() | 删除容器中满足条件的元素。 |
erase() 成员函数有以下 2 种语法格式:
1 2 | iterator erase (iterator position); iterator erase (iterator first, iterator last); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <iostream> #include <list> using namespace std; int main() { list< int >values{ 1,2,3,4,5 }; //指向元素 1 的迭代器 auto del = values.begin(); //迭代器右移,改为指向元素 2 ++del; values.erase(del); //{1,3,4,5} for ( auto begin = values.begin(); begin != values.end(); ++begin) { cout << *begin << " " ; } return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include <iostream> #include <list> using namespace std; int main() { list< int >values{ 1,2,3,4,5 }; //指定删除区域的左边界 auto first = values.begin(); ++first; //指向元素 2 //指向删除区域的右边界 auto last = values.end(); --last; //指向元素 5 //删除 2、3 和 4 values.erase(first, last); for ( auto begin = values.begin(); begin != values.end(); ++begin) { cout << *begin << " " ; } return 0; } |
erase() 成员函数是按照被删除元素所在的位置来执行删除操作,如果想根据元素的值来执行删除操作,可以使用remove()成员函数。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <iostream> #include <list> using namespace std; int main() { list< char >values{ 'a' , 'b' , 'c' , 'd' }; values. remove ( 'c' ); for ( auto begin = values.begin(); begin != values.end(); ++begin) { cout << *begin << " " ; } return 0; } |
unique() 函数也有以下 2 种语法格式:
1 2 | void unique() void unique(BinaryPredicate) //传入一个二元谓词函数 |
以上 2 种格式都能实现去除list容器中相邻重复的元素,仅保留一份。但第 2 种格式的优势在于,我们能自定义去重的规则,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <iostream> #include <list> using namespace std; //二元谓词函数 bool demo( double first, double second) { return ( int (first) == int (second)); } int main() { list< double > mylist{ 1,1.2,1.2,3,4,4.5,4.6 }; //删除相邻重复的元素,仅保留一份 mylist.unique(); //{1, 1.2, 3, 4, 4.5, 4.6} for ( auto it = mylist.begin(); it != mylist.end(); ++it) cout << *it << ' ' ; cout << endl; //demo 为二元谓词函数,是我们自定义的去重规则 mylist.unique(demo); for ( auto it = mylist.begin(); it != mylist.end(); ++it) std::cout << *it << ' ' ; return 0; } |
注意,除了以上一定谓词函数的方式,还可以使用 lamba表达式以及函数对象的方式定义。
除此之外,通过将自定义的谓词函数传给 remove_if() 成员函数,list 容器中能使谓词函数成立的元素都会被删除。举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #include <iostream> #include <list> using namespace std; int main() { std::list< int > mylist{ 15, 36, 7, 17, 20, 39, 4, 1 }; //删除 mylist 容器中能够使 lamba 表达式成立的所有元素。 mylist.remove_if([]( int value) { return (value < 10); }); //{15 36 17 20 39} for ( auto it = mylist.begin(); it != mylist.end(); ++it) std::cout << ' ' << *it; return 0; } |
C++ STL forward_list容器
forward_list 是 C++ 11 新添加的一类容器,其底层实现和 list 容器一样,采用的也是链表结构,只不过 forward_list 使用的是单链表,而 list 使用的是双向链表(如图 1 所示)。
- 通过图 1 不难看出,使用链表存储数据最大的特点在于,其并不会将数据进行集中存储(向数组那样),换句话说,链表中数据的存储位置是分散的、随机的,整个链表中数据的线性关系通过指针来维持。因此,forward_list 容器具有和 list 容器相同的特性,即擅长在序列的任何位置进行插入元素或删除元素的操作,但对于访问存储的元素,没有其它容器(如 array、vector)的效率高。
- 另外,由于单链表没有双向链表那样灵活,因此相比list容器,forward_list容器的功能受到了很多限制。比如,由于单链表只能从前向后遍历,而不支持反向遍历,因此 forward_list 容器只提供前向迭代器,而不是双向迭代器。这意味着,forward_list 容器不具有 rbegin()、rend() 之类的成员函数。
那么,既然 forward_list 容器具有和 list 容器相同的特性,list 容器还可以提供更多的功能函数,forward_list 容器有什么存在的必要呢? 当然有,forward_list 容器底层使用单链表,也不是一无是处。
- 比如,存储相同个数的同类型元素,单链表耗用的内存空间更少,空间利用率更高,
- 并且对于实现某些操作单链表的执行效率也更高。
效率高是选用forward_list而弃用list容器最主要的原因,换句话说,只要是 list 容器和 forward_list 容器都能实现的操作,应优先选择 forward_list 容器。
由于forward_list容器以模板类 forward_list<T>(T 为存储元素的类型)的形式被包含在<forward_list>头文件中,并定义在 std 命名空间中。因此,在使用该容器之前,代码中需包含下面两行代码:
1 2 | #include <forward_list> using namespace std; |
forward_list容器的创建
- 创建一个没有任何元素的空 forward_list 容器:
1 | std::forward_list< int > values; |
- 创建一个包含 n 个元素的 forward_list 容器:
1 | std::forward_list< int > values(10); |
通过此方式创建 values 容器,其中包含 10 个元素,每个元素的值都为相应类型的默认值(int类型的默认值为 0)。
- 创建一个包含 n 个元素的 forward_list 容器,并为每个元素指定初始值。例如:
1 | std::forward_list< int > values(10, 5); |
如此就创建了一个包含 10 个元素并且值都为 5 个 values 容器。
- 在已有 forward_list 容器的情况下,通过拷贝该容器可以创建新的 forward_list 容器。例如:
1 2 | std::forward_list< int > value1(10); std::forward_list< int > value2(value1); |
注意,采用此方式,必须保证新旧容器存储的元素类型一致。
- 通过拷贝其他类型容器(或者普通数组)中指定区域内的元素,可以创建新的 forward_list 容器。例如:
1 2 3 4 5 6 | //拷贝普通数组,创建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} |
(个人:综上所述,这几个顺序容器vector,deque,list,forw_list,初始化采用统一的接口,可以使用相同的方式进行构建)
forward_list容器支持的成员函数
成员函数 | 功能 |
---|---|
before_begin() | 返回一个前向迭代器,其指向容器中第一个元素之前的位置。 |
begin() | 返回一个前向迭代器,其指向容器中第一个元素的位置。 |
end() | 返回一个前向迭代器,其指向容器中最后一个元素之后的位置。 |
cbefore_begin() | 和 before_begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
empty() | 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 |
max_size() | 返回容器所能包含元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。 |
(个人:由于forward_list是单向链表,但是底层实现时,为了操作的方面,有一个头节点,所以,我们可以有before_front()成员函数,可以在开头插入,相当于在头结点之后插入,但是没有back()成员函数,也不可以在尾部插入) | |
front() | 返回第一个元素的引用。 |
assign() | 用新元素替换容器中原有内容。 |
push_front() | 在容器头部插入一个元素。 |
emplace_front() | 在容器头部生成一个元素。该函数和 push_front() 的功能相同,但效率更高。 |
pop_front() | 删除容器头部的一个元素。 |
(个人:由于forward_list不像list是双向链表,而是单向链表,所以,只能在一个已知节点的后面插入,所以为insert_after,而没有insert) | |
emplace_after() | 在指定位置之后插入一个新元素,并返回一个指向新元素的迭代器。和 insert_after() 的功能相同,但效率更高。 |
insert_after() | 在指定位置之后插入一个新元素,并返回一个指向新元素的迭代器。 |
erase_after() | 删除容器中某个指定位置或区域内的所有元素。 |
swap() | 交换两个容器中的元素,必须保证这两个容器中存储的元素类型是相同的。 |
resize() | 调整容器的大小。 |
clear() | 删除容器存储的所有元素。 |
splice_after() |
将某个 forward_list 容器中指定位置或区域内的元素插入到另一个容器的指定位置之后。
|
(个人:下面的成员函数,是列表(包括list,forward_list)才有的功能util函数) | |
remove(val) | 删除容器中所有等于 val 的元素。 |
remove_if() | 删除容器中满足条件的元素。 |
unique() | 删除容器中相邻的重复元素,只保留一个。 |
merge() | 合并两个事先已排好序的 forward_list 容器,并且合并之后的 forward_list 容器依然是有序的。 |
sort() | 通过更改容器中元素的位置,将它们进行排序。 |
reverse() | 反转容器中元素的顺序。 |
============================================================
===================================================
============================================
1 2 3 4 5 6 7 8 9 | //entire list (1) void splice_after (const_iterator position, forward_list& fwdlst); void splice_after (const_iterator position, forward_list&& fwdlst); //single element (2) void splice_after (const_iterator position, forward_list& fwdlst, const_iterator i); void splice_after (const_iterator position, forward_list&& fwdlst, const_iterator i); //element range (3) void splice_after (const_iterator position, forward_list& fwdlst,const_iterator first, const_iterator last); void splice_after (const_iterator position, forward_list&& fwdlst,const_iterator first, const_iterator last); |
=================================
============================================
- 除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 forward_list 容器包含的 begin() 和 end() 成员函数不同,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。
- 当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;
- 如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。
- forward_list 容器还有一个std::swap(x , y)非成员函数(其中 x 和 y 是存储相同类型元素的 forward_list 容器),它和 swap() 成员函数的功能完全相同,仅使用语法上有差异。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <iostream> #include <forward_list> using namespace std; int main() { std::forward_list< int > values{1,2,3}; values.emplace_front(4); //{4,1,2,3} values.emplace_after(values.before_begin(), 5); //{5,4,1,2,3} values.reverse(); //{3,2,1,4,5} for ( auto it = values.begin(); it != values.end(); ++it) { cout << *it << " " ; } return 0; } |
和使用forward_list容器相关的函数
通过表 2 我们知道,forward_list 容器中是不提供 size() 函数的,但如果想要获取forward_list 容器中存储元素的个数,可以使用头文件 <iterator> 中的 distance() 函数。举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 | #include <iostream> #include <forward_list> #include <iterator> using namespace std; int main() { std::forward_list< int > my_words{1,2,3,4}; int count = std::distance(std::begin(my_words), std::end(my_words)); cout << count; return 0; } |
并且,forward_list 容器迭代器的移动除了使用 ++ 运算符单步移动,还能使用 advance() 函数,
比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include <iostream> #include <iterator> #include <forward_list> using namespace std; int main() { std::forward_list< int > values{1,2,3,4}; auto it = values.begin(); advance(it, 2); while (it!=values.end()) { cout << *it << " " ; ++it; } return 0; } |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析