一个容器是特定类型对象的几个,在C++标准库中包含了大部分常见的容器。STL 是“Standard Template Library”的缩写,中文译为“标准模板库”。STL 是 C++ 标准库的一部分,不用单独安装。TSL核心包括3个组件。容器(containers),算法(algorithms),迭代器(iterators)。除此外还有仿函数,内存配置器和配接器。
按照容器的存储结构可以分为顺序容器与关联容器两类。元素在顺序容器的顺序与加入容器时的位置相对应。关联容器的元素位置由元素相关联的字值决定。
顺序容器一般都具有快速顺序访问元素的能力。在以下两点中有性能折中:
标准库顺序容器
补充
deque的随机读取实现方法为通过分段的连续内存加map实现。其中map将各段内存地址串起来,表现为一块连续的大内存,以此实现随机访问。
基本原则:
几乎最全能的容器,deque。一般而言,vector和list足以使用了。
容器container中包含了一些类型,通过域运算符进行定义。container::member_name
由于大多数时候并不需要关心如容器的迭代器具体是什么类型,所以配合auto使用。代码可读性和泛化性更好。
迭代器是最主要使用的容器中的类型。支持以下操作
通过begin和end可以取得容器的首元素与尾后迭代器。带r版本返回反向迭代器,带c版本返回const迭代器。
反向迭代器对于各种操作相对正向迭代器相反,例如++操作对于反向迭代器会得到上一个迭代器。
不以c开头的函数是被重载过的,实际上有2个名为begin的成员。cbegin()是C++11新标准,显式指定类型为const接受begin()函数的返回值也可以取到只读迭代器如it5。
list c = {"hello","world","!"};
auto it1 = c.begin();//list::iterator
auto it2 = c.rbegin();//list::reverse_iterator
auto it3 = c.cbegin();//list::const_iterator
auto it4 = c.crbegin();//list::const_reverse_iterator;
list::const_iterator it5 = c.begin();//显式指定数据类型
一般而言除array容器比较特殊外,几乎所有操作对于其他的顺序容器都是通用的。因为array容器需要在创建时初始化容器大小。
除array容器外,其他容器的默认构造函数都会创建一个指定类型的空容器
定义与初始化:
对应于初始化的3种常见方式
C c; //默认构造函数,如果C是array则c中元素按照默认方式初始化,否则c为空。
C c1(c2); //c1初始化为c2的拷贝。c1、c2必须是相同容器类型,且保存相同元素类型(对于array而
C c1 = c2; //言,还要有相同大小)
C c{a,b,c..}; //c初始化为初始化列表中元素的拷贝。列表元素类型必须和c保存元素类型相同。
C C = {a,b,c}; //对于array而言,列表元素数目必须小于等于array大小,遗漏的元素将进行值初始化。
C c(b,e); //c初始化为得带器be指定范围中的元素的拷贝。array不适用
只有顺序容器(除array),构造函数可以接受大小参数
C seq(n); //容器seq包含n个元素,对这些元素进行了值初始化。这个构造函数是explicit的;(string不适用)
C seq(n,t); //包含n个初始化值为t的元素
在创建一个容器为另一个容器拷贝的时候,容器类型和储存元素类型都必须相同。当使用迭代器表示拷贝时,储存元素类型可转化即可,但容器类型必须相同。
array的初始化,必须指定大小。注意,array支持容器大小相同,储存元素类型相同的2个array容器进行拷贝和赋值。但是内置数组不支持拷贝或者赋值操作。
array //类型为保存42个int的数组
注意size_type也需要指定大小
array::size_type i; //正确
array::size_type j; //错误
int digs[10] = {1,2,3,4,5,6,7,8,9,10};
int cpy[10] = digs; //错误,内置数组不支持拷贝
array数组支持拷贝初始化。
赋值运算将左边容器中的元素替换成邮编容器中元素的拷贝。
c1 = c2 //c1,c2必须具有相同类型
c = {a,b,c,....} //c1替换成初始列表的拷贝,**array不适用**
swap(c1,c2) //交换c1和c2的元素,c1,c2必须有相同的类型,
c1.swap(c2) //swap一般比从c2向c1拷贝要快
assign操作不适用于关联容器和array
seq.assign(b,e) //将seq中的元素替换迭代器b和e所表示的范围中的元素。
//迭代器b和e不能指向seq中的元素
seq.assign(ilst) //将seq中的元素替换为初始化列表ilst中元素
seq,assign(n,t) //将seq替换为n个值为t的元素
assgin的意义在于语序从一个不同但相容的类型往原容器赋值。例如可以将vector中的char*赋值给list中的string。由于旧元素被替换,所以传递给assign 的迭代器不能指向调用assign的容器。
list name;
vector oldstyle;
names = oldstyle; //错误类型不匹配!
names.assign(oldstyle.cbegin(),oldstyle.cend()); //可以将const char*转化为string
swap操作交换两个类型相同的容器。注意,元素本身未被交换。交换容器内的操作保证会很快,只是交换了容器内部数据结构(比如指针)。除array外,swap不会对元素进行拷贝删除插入的操作。对于array的swap操作会交换他们的元素,开销与array元素数目成正比。
假设iter指向svec1[3]的sting。那么swap(svec1, svec2)后,iter会指向svec2[3]的迭代器。(其中svec1和svec2类型为list
一般使用非成员版本的swap操作,便于兼容旧版本更好的泛化。
容器一定支持相等运算符操作,所有容器都支持==与!=操作。当内部元素拥有比较运算符,则可以使用关系运算符来比较容器。否则不支持。
判定标准:比较过程其实就是内部元素的逐对比较。
除array外,标准库容器都可以再运行时动态添加删除容器大小。array不支持这些操作。
注意事项:
c.push_back(t); //在c的尾部创建一个值为t或者由args创建的元素
c.emplace_back(args); //返回void
c.push_front(t); //在c的头部创建元素
c,emplace_front(args); //返回void
c.insert(p,t); //在迭代器p指向的元素!!之前!!创建一个值为t或者由args创建的元素
c.emplace(p,args); //返回指向新添加元素的迭代器
c.insert(p,n,t); //在迭代器p之前插入n个值为t的元素
//返回指向第一个新添加元素的迭代器,n为0返回p
c.insert(p,b,e); //在迭代器p之前插入迭代器b和e指定的范围内的元素(左闭右开)。b和e不能指向c
//返回指向新添加的第一个元素,若范围为空返回p
c.insert(p,ilst); //!! ilst是一个花括号包围的元素列表!! 将这些值插入到迭代器p之前。
//返回插得第一个元素,若为空返回p
push和emplace的区别
push接受的是容器的储存元素的数据类型是一个对象。emplace接收的是容器储存元素的的参数。严格而言,emplace方法更省空间。push 方法会先将接收元素对象进行拷贝创建局部临时变量,再放入容器本身,而不是直接使用对象放入。而emplace方法会将参数传给元素类型的构造函数初始化为对象随后放入容器。注意,传递给emplace函数的参数必须与元素类型的构造函数相匹配
insert
注意该方法添加的元素是在迭代器之前,主要是因为迭代器可能指向尾后元素,这是一个不存在元素的位置。部分容器不支持push_front操作,但是对于insert无限制,可以通过指向首元素的迭代器把元素插入容器开始位置。注意,但此操作可能耗时较长。
通过插入1.n个相同的元素,2.两个迭代器指示范围,3.元素列表。可以实现一次性插入多个元素。
注意事项:
c.back(); //返回c尾元素的引用,若c为空函数行为未定义
c.front(); //返回c首元素的引用,若空未定义
c[n]; //返回下标为n的元素的引用,n是一个无符号整型,若n>=c.size(),行为未定义
c.at(n); //返回下标为n的元素的引用,若下标越界抛出out_of_range异常
back()front()和begin()end()的区别
begin和end返回的是迭代器,需要解引用符号才可以获取元素值,front和back返回的直接是元素对象的引用。注意end返回的是尾后元素,需要*(–iter)才能正确获得尾元素的引用。
注意事项:
c.pop_back(); //删除c的尾元素。返回void。若c为空函数未定义
c.pop_front(); //删除c的首元素。返回void。若空未定义
c.erase(p); //删除迭代器p所指向元素,返回指向被删元素之后的迭代器。
//若p是尾元素,返回尾后元素。 若p是尾后,未定义
c.erase(b,e); //删除迭代器b和e所指定范围捏的元素,返回一个指向最后一个被删除元素之后的迭代器
//若e是尾后迭代器,也返回尾后迭代器.(计数范围使用左闭右开原则)
c.clear(); //删除c所有元素,返回void
注意erase(b,e)依旧是左闭右开原则,e应当指向希望删除的最后一个元素之后的位置,以下两者是等效操作,
c.erase(c.begin(),c.end())
c.clear()
单链表容器forward_list有着特殊的添加删除操作。因为单链表的数据结构从插入行为而言需要访问前驱,但是单链表的结构导致迭代器无法知道其前驱节点。故未定义类似其他顺序容器的添加删除函数,定义了insert_afer,rmplace_after,erase_after的函数,插入删除操作也是针对给定迭代器的下一个元素元素进行。类似尾后元素定义了before_begin函数取得首前迭代器,类似哑头结点。
lst.before_begin() //返回指向链表首元素钱不存在的元素的迭代器,此迭代器不能解引用
lst.cbefore_begin() //c前缀返回一个const_iterator
lst.insert_after(p,t) //在迭代器p后插入元素,t是一个元素对象
lst.insert_after(p,n,t) //n代表数量
lst.insert_after(p,b,e) //b和e代表范围内的一对迭代器,不能指向lst。范围为空返回p
lst.inster_after(p,ilst) //ilst是一个花括号列表
//返回值指向最后一个插入元素的迭代器
//若p为尾后迭代器,则行为未定义。
emplace_after(p,args) //使用args在迭代器p后传建一个元素,返回一个指向这个元素的指针
//若p为尾后,行为未定义
lst.erase_after(p) //删除迭代器!!p之后!!的位置指向的元素
lst.erase_after(b,e) //删除从b到e之间的元素(左闭右开不包括e)
//返回指向最后一个被删除元素之后的迭代器
// 若不存在这样的元素返回尾后迭代器,p为尾后行为未定义
可以使用resize增大或缩小容器,array不支持resize。若当前大小>新大小,容器后部元素被删除。若当前大小<新大小,会将新元素添加到容器后部。
c.resize(n) //调整c的大小为n。若c.size()>n,多出来的元素被删除。
c.resize(n,t) //若c.size()
对容器进行添加和删除元素操作均可能导致指向容器元素的指针,引用或者迭代器失效。使用失效的指针引用与迭代器是严重程序设计错误。
添加元素后:
删除元素后,指向被删除元素的肯定无效,其余:
故必须保证每次改变容器操作后正确重新定位迭代器。对于vector,string,deque需要更加注意迭代器是否有效。
常见场景:
主要函数:
除上述顺序容器外,标准库定义了3个顺序容器适配器: stack/queue/priority_queue。适配器(adaptor)是标准库通用概念,容器迭代器函数都有适配器。本质适配器是一种机制,使对象行为看起来符合某种事物要求。例如stack适配器接受一种顺序容器(除了array和forward_list),并使其操作看起来像stack。
所有适配器都支持的操作。
默认情况下,stack和queue基于deque实现。priority_queue基于vector实现。可以再创建时指定底层容器类型。
//默认创建
stack stk(deq); //从deq拷贝元素到stk
//指定创建
stack> str_stk; //指定了一个在vector上实现的空栈
stack> str_stk2(svec); //用svec去初始化str_stk2
底层容器限定
注意不能在适配器中调用底层容器的操作,例如在栈中调用push_back。即使栈基于deque实现。
s.pop();
s.push(item);
s.emplace(args);
s.top()
q.pop();
q.front(); //最早入队元素
q.back(); //仅适用队列,最晚入队元素
q.push(item);
q.emplace(args);
完整创建方式:
其中参数2指定底层容器,默认vector。参数3指定优先级。新加入元素会排在所有优先级比他低的已有元素之前。标准库默认使用<运算符确定优先级。例如1<2,故1优先级高于2,2放在前面,所以默认是降序队列,可以使用重载指定优先级运算。
//升序队列
priority_queue ,greater > pq;
//降序队列
priority_queue ,less > pq;
q.pop(); //删除最高优先级元素
q.front(); //首元素
q.top(); //仅适用优先级队列,返回优先级最高的元素
q.push(item); //在恰当位置创建元素
q.emplace(args);
关联容器支持高效关键字查找和访问。主要的关联容器(associative-container)类型是map(映射类型)和set(集合类型)。map中的元素是关键字-值(key-value)对:关键字起索引作用,值则表示与索引相关联的数据。set中每个元素质保函一个关键字;set支持高效的关键字查询操作----检查一个给定关键字是否在set中。常用于例如文本处理中需要忽略的单词。
标准库提供8种关联容器,主要在以下3点有所选择。允许重复关键字容器名字包含multi,不保持关键字按顺序存储的容器名字以unordered开头。无需容器使用哈希函数组织元素。
标准库关联容器:
按关键字有序保存
按关键字无序保存
map容器又被称为关联数组(associative array),与正常数组类似,区别在于下标不必是整数。常见应用场景如需要对应的字典。
set容器是关键字的简单集合,应用于当需要知道一个值是否存在。 常见场景如判断是否是新用户。
在关键字没有明显序关系,选用无序容器。或者维护元素序代价非常高,选用无序容器。理论上哈希技术能获得更好的平均性能。
关联容器不支持顺序容器与位置相关的操作,例如push_back,因为关联容器元素依据关键字存储,位置没有意义。
关联容器不支持构造函数或插入操作这类接受一个元素值和一个数量值的操作。不支持(n,t)这类构造函数,(b,e)这类构造函数仍支持。
关联容器的迭代器都是双向的
关联容器的迭代器都是双向的。
map的迭代器得到的是容器的value_type的值得引用(实际上类似pair),first保存的是const关键字,second成员保存值。
set的迭代器得到的是容器的value_type的值得引用,迭代器都是只读的,不能修改只能访问。
可以使用first和second访问成员。map的元素类型就是pair。first保存的是const关键字,second成员保存值。
**注意区分迭代器和pair对象的引用,前者使用->解引用,后者使用. 运算符 **
关联容器与顺序容器初始化方式类似,联容器的默认构造函数仅创建一个指定类型的空容器,可以使用同类型容器的拷贝初始化关联容器。map容器需要指明关键字类型和值类型,set容器只需指明关键字类型
关联容器初始化的3种常见方式
map word_count; //空容器
//列表初始化
map salary = {
{"john", 100},
{"bob", 500},
{"austen", 1000}};
set exclude = {"the","but","and","or"};
//构造函数初始化
vector ivec = {1,1,2,2,3,3};
set iset(ivec.begin(),ivec.end()); //仅包含来自ivec不重复的元素,大小为3
multiset miset(ivec.begin(),ivec.end()); //包含所有元素即使重复,大小为6
列表初始化器的内容必须能转换为容器中的元素类型。初始化map时需要提供关键字-值对,使用花括号包围。
对于有序容器,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<运算符比较关键字。可以自定义比较操作,但是必须定义严格弱序,类似于小于等于。
对于自建类,需要重载<运算符或者显式指定比较运算符。如下例,提供了孤雁尖子类型和比较操作类型----这应该是函数指针。使用decltype获得函数指针类型,必须加上*支出要使用一个给定函数类型的指针。
class Sales_data{
string no;
public:
string getno();
}
Sales_data::getno(){
return this.no;
}
//比较函数
bool compareNO(const Sales_data &p1,const Sales_data &p2){
return p1.getno() Salesstore(compareNO);
对于无序容器
由于需要hash计算,所以不能直接定义关键字类型为自定义类类型的无序容器,不能直接使用hash模板,需要自己创建hash模板。元素的哈希值类型为hash
size_t hasher(const Sales_data &sd){
return hash() (sd.getno());
}
bool eqOp(const Sales_data &p1,const Sales_data &p2){
return p1.getno()==p2.getno();
}
using SD_multiset = unordered_multiset;
SD_multiset Salesstore2(42,hasher,eqOp);//参数是 桶大小、哈希函数指针、相等性判断运算符指针。
//如果定义了==则可以值重载哈希函数
对于单个插入map/set返回一个pair。first是迭代器指向具有给定关键字的元素,second成员是bool,指出插入成功(true)或已存在(false)。对于multimap/multiset因不存在插入失败仅返回迭代器。多个插入返回void。
erase
map可用下标访问。如果使用一个不在容器中的关键字作为下标,会添加一个具有此关键字的元素到map中。下标和at操作只适用于非const的map和unorder_map。
为了防止不必要的添加可以使用find操作。
其中
find(k) 返回指向k的迭代器,若不存在返回.end()
count(k) 返回int
lower_bound(k) 不小于(可能是本身的第一个) 无序容器不适用
upper_bound(k) 大于 无序容器不适用
equal_range(k) 返回一个迭代器pair,指示关键字等于k的元素范围(左闭右开)。若不存在两者均为c.end()或指向关键字可插入的位置。
无序容器在存储上本质组织为一组桶,每个桶保存零个或多个元素。通过哈希函数将元素映射到桶。为了访问元素,容器先计算哈希值,指出搜索桶。搜索桶将具有相同哈希值的所有元素保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也会在同一个桶中。所以无序容器性能依赖于哈希函数质量和桶的数量和大小。一般而言计算元素哈希值和在桶中搜索都很快,除非一个桶保存了很多元素,那么查找特定元素需要大量比较操作。
容器会在需要时添加新的桶,以使得load_factor()<=max_load_factor重组存储。
桶管理的相关函数。
//桶接口
c.bucket_count(); //正在使用的桶数目
c.max_bucket_count(); //容器能容纳的最多的桶的数量
c.bucket_size(n); //第n个桶有多少元素
c.bucket(k); //关键字k的元素在哪个桶
//桶迭代
local_iterator //用来访问桶中元素的迭代器类型(equal_range()的返回值类型)
const_local_iterator //const版本
c.begin(n),c.end(n) //桶n的首元素迭代器和尾后迭代器
c.cbegin(n),c.cend(n) //const
//哈希策略
h = c.hash_function(); //返回c的哈希函数
eq = c.key_eq(); //eq是c的相等检测函数
c.load_factor(); //装载因子。元素除以桶数,double(c.size())/c.bucket_count()每个桶平均元素数量,float
c.max_load_factor(); //最大装载因子。c试图维护的平均桶大小,返回float。
//以下函数代价可能会触发重新hash所有元素,代价可能非常高,最坏情况O(N^2)
c.max_load_factor(d); //输入参数float d,将最大装载因子设定为d,若装载因子已接近最大,c将改变哈希表大小
c.rehash(n); //重组储存,使得bucket_count>=n且bucket_count>size/max_load_factor
c.reserve(n); //重组储存,使得c可以保存n个元素并且不用rehash.c.rehash(ceil(n/c.max_load_factor()))
无序关联容器装载因子定义为已用空间的比例,也是size()/capacity()。
加载因子指的是hash表元素填满的程度,越满空间利用率越高,但是冲突的几率增加了。越小填满的元素越少,但是空间浪费多了
目前,STL 中已经提供的容器主要如下:
STL 提供了非常多的数据结构算法。这些算法在命名空间 std 的范围内定义,通过包含头文件 来获得使用权。
常见的部分算法如下: