C++ STL 基础及应用 容器

Posted 哈士奇超帅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ STL 基础及应用 容器相关的知识,希望对你有一定的参考价值。

读者可能有这样的经历,自己编写了动态数组类、链表类、集合类和映射类等程序,然后小心地维护着。其实 STL 提供了专家级的几乎我们所需要的各种容器,功能更好,效率更高,复用性更强,所以开发应用系统应该首选 STL 容器类,摒弃自己的容器类,尽管它可能花费了你很多的开发时间。

本章将介绍 STL 中的通用容器

包括 vector、deque、list、queue和stack、priority_queue、bitset、set和multiset、map和multimap等等。

概述

容器分类

(1)序列性容器

按照线性排列来存储某类型值的集合,每个元素都有自己特定的位置,顺序容器主要有 vector、deque、list。
vector:动态数组。它在堆中分配内存,元素连续存放,有保留内存,分配的内存大小只增加不减小,当它满时再添加元素会重新分配一块更大的内存,此时需要移动内存,如果你的元素是结构体或者是类,那么移动的同时还会进行构造和析构操作。对最后元素操作最快(在后面添加删除最快),此时一般不需要移动内存。
deque:与 vector 类似,支持随机访问和快速插入删除,它在容器某一位置上的操作花费的是线性时间。与 vector 不同的是,deque 支持从开始端插入、删除元素。由于它主要对前端、后端进行操作,因此也叫做双端队列。
list:又叫链表,只能顺序访问(从前往后或者从后往前),与前面两种容器有一个明显的区别就是它不支持随机访问。访问链表某一个元素需要从表头或表尾开始循环,直到找到该元素。

(2)关联式容器

与顺序性容器相比,关联式容器更注重快速和高效地检索数据的能力。这些容器是根据键值(key)来检索数据的,键可以是值也可以是容器中的某一成员。这一类中的成员在初始化后都是按一定顺序排好序的。关联式容器主要有 set、multiset、map、multimap。
set:集合,快速查找,不允许重复值。
multiset:集合,快速查找,允许重复值。
map:一对一映射,基于关键字快速查找,不允许重复值。
multimap:一对多映射,基于关键字快速查找,允许重复值。

(3)容器适配器

对已有的容器进行某些特性的再封装,不是一个真正的新容器,主要有 stack、queue。
stack:栈类,特点是后进先出。
queue:队列类,特点是先进先出。

容器共性

容器一般来说都有下列函数:
默认构造函数:    容器默认初始化。
拷贝构造函数:    容器初始化为同类容器副本。
析构函数:    容器关闭,资源释放。
empty():    判断容器是否为空。
max_size():    返回容器中最大元素个数。
size():    返回容器中当前元素个数。
operator = () :    将一个容器赋给另一个容器。
operator < () :    容器比较。
operator <= ():
operator > ():
operator >= ():
operator == ():
operator != ():
swap():    交换两个容器的元素
begin():    返回第一个元素迭代器指针。
end():    返回最后一个元素的后一位的迭代器指针。
rbegin():    返回最后一个元素迭代器指针。
rend():    返回第一个元素的前一位的迭代器指针。
erase():    从容器中清除一个或几个元素。
clear():    清除容器中所有元素。

vector 容器

