STL标准模板库
六大组件简介
STL提供了六大组件,彼此之间可以组合套用,这六大组件分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。
容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据,从实现角度来看,STL容器是一种class template。
算法:各种常用的算法,如sort、find、copy、for_each。从实现的角度来看,STL算法是一种function tempalte.
迭代器:扮演了容器与算法之间的胶合剂,共有五种类型,从实现角度来看,迭代器是一种将operator* , operator-> , operator++,operator–等指针相关操作予以重载的class template. 所有STL容器都附带有自己专属的迭代器,只有容器的设计者才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。
仿函数:行为类似函数,可作为算法的某种策略。从实现角度来看,仿函数是一种重载了operator()的class 或者class template
适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
空间配置器:负责空间的配置与管理。从实现角度看,配置器是一个实现了动态空间配置、空间管理、空间释放的class tempalte.
STL六大组件的交互关系,容器通过空间配置器取得数据存储空间,算法通过迭代器存储容器中的内容,仿函数可以协助算法完成不同的策略的变化,适配器可以修饰仿函数。
STL优点
STL 是 C++的一部分,因此不用额外安装什么,它被内建在你的编译器之内。
STL 的一个重要特性是将数据和操作分离。数据由容器类别加以管理,操作则由可定制的算法定义。迭代器在两者之间充当“粘合剂”,以使算法可以和容器交互运作
程序员可以不用思考 STL 具体的实现过程,只要能够熟练使用 STL 就 OK 了。这样他们就可以把精力放在程序开发的别的方面。
STL 具有高可重用性,高性能,高移植性,跨平台的优点。
高可重用性:STL 中几乎所有的代码都采用了模板类和模版函数的方式实现,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。关于模板的知识,已经给大家介绍了。
高性能:如 map 可以高效地从十万条记录里面查找出指定的记录,因为 map 是采用红黑树的变体实现的。
高移植性:如在项目 A 上用 STL 编写的模块,可以直接移植到项目 B 上。
STL常用三大组件
容器
序列式容器
序列式容器强调值的排序,序列式容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。Vector容器、Deque容器、List容器等。
关联式容器
关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。关联式容器另一个显著特点是:在值中选择一个值作为关键字key,这个关键字对值起到索引的作用,方便查找。Set/multiset容器 Map/multimap容器。
算法
质变算法
质变算法:是指运算过程中会更改区间内的元素的内容。例如拷贝,替换,删除等等。
非质变算法
非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍历、寻找极值等等中会更改。
迭代器
提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。
迭代器的种类:
输入迭代器 | 提供对数据的只读访问 | 只读,支持++、==、!= |
---|---|---|
输出迭代器 | 提供对数据的只写访问 | 只写,支持++ |
前向迭代器 | 提供读写操作,并能向前推进迭代器 | 读写,支持++、==、!= |
双向迭代器 | 提供读写操作,并能向前和向后操作 | 读写,支持++、–, |
随机访问迭代器 | 提供读写操作,并能以跳跃的方式访问容器的任意数据,是功能最强的迭代器 | 读写,支持++、–、[n]、-n、<、<=、>、>= |
案例
1 | #define _CRT_SECURE_NO_WARNINGS |
常用容器
string容器
- String和char*:
- Char*是一个指针,String是一个类。
- string封装了char*,管理这个字符串,是一个char*型的容器。
- String封装了很多实用的成员方法:
- 查找find,拷贝copy,删除delete,替换replace,插入insert。
- 不用考虑内存释放和越界:
- string管理char*所分配的内存。每一次string的复制,取值都由string类负责维护,不用担心复制越界和取值越界等。
string构造函数
1 | string();//创建一个空的字符串 例如: string str; |
string赋值操作
1 | string& operator=(const char* s);//char*类型字符串 赋值给当前的字符串 |
string获取字符操作
1 | char& operator[](int n);//通过[]方式取字符 |
string拼接操作
1 | string& operator+=(const string& str);//重载+=操作符 |
string查找和替换
1 | int find(const string& str, int pos = 0) const; //查找str第一次出现位置,从pos开始查找 |
string比较操作
1 | /* |
string子串
1 | string substr(int pos = 0, int n = npos) const;//返回由pos开始的n个字符组成的字符串 |
string插入和删除
1 | string& insert(int pos, const char* s); //插入字符串 |
string和c-type转换
1 | //string 转 char* |
提示
在c++中存在一个从const char*到string的隐式类型转换,却不存在从一个string对象到C_string的自动类型转换。对于string类型的字符串,可以通过c_str()函数返回string对象对应的C_string。
通常,程序员在整个程序中应坚持使用string类对象,直到必须将内容转化为char*时才将其转换为C_string。
注意
为了修改string字符串的内容,下标操作符[]和at都会返回字符的引用。但当字符串的内存被重新分配之后,可能发生错误。
1 | string s = "abcdefg"; |
vector容器
vector与Array数组
Array是静态空间,一旦配置了就不能改变,要换大一点或者小一点的空间得由自己来,首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间。
Vector是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素。因此vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必害怕空间不足而一开始就要求一个大块头的array了。
vector迭代器
Vector维护一个线性空间,所以不论元素的型别如何,普通指针都可以作为vector的迭代器,所以vector提供的是随机访问迭代器。
1 |
vector的数据结构
Vector所采用线性连续空间,它以两个迭代器Myfirst和Mylast分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器_Myend指向整块连续内存空间的尾端。
为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端需求大一些,以备将来可能的扩充,这便是容量的概念。换句话说,一个vector的容量永远大于或等于其大小,一旦容量等于大小,便是满载,下次再有新增元素,整个vector容器就得另觅居所。
注意
所谓动态增加大小,并不是在原空间之后续接新空间(因为无法保证原空间之后尚有可配置的空间),而是分配一块更大的内存空间,然后将原数据拷贝新空间,并释放原空间。因此,对vector的任何操作,一旦引起空间的重新配置,指向原vector的所有迭代器就都失效了。
API
vector构造函数
1 | vector<T> v; //采用模板实现类实现,默认构造函数 |
vector常用赋值操作
1 | assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。 |
vector大小操作
1 | size();//返回容器中元素的个数 |
vector数据存取操作
1 | at(int idx); //返回索引idx所指的数据,如果idx越界,抛出out_of_range异常。 |
vector插入和删除操作
1 | insert(const_iterator pos, int count,ele);//迭代器指向位置pos插入count个元素ele. |
巧用swap收缩内存空间
1 | #define _CRT_SECURE_NO_WARNINGS |
reserve预留空间
1 | #define _CRT_SECURE_NO_WARNINGS |
deque容器
deque与vector
Deque容器和vector容器最大的差异,一在于deque允许使用常数项时间对头端进行元素的插入和删除操作。二在于deque没有容量的概念,因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,换句话说,像vector那样,”旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间”这样的事情在deque身上是不会发生的。也因此,deque没有必须要提供所谓的空间保留(reserve)功能.
虽然deque容器也提供了Random Access Iterator,但是它的迭代器并不是普通的指针,其复杂度和vector不是一个量级,这当然影响各个运算的层面。因此,除非有必要,我们应该尽可能的使用vector,而不是deque。对deque进行的排序操作,为了最高效率,可将deque先完整的复制到一个vector中,对vector容器进行排序,再复制回deque.
deque容器实现原理
Deque容器是连续的空间,至少逻辑上看来如此,连续现行空间总是令我们联想到array和vector,array无法成长,vector虽可成长,却只能向尾端成长,而且其成长其实是一个假象,事实上(1) 申请更大空间 (2)原数据复制新空间 (3)释放原空间 三步骤,如果不是vector每次配置新的空间时都留有余裕,其成长假象所带来的代价是非常昂贵的。
Deque是由一段一段的定量的连续空间构成。一旦有必要在deque前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在deque的头端或者尾端。Deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
既然deque是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。Deque代码的实现远比vector或list都多得多。
Deque采取一块所谓的map(注意,不是STL的map容器)作为主控,这里所谓的map是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是deque的存储空间的主体。
API
deque构造函数
1 | deque<T> deqT;//默认构造形式 |
deque赋值操作
1 | assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。 |
deque大小操作
1 | deque.size();//返回容器中元素的个数 |
deque双端插入和删除操作
1 | push_back(elem);//在容器尾部添加一个数据 |
deque数据存取
1 | at(idx);//返回索引idx所指的数据,如果idx越界,抛出out_of_range。 |
deque插入操作
1 | insert(pos,elem);//在pos位置插入一个elem元素的拷贝,返回新数据的位置。 |
deque删除操作
1 | clear();//移除容器的所有数据 |
stack容器
stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取stack的其他元素。换言之,stack不允许有遍历行为。
有元素推入栈的操作称为:push,将元素推出stack的操作称为pop.
stack迭代器
Stack所有元素的进出都必须符合”先进后出”的条件,只有stack顶端的元素,才有机会被外界取用。Stack不提供遍历功能,也不提供迭代器。
stack常用API
stack构造函数
1 | stack<T> stkT;//stack采用模板类实现, stack对象的默认构造形式: |
stack赋值操作
1 | stack& operator=(const stack &stk);//重载等号操作符 |
stack数据存取操作
1 | push(elem);//向栈顶添加元素 |
stack大小操作
1 | empty();//判断堆栈是否为空 |
queue容器
Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。
queue迭代器
Queue所有元素的进出都必须符合”先进先出”的条件,只有queue的顶端元素,才有机会被外界取用。Queue不提供遍历功能,也不提供迭代器。
queue常用API
queue构造函数
1 | queue<T> queT;//queue采用模板类实现,queue对象的默认构造形式: |
queue存取、插入和删除操作
1 | push(elem);//往队尾添加元素 |
queue赋值操作
1 | queue& operator=(const queue &que);//重载等号操作符 |
queue大小操作
1 | empty();//判断队列是否为空 |
list容器
List容器是一个双向链表。
相较于vector的连续线性空间,list就显得负责许多,它的好处是每次插入或者删除一个元素,就是配置或者释放一个元素的空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素的移除,list永远是常数时间。
- 采用动态存储分配,不会造成内存浪费和溢出。
- 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素。
- 链表灵活,但是空间和时间额外耗费较大。
list迭代器
由于list是一个双向链表,迭代器必须能够具备前移、后移的能力,所以list容器提供的是Bidirectional Iterators.
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效。这在vector是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效,甚至List元素的删除,也只有被删除的那个元素的迭代器失效,其他迭代器不受任何影响。
list容器的数据结构
list容器不仅是一个双向链表,而且还是一个循环的双向链表。
list常用API
list构造函数
1 | list<T> lstT;//list采用采用模板类实现,对象的默认构造形式: |
list数据元素插入和删除操作
1 | push_back(elem);//在容器尾部加入一个元素 |
list大小操作
1 | size();//返回容器中元素的个数 |
list赋值操作
1 | assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。 |
list数据的存取
1 | front();//返回第一个元素。 |
list反转排序
1 | reverse();//反转链表,比如lst包含1,3,5元素,运行此方法后,lst就包含5,3,1元素。 |
set/multiset容器
set容器
Set的特性是所有元素都会根据元素的键值自动被排序。Set的元素不像map那样可以同时拥有实值和键值,set的元素即是键值又是实值。Set不允许两个元素有相同的键值。
我们可以通过set的迭代器改变set元素的值吗?不行,因为set元素值就是其键值,关系到set元素的排序规则。如果任意改变set元素值,会严重破坏set组织。换句话说,set的iterator是一种const_iterator.
set拥有和list某些相同的性质,当对容器中的元素进行插入操作或者删除操作的时候,操作之前所有的迭代器,在操作完成之后依然有效,被删除的那个元素的迭代器必然是一个例外。
multiset容器
multiset特性及用法和set完全相同,唯一的差别在于它允许键值重复。
set/multiset数据结构
set和multiset的底层实现是红黑树,红黑树为平衡二叉树的一种。
set常用API
set构造函数
1 | set<T> st;//set默认构造函数: |
set赋值操作
1 | set& operator=(const set &st);//重载等号操作符 |
set大小操作
1 | size();//返回容器中元素的数目 |
set插入和删除操作
1 | insert(elem);//在容器中插入元素。 |
set查找操作
1 | find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end(); |
set的返回值 指定set排序规则:
1 | //插入操作返回值 |
对组(pair)
对组(pair)将一对值组合成一个值,这一对值可以具有不同的数据类型,两个值可以分别用pair的两个公有属性first和second访问。
类模板:template \
如何创建对组?
1 | //第一种方法创建一个对组 |
map/multimap容器
map/multimap基本概念
Map的特性是,所有元素都会根据元素的键值自动排序。Map所有的元素都是pair,同时拥有实值和键值,pair的第一元素被视为键值,第二元素被视为实值,map不允许两个元素有相同的键值。
我们可以通过map的迭代器改变map的键值吗?答案是不行,因为map的键值关系到map元素的排列规则,任意改变map键值将会严重破坏map组织。如果想要修改元素的实值,那么是可以的。
Map和list拥有相同的某些性质,当对它的容器元素进行新增操作或者删除操作时,操作之前的所有迭代器,在操作完成之后依然有效,当然被删除的那个元素的迭代器必然是个例外。
Multimap和map的操作类似,唯一区别multimap键值可重复。
Map和multimap都是以红黑树为底层实现机制。
map/multimap常用API
map构造函数
1 | map<T1, T2> mapTT;//map默认构造函数: |
map赋值操作
1 | map& operator=(const map &mp);//重载等号操作符 |
map大小操作
1 | size();//返回容器中元素的数目 |
map插入数据元素操作
1 | map.insert(...); //往容器插入元素,返回pair<iterator,bool> |
map删除操作
1 | clear();//删除所有元素 |
map查找操作
1 | find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;/若不存在,返回map.end(); |
STL容器使用时机
vector | deque | list | set | multiset | map | multimap | |
---|---|---|---|---|---|---|---|
典型内存结构 | 单端数组 | 双端数组 | 双向链表 | 二叉树 | 二叉树 | 二叉树 | 二叉树 |
可随机存取 | 是 | 是 | 否 | 否 | 否 | 对key而言:不是 | 否 |
元素搜寻速度 | 慢 | 慢 | 非常慢 | 快 | 快 | 对key而言:快 | 对key而言:快 |
元素安插移除 | 尾端 | 头尾两端 | 任何位置 | - | - | - | - |
- vector的使用场景:比如软件历史操作记录的存储,我们经常要查看历史记录,比如上一次的记录,上上次的记录,但却不会去删除记录,因为记录是事实的描述。
- deque的使用场景:比如排队购票系统,对排队者的存储可以采用deque,支持头端的快速移除,尾端的快速添加。如果采用vector,则头端移除时,会移动大量的数据,速度慢。
- vector与deque的比较:
- 一:vector.at()比deque.at()效率高,比如vector.at(0)是固定的,deque的开始位置 却是不固定的。
- 二:如果有大量释放操作的话,vector花的时间更少,这跟二者的内部实现有关。
- 三:deque支持头部的快速插入与快速移除,这是deque的优点。
- list的使用场景:比如公交车乘客的存储,随时可能有乘客下车,支持频繁的不确实位置元素的移除插入。
- set的使用场景:比如对手机游戏的个人得分记录的存储,存储要求从高分到低分的顺序排列。
- map的使用场景:比如按ID号存储十万个用户,想要快速要通过ID查找对应的用户。二叉树的查找效率,这时就体现出来了。如果是vector容器,最坏的情况下可能要遍历完整个容器才能找到该用户。
常用算法
函数对象
重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对象,也叫仿函数(functor),其实就是重载“()”操作符,使得类对象可以像函数那样调用。
注意:
1.函数对象(仿函数)是一个类,不是一个函数。
2.函数对象(仿函数)重载了”() ”操作符使得它可以像函数一样调用。
分类:假定某个类有一个重载的operator(),而且重载的operator()要求获取一个参数,我们就将这个类称为“一元仿函数”(unary functor);相反,如果重载的operator()要求获取两个参数,就将这个类称为“二元仿函数”(binary functor)。
函数对象的作用主要是什么?STL提供的算法往往都有两个版本,其中一个版本表现出最常用的某种运算,另一版本则允许用户通过template参数的形式来指定所要采取的策略。
1 | //函数对象是重载了函数调用符号的类 |
总结:
1、函数对象通常不定义构造函数和析构函数,所以在构造和析构时不会发生任何问题,避免了函数调用的运行时问题。
2、函数对象超出普通函数的概念,函数对象可以有自己的状态
3、函数对象可内联编译,性能好。用函数指针几乎不可能
4、模版函数对象使函数对象具有通用性,这也是它的优势之一
谓词
谓词是指普通函数或重载的operator()返回值是bool类型的函数对象(仿函数)。如果operator接受一个参数,那么叫做一元谓词,如果接受两个参数,那么叫做二元谓词,谓词可作为一个判断式。
1 | class GreaterThenFive |
内建函数对象
STL内建了一些函数对象。分为:算数类函数对象,关系运算类函数对象,逻辑运算类仿函数。这些仿函数所产生的对象,用法和一般函数完全相同,当然我们还可以产生无名的临时对象来履行函数功能。使用内建函数对象,需要引入头文件 #include\
6个算数类函数对象,除了negate是一元运算,其他都是二元运算。
1
2
3
4
5
6template<class T> T plus<T>//加法仿函数
template<class T> T minus<T>//减法仿函数
template<class T> T multiplies<T>//乘法仿函数
template<class T> T divides<T>//除法仿函数
template<class T> T modulus<T>//取模仿函数
template<class T> T negate<T>//取反仿函数6个关系运算类函数对象,每一种都是二元运算。
1
2
3
4
5
6template<class T> bool equal_to<T>//等于
template<class T> bool not_equal_to<T>//不等于
template<class T> bool greater<T>//大于
template<class T> bool greater_equal<T>//大于等于
template<class T> bool less<T>//小于
template<class T> bool less_equal<T>//小于等于逻辑运算类运算函数,not为一元运算,其余为二元运算。
1
2
3template<class T> bool logical_and<T>//逻辑与
template<class T> bool logical_or<T>//逻辑或
template<class T> bool logical_not<T>//逻辑非
内建函数对象举例:
1 | //取反仿函数 |
函数对象适配器
1 | //函数适配器bind1st bind2nd |
算法概述
算法主要是由头文件\
\
\
\
常用遍历算法
1 | /* |
for_each:
1 | /* |
transform:
1 | //transform 将一个容器中的值搬运到另一个容器中 |
常用查找算法
1 | /* |
常用排序算法
1 | /* |
常用拷贝和替换算法
1 | /* |
常用算数生成算法
1 | /* |
常用集合算法
1 | /* |