一、STL基础知识
1、整体介绍
STL标准模板库(Standard Template Library)是基于模板技术实现的C++标准程序库。STL标准模板库为用户提供了很多在计算机科学领域里常用的基本数据结构和基本算法,这些数据结构和算法以类型参数化(type parameterized)的方式广泛适用于各种数据类型(包括自定义的数据类型),体现了泛型化程序设计的思想 。
STL包括六大组件:容器(containers)类模板、迭代器(iterators)类模板、空间配置器(allocator)、适配器(adapters)、算法(algorithms)函数模板、仿函数(functors:函数对象模板)
容器:是以模板类的方式实现的一种数据结构,可以是list、vector、二叉树之类的数据结构。因为是模板,所以任意数据类型(包括自定义的数据类型)都可以存储到容器中,形成数据对象集合。
迭代器:是基于模板类创建的对象,用户使用迭代器遍历容器中部分或全部元素就像使用指针一样方便。这就要求迭代器向上屏蔽不同容器之间的差异,并且通过运算符重载,对外提供和普通指针一样的使用接口(例如:operator++可获取下一个元素的位置,operator* 提取迭代器所指向位置的元素)。因此,每一种容器类型都必须自己提供迭代器。
算法:是一种全局的模板函数,用来通过迭代器操作容器中的数据。所以,算法和数据分离,通过迭代器联系。
仿函数:也称为函数对象(function object),就是一个普通的类,同时这个类还定义了()操作符的重载函数。(当编译器在代码中看到’对象()’的地方,就会直接调用对应的重载函数)
迭代适配器:属于特殊的迭代器,面向更复杂的容器,可能完成更复杂的功能。
空间配置器:主要用于管理内存以及创建、销毁对象。
2、STL容器
STL容器类型按照对象元素的存取方式可分为:序列式容器(Sequence containers)和关联式容器(Associated containers)。
序列式容器中每个元素保存的位置取决于插入时机和地点,和元素值无关,与普通的数组很相似。具体包括:包括:vector, deque, list。
vector:将元素置于一个动态数组中加以管理,可以随机存取元素(用索引直接存取),数组尾部添加或移除元素非常快速。但是在中部或头部安插元素比较费时;
deque:是双口队列(double-ended queue)的缩写,内部也是通过动态数组管理插入的元素。可以随机存取元素(用索引直接存取),数组头部和尾部添加或移除元素都非常快速。但是在中部或头部安插元素比较费时;
list:内部是通过双向链表管理插入的元素,不提供随机存取(按顺序找到需存取的元素,时间复杂度为O(n)),在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针;
关联式容器中每个元素的位置取决于特定的排序准则,和插入顺序无关,类似于二叉树排序。所以,管理容器的优点是能够快速访问元素。关联容器包括:set, multiset, map, multimap。
set/multiset:内部的元素依据元素值自动排序,set内的相同数值的元素只能出现一次,multiset内可包含多个数值相同的元素,内部由二叉树实现(实际上基于红黑树(RB-tree)实现),便于查找;
map/multimap:map的元素是成对的键值/实值,内部的元素依据键值自动排序,map内的相同键值的元素只能出现一次,multimaps内可包含多个键值相同的元素,内部由二叉树实现(实际上基于红黑树(RB-tree)实现),便于查找。
通过了解容器的结构与原理,可以明确容器元素的基本要求:
1、容器元素必须支持通过拷贝构造函数赋值。
2、支持通过赋值运算符完成赋值操作。
3、能够通过析构函数完成对象的销毁。
4、序列式容器元素的默认构造函数必须可用(和创建类对象数组的要求一样)。
5、查找元素是自定义的类,需要重载 operator== 运算符。
6、关联式容器元素的key值必须给出排序规则,key值是类或结构体的话,需要重载 operator< 运算符。
6、基本数据类型(char、int、long、double)总是满足以上规则。
3、STL迭代器
每种容器类型都提供了自己定义的迭代器类型,而且每种迭代器都对运算符(operator*、operator++、operator==、operator!=、operator=)重载,提供了近似于指针的操作接口:
例如vector容器,其中的元素是连续存放的,类似于数组,完全可以通过指针实现对vector容器的遍历,所以,vector中迭代器类型定义实际上就是容器元素的类型参数T的指针。例如:定义一个存放int类型元素的vector容器对象(
vector<int>v_IntNum
),这个容器的迭代器类型其实就是int*
。相反,list容器内部元素的物理地址是不连续的,所以list容器需要单独定义一个迭代器的(模板)类,并且还要重载==,!=,*,++等运算符。
每种容器类必须提供相同的接口用于获取迭代器对象,如下所示:
begin():返回一个迭代器对象,它指向容器的第一个元素。
end(): 返回一个迭代器对象,它指向容器的最后一个元素的下一个位置。
rbegin(): 返回一个逆序迭代器对象,它指向容器的最后一个元素。
rend(): 返回一个逆序迭代器对象,它指向容器的第一个元素前面的位置。
迭代器的分类
输入迭代器:只允许读。支持的操作集:==, !=, 前缀++, 后缀++, *, ->。
输出迭代器:只允许写。支持的操作集:前缀++, 后缀++, *,只能出现在赋值运算的左操作数上。
前向迭代器:允许读和写,只能向前遍历容器。组合了输入迭代器和输出迭代器提供的所有操作,还支持对同一个元素多次读写。
双向迭代器:允许读和写,支持双向遍历容器,即支持的操作集包括:前缀++, 后缀++,前缀–,后缀–。标准要求STL容器提供的迭代器至少要达到双向迭代器的能力。所以,我们讨论的这些容器都具备双向迭代器的能力。
随机存取迭代器:允许读和写,可以任意移动(向前向后跳过任意个元素)。需要支持完整的迭代器操作集:
1)关系运算:==, !=, <, <=, >, >=;
2)算术运算:it + n, it - n, it += n, it -= n以及it1 - it2;
3)下标运算:it[n]。vector,deque,string 这些容器提供随机存取迭代器
4、算法
STL的算法是一些全局化的模板函数。STL算法通过迭代器完成容器内元素的排序、查找、拷贝和数值运算。所以,容器、迭代器、算法之间的关系可以用一句话概括:容器提供迭代器,算法使用迭代器,实现数据对象在容器中的存储和读取。
算法的内容相对比较复杂,后面单独分析。初学阶段重点分析容器和迭代器的使用。
二、容器原理与应用
1、容器对外的公共接口
因为是所有容器都具备的公共接口,所以就随便找一个容器作为例子:
vector<int>::value_type; // vector<int>容器类的元素类型,这里就是int类型
vector<int>::reference; // vector<int>容器元素的引用类型,这里就是int&类型
vector<int>::iterator; // vector<int>类的迭代器类型,以读写方式遍历容器
vector<int>::const_iterator; // 常量迭代器类型,以只读方式遍历容器
vector<int> v_num; // 创建一个vector空容器,容器中的元素类型为int型
vector<int> v_num(5); // 创建包含5个int元素的vector容器,初始值全为0
vector<int>::reference r = *(v_num.begin()); // 变量r是指向容器中第一个元素的引用
vector<int>::iterator it = v_num.begin(); // 使用迭代器类型声明一个迭代器变量it,it表示第一个元素所在的位置
vector<int> v_other(v_num.begin(), v_num.end());// 创建容器对象v_other,并用v_num中的元素初始化,两种容器类型可以不一样
vector<int> v_other(v_num); // 创建容器对象v_other,并用v_num中的元素初始化,两种容器类型必须一样。
v_other = v_num; // 将v_num的全部元素赋值给v_other,两种容器类型必须一样
v_num.size(); // 返回容器中当前拥有的元素个数
v_num.capacity(); // 返回容器在必须分配新存储空间之前可以存储的元素总数
v_num.empty(); // 返回 bool 型,判断容器是否为空。
v_num.max_size(); // 返回可容纳元素的最大数量
v_num.swap(v_other); // 交换两个类型完全一样的容器中的内容
v_num.begin(); // 返回一个迭代器,指向容器中第一个元素
v_num.end(); // 返回一个迭代器,指向容器最后一个元素的下一个位置
v_num.insert(it, 5); // 在it位置之前插入数据5,并返回新元素位置
v_num[0] = 5; // []运算符只能是访问容器中已有的元素;list不支持数组表示法和随机访问
c.erase(it_begin, it_end);// 移除 [begin,end) 区间内的所有元素(不包括end),返回下一元素的位置
v_num.clear(); // 将容器中的所有元素清空
v_num.~vector<int>(); // 销毁所有元素,相当于是一个空容器
两个容器对象之间可以使用“==、!=、>、>=、<、<=”运算符进行比较,要求:
1. 参与比较的两个容器的类型必须相同
2. 如果两个容器内的所有元素都相等,则认为两个容器相等
如果将自定义的复杂类对象通过容器存储,容器中将保存此对象的副本,这会引起大量拷贝构造的动作,一个比较好的方案是将对象的地址保存到容器中,容器中的元素是自定义类的指针。
vector<CMyclass*> v_CMyclass;
2、关联式容器的特殊之处
关联式容器中的元素是由<key,value>
组成的数对(对于set或multiset容器,key和value合二为一)。在关联式容器中,按照key值的大小顺序保存value值(默认情况是由小到大),通过这种key值的有序数列,可以在容器中实现元素的快速查找。
前面以vector容器为例,介绍的容器类公共接口,可以满足普通开发工作中对序列式容器(vector, deque, list)使用要求。这里,以map容器为例,了解关联式容器的应用规则。
map/multimap/set/multiset这些关联式容器在内部通过平衡二叉树进行元素的内存管理。
- 在map和set容器中,元素的key值不能重复,key值重复的元素在容器中将相互覆盖。
- mutimap和multiset容器,允许元素的key重复。
- 关联容器中元素的key不能直接修改,只能先删除旧的
<key, value>
,再插入一个新的<key, value>
。
代码样例
// map和multimap的原型声明
namespace std
{
template <class Key, class T,
class Compare = less<Key>,
class allocator = allocator< pair<const Key, T> >
class map;
template <class Key, class T,
class Compare = less<Key>,
class allocator = allocator< pair<const Key, T> >
class multimap;
}
// .h文件,定义key和value的类型
struct MAP_KEY
{
unsigned int m_a;
unsigned int m_b;
MAP_KEY(unsigned int a, unsigned int b):m_a(a),m_b(b){}
// 重载小于运算符,提供key值的排序规则,这里的const关键字一个都不能少
bool operator<(const MAP_KEY& rmap_key) const
{
if (this->m_a < rmap_key.m_a)
{
return true;
}
else if ((this->m_a == rmap_key.m_a)
&&(this->m_b < rmap_key.m_b))
{
return true;
}
else
{
return false;
}
}
};
struct MAP_VALUE
{
unsigned int m_a;
unsigned int m_b;
MAP_VALUE(unsigned int a, unsigned int b):m_a(a),m_b(b){}
};
// cpp文件,创建、初始化、遍历map容器
map<MAP_KEY, MAP_VALUE*> mapTest;
map<MAP_KEY, MAP_VALUE*>::iterator it_mapTest;
for(int i = 0; i < 10; i++)
{
MAP_KEY stmapKey(i, i);
MAP_VALUE* pmapValue = new MAP_VALUE(1000+i,1000+i);
mapTest[stmapKey] = pmapValue;// map支持下标运算符[],但multimap不支持
}
for (it_mapTest = mapTest.begin();
it_mapTest != mapTest.end(); ++ it_mapTest)
{
cout <<"["
<< it_mapTest->first.m_a
<< ","
<< it_mapTest->first.m_b
<< "]="
<< it_mapTest->second->m_a
<< ","
<< it_mapTest->second->m_b
<< endl;
}
// cpp文件,创建、初始化、遍历multimap容器
multimap<MAP_KEY, MAP_VALUE*> mmapTest;
multimap<MAP_KEY, MAP_VALUE*>::iterator it_mmapTest;
for(int i = 0; i < 10; i++)
{
MAP_KEY stmapKey(i, i);
for (int j = 0; j < 2; j++)
{
MAP_VALUE* pmapValue = new MAP_VALUE(1000+i*10+j,1000+i*10+j);
mmapTest.insert(make_pair(stmapKey, pmapValue)); // 向multimap容器中插入元素
}
}
for (it_mmapTest = mmapTest.begin();
it_mmapTest != mmapTest.end(); ++ it_mmapTest)
{
cout <<"["
<< it_mmapTest->first.m_a
<< ","
<< it_mmapTest->first.m_b
<< "]="
<< it_mmapTest->second->m_a
<< ","
<< it_mmapTest->second->m_b
<< endl;
}
C++中防止STL中迭代器失效
1、对于序列性容器::(vector和list和deque),erase迭代器不仅使所有指向被删元素的迭代器失效,而且使被删元素之后的所有迭代器失效,所以不能使用erase(iter++)的方式,但是erase的返回值为下一个有效的迭代器,所以正确方法为:
for (iter = cont.begin(); iter != cont.end();)
{
(*it)->doSomething();
if (shouldDelete(*iter))
iter = cont.erase(iter);
else
++iter;
}
2、关联性容器(map和set):erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器, 正确方法为:
for (iter = cont.begin(); it != cont.end();)
{
(*iter)->doSomething();
if (shouldDelete(*iter))
cont.erase(iter++);
else
++iter;
}