使用 vector 容器时需要头文件 <vector>。
(1)构造函数
vector<Elem>();    //创建一个空的 vector
vrctor<Elem>(int nSize);    //创建一个 vector,元素个数为 nSize
vrctor<Elem>(int nSize const T& t);    //创建一个 vector,元素个数为 nSize,且值均为 T
vector<Elem>(const vector&);    //拷贝构造函数
(2)添加函数
void push_back(const T& x);    //在尾部添加元素 x
iterator insert(iterator it,const T& x);    //在某一元素前添加元素 x
void insert(iterator it,int n,const T& x);    //在某一元素前添加 n 个相同元素 x
void insert(iterator it,const_iterator first,const_iterator last);    //在某一元素前插入另一个相同类型的[first,last)间的元素
(3)删除函数
iterator erase(iterator it);    //删除某一元素
iterator erase(iterator first,iterator last);    //删除[first,last)之间的元素
void pop_back();    //删除最后一个元素
void clear();    //删除所有元素
(4)遍历函数
reference at(int pos);    //返回 pos 位置元素的引用
reference front();    //返回首元素的引用
reference back();    //返回尾元素的引用
iterator begin();    //返回首元素指针
iterator end();    //返回尾元素的后一个位置的指针
reverse_iterator rbegin();    //反向迭代器,返回尾元素迭代指针
reverse_iterator rend();    //反向迭代器,返回首元素的前一个位置的迭代指针
(5)判断函数
bool empty() const;    //判断 vector 是否为空,为空返回 true
(6)大小函数
int size() const;    //返回向量中元素个数
int capacity() const;    //返回 vector 当前所能容纳元素数量的最大值
int max_size() const;    //返回最大可允许的 vector 元素数量值
(7)其他函数
void swap(vector&);    //交换两个同类型 vector 的元素
void assign(int n,const T& x);    //设置容器大小为 n,每个元素为 x
void assign(const_iterator first,const_iterator last);    //将容器中[first,last)间的元素设置成当前元素

vector 使用代码如下:

#include <iostream>
#include <vector>
using namespace std;
//使用迭代器正序输出 vector 元素
void print(vector<int> v,char s[])
{
	vector<int>::iterator it;
	cout<<s<<":";
	for(it=v.begin();it!=v.end();it++)
	{
		cout<<*it;
	}
	cout<<endl;
}
int main () {
	vector<int> v1;    //默认构造函数,创建一个空的 vector
	vector<int> v2(2);    //创建一个大小为2的 vector
	vector<int> v3(3,0);    //穿件一个 vector,并填充3个0
	vector<int> v4(v3);    //拷贝构造函数,创建v3的副本v4

	v1.push_back(1);    //在尾部添加1,此时v1:1
	v1.insert(v1.end(),2);    //在尾部添加2,此时v1:12
	v1.insert(v1.begin(),2,0);    //在首部添加2个0,此时v1:0012
	v1.insert(v1.begin(),v1.begin(),v1.end());    //在首部添加v1所有内容,此时v1:00120012
	print(v1,"v1");
	cout<<endl;

	v2.push_back(1);    //在尾部添加1,此时v2:001
	v2.push_back(2);    //在尾部添加1,此时v2:0012
	cout<<"v2 size 大小:"<<v2.size()<<"    v2 capacity 大小:"<<v2.capacity()<<endl;
	v2.push_back(3);    //在尾部添加3,此时v2:00123
	cout<<"v2 size 大小:"<<v2.size()<<"    v2 capacity 大小:"<<v2.capacity()<<endl;
	cout<<"v2 max_size 大小:"<<v2.max_size()<<endl;
	v2.erase(v2.begin());    //删除首元素,此时v2:0123
	v2.erase(v2.begin(),v2.begin()+2);    //删除第1、2个元素,此时v2:23
	v2.pop_back();    //删除v2尾元素,此时v2:2
	if(!v2.empty()) cout<<"v2不为空!"<<endl;
	print(v2,"v2");
	cout<<endl;

	v3.clear();    //清空v3,此时v3:
	v3.push_back(0);    //在尾部添加0,此时v3:0
	v3.push_back(1);    //在尾部添加1,此时v3:01
	v3.push_back(2);    //在尾部添加2,此时v3:012
	cout<<"v3:"<<v3.front()<<v3.at(1)<<v3.back()<<endl;
	vector<int>::reverse_iterator rt;
	cout<<"v3倒叙:";
	for(rt=v3.rbegin();rt!=v3.rend();rt++)    //使用反向迭代器逆序输出,输出210
	{
		cout<<*rt;
	}
	cout<<endl<<endl;

	v4.assign(5,1);    //分配v4为5个1,此时v4:11111
	print(v4,"v4");
	swap(v3,v4);    //交换v3、v4元素,此时v3:11111,v4:012
	print(v4,"v4");
	vector<int>::iterator it1=v3.begin();
	vector<int>::iterator it2=v3.end();
	v4.assign(it1,it2);    //将v3的内容复制给v4,此时v3:11111,v4:11111
	print(v4,"v4");
	return 0;
}
输出:
v1:00120012

