STL容器类介绍
容器,顾名思义就是盛放东西的东西,如盛饭的饭碗就是一个容器,这样的容器c++库也为我们提供了,只是不能用来盛饭。在程序中只有数据和数据处理方法,数据和方法结合在一起就形成了类或对象,在面向对象的语言中流行一切皆是对象,要有对象首先肯定要有类,用来存放对象的类就叫容器类,简称容器。
计算机的运行时数据只能存放在内存中,而容器类就是用做容器的内存管理方法。容器的内涵就是数据结构(数据打包的方法)+算法(数据处理方法)。
容器其实我们早就接触到了,c中的数组、结构体就是个容器,只是c中的这个容器内部只有数据,没有方法。
c++容器,通过类库方式提供,容器的类库被模板技术泛化后就是STL(stande tempelate libriy)容器库。学STL的核心就是学类库的容器类,跟我们学习使用c中的stdio库时一个道理。
STL容器分类
STL容器比较多,大致可以分为序列容器、排序容器、哈希容器三类。
序列容器
元素在容器中的位置与值无关,即容器不是排序的,类似于数组存放的数据是无序的。包括array、vector、list、forward_list。
排序容器
排序容器就是插入数据的时候,自动按照从小到大排列好,这种排序肯定需要开销的,好处就是我们要使用的时候不用再去排序,他已经自动排好了,我们直接用即可。包括set、multiset、map、mutilmap等。
哈希容器
哈希是hash英文的发音,哈希容器中的元素是未排序的,元素位置由哈希函数确定,遵守一定规则的<key,value>对式存储,key代表参数类型,value代表具体的值,如描述一个长方形:<长,25>,<宽,30>,包括unordered_set、 unordered_map、 hash_set hash_multiset、hash_multimap。
容器类如何学
STL的核心就是容器类,STL的其它技术都是围绕容器类展开。STL的本质就是一套模板技术泛化类型的c++基本数据结构和算法的类库。学习STL就是学会每种容器的使用,根据需求知道那种容器类比较合适。
array容器类
array容器其实就是c++使用template对c语言的数组进行的二次封装。具有数组的定长、同类型元素、在内存中连续排布的特点。
头文件:<array>
定义并初始化
template<class T,std::size_t N>struct array //模板结构体
array<int, 2> a, b; //定义但不初始化,可定义1个或多个
array<int, 3> a1{1,2,3}; //聚合初始化,错误示例array<int, 2> a(1,2)
array<int, 4> a2={1,2,3,4}; //调用带参构造函数聚合初始化
array<int, 4> a3=a2; //调用拷贝构造函数
元素访问
operator[ ]方式: a[0]=1或者int b=a[0]; //[ ]运算符重载
调用at函数: a.at(2)=4 或 b= a.at(2); //调用类中的方法at
front函数: int b = a.front或a.front=5; //读写数组的第一个元素
back函数: int b = a.back; // 读写数组的最后一个元素
date函数: int *p=a.date; //获取存放数据的首地址
容量
empty函数: if(a.empty()==true); //检查是否为空,空1,非空0
size函数: int b=a.size(); //返回容纳元素数量
max_size函数: int b=a. max_size(); //返回最大可容纳元素的数量
操作
fill函数: a.fill(6); //全部填充指定值6
swap函数: a.swap(b); //a容器与b容器内的array数据交换
非成员函数
operator== if(a=b); //判断两个array容器内的值是否相等,是turn,否false
get函数:
template< size_t I, class T, size_t N > T& get(array<T,N>& a ) noexcept; //原型
int b = get<1>(a) 或 int b = get<1,int,5>(a); //获取a容器第1个元素
get<1,int,5>(a)=33 或 get<1>(a)=33; //向a容器第1个元素写入33
swap函数:
template<class T, size_t N>
void swap( array<T,N>& lhs,array<T,N>& rhs ); //两行原型
swp<int,5>(a,b);或swp (a,b) //实际上可以通过对a和b的反推
辅助类
tuple_size
作用:在编译阶段获取到传入的数组的大小,主要应用于写类库的人不知道将来用户会传入多长的数组,但是在其内部又需要为其申请空间,用该语句占位拖延,待编译到调用本类时再根据传入的数组长度来确定大小。
原型:template< typename T, size_t N >
struct tuple_size<array<T, N> > : integral_constant< size_t, N>
{
};
是一个模板类,根据传入的array容器类对象,推导出T,N,还 private(默认)继承了integral_constant< size_t, N>模板类。在tuple_size类中定义了一个静态成员变量value,他记录的是传入array容器内数组的长度。
示例:
array<int, 5> a;
cout << "size= " << tuple_size<a> :: value << endl; //编译时获得
cout << "size= " << a.size() << endl; //运行时获得
array容器传参
模板类的关键就是根据传参,推导数据类型,如果是一个类,同样可以利用推导的结果来定义一个与传参相同类型的对象,示例如下:
template<typename T,size_t N> void func(array<T,N>) //通过传参获取T,N。
{
array<T,N> aa;
cout<<"size= "<<aa.size()<<endl;
}
array<int,6> a; //定义一个array容器
func(a); //将容器类传给函数func
tuple_element
作用:获取容器中指定元素的类型;
原型:template<size_t I, class T, size_t N > struct tuple_element<I, array<T, N> >;
使用:
using T=tuple_element<0, decltype(data)>::type //using T起typedef效果;
is_same<t,int> // is_same在标准库,判断两个类型相同true,否false
上式中decltype时c++关键字,作用是获得表达式的类型。
迭代器
迭代,升级迭代,去旧用新的意思,包含了从最初的版本一次次的升级迭代,本质上是一种遍历行为,其实迭代器底层就是通过移动指针来遍历处理的一种机制。
实际上我们也早都接触过迭代行为,在c中我们遍历数组使用*p++,指针变量就是一种遍历迭代器。但是指针不能用来直接p++来访问结构体内部元素,就需要一种更高级的迭代器。
所有迭代器都共同继承了一个基类(接口),这个基类规定迭代器的一些基本行为,如++;--这种基本操作。每一个容器内都包含了一个专属的迭代器成员类iterator,将该类实例化后通过对象就可以直接对该对象进行++、--就可对STL容器进行遍历。通常我们定义这个变量的时候习惯将名称写为iter,当然写其他的也可以,使用示例如下。
array<int, 5> a= {1,2,3,4,5}; //定义一个容器类
array<int, 5> :: iterator iter; //定义一个迭代指针iter
for(iter=a.begin(); iter != a.end();iter++) //判断不用小于,使用!=
{
cout<<iter<<" = " << *iter <<endl; //读
}
式中begin( )和end( )是array容器类中的函数,分别返回起始迭代器和结束迭代器。
const与非const
出于保护的数据的目的,c++还提供了cbegin与cend带cosnt的只读迭代器,begin与end返回的是可读写的迭代器,而cbegin、cend只读迭代器也就意味着我们对容器的元素只能读不能写。
迭代器内还包含了迭代器指针iterator和constiterator,iterator用于接收begin、end返回的地址,cosnt_iterator用来接收cbegin与cend对应的指针类似是const_iterator,他们必须配套使用,示例如下:
array<int, 5>::const_iterator iter;
for(iter=a.cbegin(); iter != a.cend();iter++)
{
cout<<iter<<" = " << *iter <<endl;
}
在上例中涉及的cosnt_iterator与iterator有各自对应的迭代器,交叉使用容易出错,所以有的人干脆就不定义iter指针,直接使用auto来自动推导,示例如下:
for(auto iter=a.cbegin(); iter != a.cend();iter++) //自动推导,cbegin/begin
{
cout<<iter<<" = " << *iter <<endl;
}
begin返回的是第0个元素的迭代器,而end返回的是最后一个的下一个元素的地址。如下图,这样做的目的是方便我们去遍历完所有元素,要强调的是在遍历时for循环中不要写“<”,因为在链表中,地址不是连续排列的,尾地址不一定比前面的地址编号大,推荐写“!=”。
逆向迭代器
rbegin与rend返回逆向迭代器,逆向迭代器的rbegin返回的是末尾元素,而rend返回的是首元素的前一个地址,逆向迭代器的++是向前移,--是向后移动,就像挂倒挡给油是向后移动,示例如下。
不管是正向还是逆向迭代器,都遵循“++不到end;--不到begin”。++和--不能单纯的从符号上按常规思维来理解,本质上还是对++和--的运算符重载,所以取决于函数实现方法。
STL的不同类型的迭代器
InputIterator
输入迭代器,只能从容器内读出而不能向容器内写入,只能单次读出(二次读取不保证仍然可二次读取,就像串口接收缓存),只能++不能--(单向的),不能保证二次遍历容器时顺序不变,输入迭代器适用于单通(单向)只读型算法。
outputiterator
输出迭代器,用于将信息传输给容器(修改容器值),但是不能读取(如显示器就是一个典型的只写不读的的设备,可以用输出容器来表示),只能++不能--(单向的),输出迭代器适用于单通只写型算法。
forwarditerator
前向迭代器,只能++不能--(单向的)
bidireciterator
双向迭代器,即能++也能--,双向移动
randomaccessiterator
随机访问迭代器,能双向移动,且可以单出跨越多个元素移动
contiguousiterator
连续迭代器,所指向的逻辑相令元素,在物理上也相邻,如数组。
基于范围的for
用作:对容器中的各个元素进行遍历,可读性更高,书写更简单。
语法:for ( 初始化语句(可选)范围变量声明 : 范围表达式 ) 循环语句
array<int,5> aa={1,2,3,4,5};
for(int iter: aa)
{
cout << iter<<endl;
}
指定元素类型必须与对象元素类型相同,实际大多使用auto自动推导,示例如下:
for(auto iter : aa)
{
cout << iter<<endl;
}
vector容器
vector是动态数组,可以按需扩展和收缩数组大小,实际上vecrot在C99版的c中就已经有了,只是我们很少用,vector是数组的升级,vector容器是array容器的升级版,本质上都是数组,具有数组的共同特性,vector新增了一个动态扩展和收缩的功能,这个扩展和收缩是自动的,我们无需去干预,内部实现原理就是当我们申请一个数组时,他分配的空间会大于我们要申请的空间,以预留给我们扩容,当预留的空间用完了,下次再扩展时,他就只能重新分配一个更大的空间,再把之前的数据拷贝过去,所以vector的地址是动态的,我们不能使用指针去访问,应该使用迭代器去访问,因为他在迁移时会更新迭代器的begin和end。
头文件:vector
定义和初始化:
vector<类型> 对象
vector<int> aa={1,2,3,4,5}; //定义并赋初值
vector<int> aa{1,2,3,4,5}; //定义并赋初值
vector<int> bb(aa); //定义并调用拷贝构造函数
vector<string> bb(5, ”test”); //定义并填充5个相同元素sest
元素访问
at函数 : aa.at(4)=80或 int b= aa.at(4); //读写5个元素
operator[ ] aa[4]=80或 int b=aa[4]; //读写第5个元素
front函数: int b = a.front或a.front=5; //读写数组的第一个元素
back函数: int b = a.back; // 读写数组的最后一个元素
date函数: int *p=a.date; //获取存放数据的首地址
迭代器
begin( )与cbegin( ) auto iter = a.begin( ); //获取起始迭代指针
end( ) 与cend( ) auto iter = a.end( ); //获取起始迭代指针
rbegin( )与crbegin( ) auto iter =a.begin( ); //获取起始迭代指针
容量
empty(): if(a.empty()==true); //检查是否为空,空1,非空0
size(): int b=a.size(); //返回容纳元素数量
max_size(): int b=a. max_size(); //返回最大可容纳元素的数量
reserve(): a.reserve(50); //a容器底层数组分配50个元素大小
capacity( ) int b=a.capacity(); //获取a容器底层数组大小。
修改器
clear( ) a.clear() //清空底层数组元素
insert() auto T=aa.begin(); aa.insert(T+0,90); //插入
erase() a.erase(a.begin() +1); //擦除第1个元素
push_back() a.push_back(55); //将55添加的末尾
pop_back() a.pop_back(); //移除末尾元素
shrink_to_fit() a. shrink_to_fit() //释放底层数组未使用的多余分配空间
遍历
for(auto c: cc)
{
cout << c <<endl;
}
list容器
前面介绍的array、vector都是对普通数组、动态数组的封装,而list则是对链表的封装,具有链表的可在任何时间任何位置插入节点数据、物理地址不一定连续等特点,正因如此所以不支持快速访问,必须重头或者尾遍历到目标节点,与forward_list(单向链表容器)比较,移动更方便,但是占用内存也更多,主要是双向链表多了一个前指针。因为节点的前后由指针链接,所以中间节点的移除不会影响到头尾迭代器。
定义和初始化
list<类型> 对象
list <int> aa={1,2,3,4,5}; //定义并赋初值
list <int> aa{1,2,3,4,5}; //定义并赋初值
list <int> bb(aa); //定义并调用拷贝构造函数
list <string> bb(5, ”test”); //定义并填充5个相同元素sest
元素访问
front() aa.front()+1; //访问第2个元素
back() aa.back()-1; //访问倒数第2个元素
迭代器
begin( )与cbegin( ) auto iter = a.begin( ); //获取起始迭代指针
end( ) 与cend( ) auto iter = a.end( ); //获取起始迭代指针
rbegin( )与crbegin( ) auto iter =a.begin( ); //获取起始迭代指针
容量
empty(): if(a.empty()==true); //检查是否为空,空1,非空0
size(): int b=a.size(); //返回容纳元素数量
max_size(): int b=a. max_size(); //返回最大可容纳元素的数量
修改器
clear( ) a.clear() //清空底层数组元素
insert() auto T=aa.begin(); aa.insert(T+0,90); //插入节点前插
erase() a.erase(a.begin()); //擦除第0个元素
push_back() a.push_back(55); //将55添加的末尾
pop_back() a.pop_back(); //移除末尾元素
操作
sort() a.sort(); //链表值升序排序1、2、3、4、5
reverse() a. reverse(); //将所有元素反转5、4、3、2、1
merge() a.maerge(b); //将b按升序合并,使用前必须分别升序排序