v2 size 大小:4    v2 capacity 大小:4
v2 size 大小:5    v2 capacity 大小:6
v2 max_size 大小:1073741823
v2不为空!
v2:2

v3:012
v3倒叙:210

v4:11111
v4:012
v4:11111

值得注意的是,v2初始大小设置为2,再添加两个元素"1"、"2"后,capacity 大小为4,再添加"3"后,capacity 大小为6,说明其大小动态在增加。另外,使用 max_size() 得知 v2 最多能有 1073741823 个元素,元素为 int 型,1073741823*4 个字节约为 4G(我的电脑内存为4G),是不是有点巧合呢?读者可以自行去深究。

deque 容器

使用与 vector 类似,多了如下操作:
(2)添加函数
void push_front(const T& x)    //在首部添加元素 x
(3)删除函数
void pop_front();    //删除首元素

大部分功能与 vector 类似,因此不做代码演示了。主要记住 deque 是个双端队列,从在头尾进行操作即可。

list 容器

使用与 deque 类似,多了如下操作:
(3)删除函数
void remove(const T& x);    //删除 list 中所有元素值等于 x 的元素
(7)其他函数
void sort();    //容器内所有元素排序,默认是升序
template<class Pred>void sort(Pred pr);    //容器内所有元素根据预定函数 pr 排序
void unique();    //list 内相邻元素若有重复的,则仅保留一个
void splice(iterator it,list& x);    //队列合并函数,队列 x 所有元素插入迭代器指针 it 前,x 变成空队列
void splice(iterator it,list& x,iterator first,iterator last);    //x 中移走[first,last)间的元素插入至 it 前
void reverse();    //反转容器中元素顺序

list 相关函数使用代码如下:
#include <iostream>
#include <string>
#include <list>
using namespace std;
typedef class student
{
private:
	string name;
	int number;
public:
	//构造函数
	student(string name,int number)
	{
		this->name=name;
		this->number=number;
	}
	//重载全局 operator <<
	friend ostream& operator<<(ostream& out,student& s)
	{
		cout<<s.name<<" "<<s.number;
		return out;
	}
	//重载 operator == 
	bool operator == (const student &s)
	{
		return this->number == s.number;
	}
	//重载 operator <
	bool operator < (const student &s)
	{
		return this->number < s.number;
	}
}S;
void print(list<S> &l,string s)
{
	cout<<s<<endl;
	list<S>::iterator it;
	for(it=l.begin();it!=l.end();it++)
	{
		cout<<*it<<endl;
	}
}
int main()
{
	list<student> list1;
	list1.push_back(S("乾隆",4));
	list1.push_back(S("康熙",3));
	list1.push_back(S("秦始皇",2));
	list1.push_back(S("秦始皇",2));
	//此时list1:乾隆4 康熙3 秦始皇2 秦始皇2

	list<student> list2;
	list2.push_back(S("雍正",1));
	list2.push_back(S("武则天",5));
	list2.push_back(S("康熙",3));
	//此时list2:雍正1 武则天5 康熙3

	list1.swap(list2);
	//此时list1:雍正1 武则天5 康熙3
	//此时list2:乾隆4 康熙3 秦始皇2 秦始皇2
	list1.splice(list1.end(),list2,list2.begin(),list2.end());
	//此时list1:雍正1 武则天5 康熙3 乾隆4 康熙3 秦始皇2 秦始皇2
	//此时list2:空
	list1.sort();
	//此时list1:雍正1 秦始皇2 秦始皇2 康熙3 康熙3 乾隆4 则天5
	//此时list2:空
	list1.unique();
	//此时list1:雍正1 秦始皇2 康熙3 乾隆4 则天5
	//此时list2:空
	list1.reverse();
	//此时list1:则天5 乾隆4 康熙3 秦始皇2 雍正1
	//此时list2:空
	print(list1,"list1:");
	print(list2,"list2:");
	return 0;
}
做几点说明:
1.编写 list<student> 的 print() 函数时,想用 cout<<*it 必须先重载全局函数 ostream& operator<< ,具体见第2章。
2.使用容器的 sort() 函数时,若元素不是基本类型,必须先重载 operator < 函数以供 sort() 函数比较。
3.使用容器的 unique() 函数时,若元素不是基本类型,必须先重载 operator == 函数以供 unique() 函数比较。

queue和stack

队列和栈是常用而且重要的数据结构。队列先进先出,栈后进先出,STL 中将基本容器再次封装,实现这两个数据结构。
队列独有函数:
queue<class T,class Container=deque<T>>;    //构造函数,创建元素类型为T的空队列,默认容器是 deque
T& front();    //当队列非空情况下,返回队头元素引用
T& back();    //当队列非空情况下,返回队尾元素引用

栈独有函数:
stack<class T,class Container=deque<T>>;    //构造函数,创建元素类型为T的空栈,默认容器是 deque
T& top();    //当栈非空情况下,返回栈顶元素的引用

队列和栈共有函数:
bool empty();    //队列(栈)为空返回 true,否则返回 false
int size();    //返回队列(栈)中元素数值
void push(const T& t);    //把 t 元素压入队尾(栈顶)
void pop();    //当队列(栈)非空时,返回队头(栈顶)元素

使用 queue 需要头文件<queue>,使用 stack 需要头文件<stack>,比如队列使用如下:
queue<int,list<int>> q;

容器适配器

为了清除容器适配器的概念,先看一段 STL 中 stack 的源代码:
template<class _Ty,class _Container = deque<_Ty> >
class stack
{	
protected:
	_Container c;	// the underlying container
public:
	......

	stack(): c()
	{	// 使用容器类的默认构造函数
	}

	explicit stack(const _Container& _Cont): c(_Cont)
	{	//使用容器类的拷贝构造函数
	}

	void push(value_type&& _Val)
		{	
		c.push_back(_STD move(_Val));
		}

	size_type size() const
		{	
		return (c.size());
		}
	......
};
可以发现构造 stack 时传入的容器对象即为 stack 类的成员函数 _Container c,栈的各元素都存储于 c 中。查看 push() 函数得知, stack 的 push() 实际上是调用了 c.push__back(),也就是说,stack 实际上只是调用了传入的容器对象的原有函数而已,它的各种操作函数只是起到一个适配器的作用,几乎没有自己独有的功能。广而言之,stack 类是对基础容器类的再封装,不是重新定义,queue 类也很相似。注意!由于 queue 类有 T& front() 函数,因此要求封装的容器必须有 pop_front()函数,因此 vector 不能被封装为 queue。 

priority_queue

优先队列即 priority_queue 类,带优先权的队列,根据优先权决定出队顺序,默认的序列容器是 vector。

构造函数
//创建元素类型为 T 的空优先队列,Pred 是二员比较函数,默认是 less<T>。
priority_queue(const Pred& pr=Pred(),const allocator_type& al=allocator_type());
//以迭代器[first,last)指向元素,创建元素类型为 T 的优先队列,Pred 是二员比较函数,默认是 less<T>,可以传 greater<T>。
priority_queue(const value_type* first,const value_type* last,const Pred& pr=Pred(),const allocator_type& al=allocator_type());
操作函数同 queue 类似。

priority_queue 使用代码如下:
#include <iostream>
#include <queue>
#include <string>
#include <vector>
using namespace std;
class student
{
public:
	int number;    //学号
	string name;    //姓名
	int math;    //数学成绩
	int chinese;    //语文成绩
public:
	student(int number,const string &name,int math,int chinese)
	{
		this->number=number;
		this->name=name;
		this->math=math;
		this->chinese=chinese;
	}
	friend ostream& operator << (ostream& out,const student& s)
	{
		cout<<"学号:"<<s.number<<"\t姓名:"<<s.name<<"\t数学:\t"<<s.math<<"\t语文:"<<s.chinese<<endl;
		return out;
	}
	bool operator < (const student& s) const
	{
		//先比较数学成绩,相同再比较语文成绩,相同再比较学号
		//return this->number < s.number;
		if(this->math < s.math) return true;
		if(this->math == s.math && this->chinese < s.chinese) return true;
		if(this->math == s.math && this->chinese == s.chinese && this->number < s.number) return true;
		return false;
	}
};
int main()
{
	priority_queue<student,vector<student>,less<student> > p;
	p.push(student(3,"李世民",60,70));
	p.push(student(1,"秦始皇",70,90));
	p.push(student(2,"康熙",70,50));
	while(!p.empty())
	{
		cout<<p.top();
		p.pop();
	}
	return 0;
}
输出:
学号:1  姓名:秦始皇     数学:   70      语文:90
学号:2  姓名:康熙       数学:   70      语文:50
学号:3  姓名:李世民     数学:   60      语文:70

上述程序中有一个 student 类,主函数以 vector 为内置容器建立了一个优先队列,并使用 less<student> 作为比较函数,为了使用该函数,必须重载 student 类的 operator< 操作符。程序中先比较数学成绩,相同再比较语文成绩,相同再比较学号,权值小的先输出。从输出中可以看到,"李世民"的数学最低,因此权值最小,所以最后才输出。如果想先输出权值大的该怎么办呢?很简单,只需要再建立优先队列时使用 gteater<T> 参数,即 priority_queue<student,vector<student>,greater<student> > p,并重载 student 类的 operator> 操作符即可。

bitset 容器

C 是一种"接近硬件"的语言,但 C 语言并没有固定的二进制表示法。bitset 可以看做是二进制位的容器,并提供了位的相关操作数。
bitset常用函数如下所示:
(1)构造函数
bitset<int Bits>();    //默认构造函数,Bits 为位最大长度
bitset<int Bites>(const bitset&);    //拷贝构造函数
bitset<int Bites>(unsigned long val);    //由无符号长整型数构建位容器
bitset<int Bites>(const string& str,size_t pos,size_t n=-1);    //由字符串创建位容器,pos 为 str 起始位置,n 为根据 str 构建字符个数
bitset& operator= (const bitset&);    //赋值操作
(2)逻辑运算操作
bitset& operator &= (const bitset&);    //返回两个位容器 & 运算后的引用,并修改第一个位容器值
bitset& operator |= (const bitset&);
bitset& operator ^= (const bitset&);
bitset& operator << = (size_t n);    //返回位容器左移 n 位后的引用,并修改第一个位容器值
bitset& operator >> = (size_t n);
bitset operator << (size_t n) const;    //返回位容器左移 n 位后的备份
bitset operator >> (size_t n) const;
bitset operator & (const bitset&,const bitset&);    //返回两个位容器 & 运算后的备份
bitset operator | (const bitset&,const bitset&);
bitset operator ^ (const bitset&,const bitset&);
(3)其他操作函数
string to_string();    //将位容器内容转换成字符串
size_t size() const;    //返回位容器大小
size_t count() const;    //返回设置成 1 的位个数
bool any() const;    //是否有位设置 1
bool none() const;    //是否没有为设置 1
bool test(size_t n)const;    //测试某位是否为 1
bool operator[](size_t n)const;    //随机访问位元素
unsigned long to_ulong() const;    //若没有溢出异常,返回无符号长整型数
bitset& set();    //位容器所有位置 1
bitset& flip();    //位容器所有位翻转
bitset& reset();    //位容器所有位置 0
bitset& set(size_t n,int val=1);    //设置某位为 1 或 0,默认为 1
bitset& reset(size_t n);    //复位某位为 0
bitset& flip(size_t n);    //翻转某位

bitset 容器的使用比较简单,这里直接给出一个它的简易应用。已知有 n 个整形数组,长度都是 10,元素都在[1,20]之间,且均递增排列,无重复数据。试利用 bitset 压缩这些数组,并存入文件中。
分析:一个数组大小=4*10=40字节,而每一位数字其实都能用一个20位大小的 bitset 容器存储,数字是几位容器的第几位就是几。如一个数n=5,则存储为 00000000000000010000。20位相当于2.5字节,与原先的40字节相比,压缩成了原来的1/16。但实际上文件操作的最小单位是字节,无法读写2.5字节,因此位容器需要选择24位大小,这样读写操作正好是3字节了。

使用 bitset 压缩数组代码如下:

#include <bitset>
#include <iostream>
#include <fstream>
using namespace std;
template <size_t N>
class Mynum
{
public:
	bitset<N> b;
public:
	void set(int array[],int nSize)
	{
		b.reset();
		for(int i=0;i<nSize;i++)
		{
			b.set(array[i]-1,1);
		}
	}
};
int main()
{
	int a[4][10]={
	{1,2,3,4,5,6,7,8,9,10},
	{11,12,13,14,15,16,17,18,19,20},
	{2,4,6,8,10,12,14,16,18,20},
	{1,3,5,7,9,11,13,15,17,19}};
	
	fstream out("test.txt");
	Mynum<24> m;
	for(int i=0;i<4;i++)
	{
		m.set(a[i],(sizeof(a[i])/sizeof(int)));
		out.write((char*)&(m.b),3);    //将bitset写入文件
	}
	out.close();

	ifstream in("test.txt");
	bitset<24> b;
	if(!in) return 0;
	else
	{	
		for(int k=0;k<4;k++)
		{
			in.read((char*)(&b),3);    //从文件中读出并存储到bitset中
			for(int i=0;i<24;i++)
			{
				if(b.test(i))
				{
					cout<<i+1<<"\t";
				}
			}			
			cout<<endl;
		}
	}
	in.close();
	return 0;
}
输出:
1       2       3       4       5       6       7       8       9       10
11      12      13      14      15      16      17      18      19      20
2       4       6       8       10      12      14      16      18      20
1       3       5       7       9       11      13      15      17      19

实际中应该把 main 函数中的写入读出功能封装至 Mynum 类中,这里为了简便就这么写了。可以在程序目录下找到"test.txt",可以看到其大小为12字节,而正常存储40个int应该为160字节。

set和multiset

multiset 与 set 的区别就是 multiset 允许重复元素存在,两者都是有序集合。
大部分函数与之前介绍的容器的函数类似,这里就介绍下独有的函数。
首先需要介绍 pair 数据结构,定义如下:
template <class T,class U>
struct pair : public _Pair_base<T, U>
{
	typedef T first_type;
	typedef U second_type;
	T first;
	U second;
	pair();
	pair(const T&x,const U& y);
	template <class V,class W>
		pair(const pair<V,W>& pr);
}
在父结构 _Pair_base 中定义了 T first;U second; 简单地说 pair 是有着两个动态类型成员变量的数据结构。
(1)插入函数:
pair<iterator,bool>insert(const value_type& x);    //插入成功时,pair.first 为插入元素的位置迭代器,pair.second 为 true,插入成功时,pair.first 为与插入元素重复元素的位置迭代器,pair.second 为 false
(2)操作函数
const_iterator lower_bound(const Key& key);    //返回容器元素大于等于 key 的迭代指针,否则返回 end()
const_iterator upper_bound(const Key& key);    //返回容器元素大于 key 的迭代指针,否则返回 end()
pair<const_iterator,cons_iterator>equal_range(const Key& key) const;     //返回容器中包含值等于 key 元素的最小范围[first,last)
可以发现,equal_range() 返回的 pair 的first 即是 lower_bound(),其 second 即是 upper_bound()。

map和multimap

在之前的容器中,仅保存着一样东西,但是在 map和multimap 中将会得到两样东西,关键字 Key 和以关键字查询得到的结果值 Value,即一对值<Key,Value>,map 但映射中,Key 和 Value 是一对一的关系,而在 multimap 中,Key 和 Value 可以是一对多的关系。map和multimap 的函数使用基本与 set 类似,值得讲的是,map和multimap 可以用 [] 运算符来给映射添加键-值对,并返回值得引用。
map和multimap使用代码如下:
#include <map>
#include <string>
#include <iostream>
using namespace std;
int main()
{
	map<string,string> m;
	m["1-1"]="元旦节";    //使用赋值形式添加映射,键在[]内,值在赋值号右侧
	m["5-1"]="劳动节";
	pair<string,string> p("8-1","建军节");    //使用键值对通过 insert() 添加
	m.insert(p);
	cout<<m["8-1"];
	return 0;
}

以上是关于C++ STL 基础及应用 容器的主要内容,如果未能解决你的问题,请参考以下文章

C++ STL 基础及应用 函数对象(仿函数)

C++ STL 基础及应用 模板与操作符重载

C++ STL 之 deque

C++ :1STL 的容器概述array容器详解迭代器初步分析

C++ STL 基础及应用 输出输出流

(C++基础_STL) —— vector 类的基本应用