C++复习(初识STL)
本复习主要用于《传智播客的C++程序设计教程》教材的复习
1、STL简介
STL简称标准模板库,它是所有C++编译器和操作系统平台都支持的一种库。
STL的内容从广义上分为三个部分:容器、迭代器、算法。
STL的一个基本理念就是将数据和操作分离,数据又容器类别加以管理,操作则由算法管理,迭代器则用于连接两者(类似指针)。STL的结构如下:
在C++标准中,STL被组织在13个头文件中:、、
1.1、容器
容器类集合在C++中就是标准模板库(STL)。
容器的定义为:在数据存储上,有一种对象类型,它可以装入其它对象或指向****其它对象的指针,这种对象类型就叫做容器。简单地说,容器就是保存其它对象的对象。并且容器类里面还有一系列成员函数用来处理其它对象的方法。
另外容器是可以自由扩展的,即可以自行申请或者释放空间,并且用最优的算法来执行命令。
以下是STL中的主要容器:
template
vector 是一种向量,也可以说成是可以自由扩展的数组。
list 是一种双向链表容器。
queue 是一种队列容器。
stack 是一种栈容器。
priority 是一种按值排序的队列容器。
set 是一种集合容器,集合里面是相同的数据类,但不允许有重复元素。
multiset 是一种允许出现重复元素的集合容器。
map<key,value> 隐射,是一种关联数组容器,即键值对元素。
multimap<key,value> 是一种允许出现重复key值得关联数组容器。
其中以上容器可分为两大类:序列式容器和关联式容器。
序列式容器:vector、list、deque
关联式容器:set、map、multiset、multipap
1.2、迭代器
迭代器是STL中最基本的一部分,它用于将容器与算法联系起来,起着粘合剂的作用,几乎所有STL提供的算法都是通过迭代器存取元素来实现的,类似于指针,但是比指针功能多,只有当参数类型是C++基本数据类型时,迭代器就是指针。
STL中定义了5中类型的迭代器:
输入迭代器:用于读取数据
输出迭代器:用于写入数据
前向迭代器:既可以输入也可以输出,并且可以对一个值进行多次读写
双向迭代器:既可以用来读也可以用来写,比前向迭代器多一个后向元素操作功能
随机访问迭代器:可以随机访问容器中的数据,在双向迭代器上增加了功能,功能最强大。
每一种容器都支持某一种类型的迭代器。
1.3、算法
STL提供了大概70个实现算法的模板函数,即适用于不同容器进行操作。
2、序列式容器
2.1、序列式容器概述。
序列式容器也叫做顺序性容器,各元素之间有顺序的线性表,是一种线性结构的有序群集。
容器中的元素都是可排序的(有顺序的),但未必是已经有序的。序列式容器最重要的特点就是可以在首端删除元素,在尾端添加元素,而双向序列容器(deque),允许在两端添加和删除元素。
序列式容器一般有两种存储方式:连续存储(vector、deque)和链式存储(list)。
序列中每个元素都有固定的位置,除非用删除或者插入的操作改变这个位置,这个位置和元素本身无关,和操作的逻辑顺序有关。
2.1.1、vector向量的实现以及访问特点
vector向量容器一种支持高效的随机访问和高效向尾部插入新元素的容器。向量容器一般实现为一个动态分配的数组,向量中的元素连续地存放在这个数组中,因此对向量容器进行随机访问,具有和动态访问数组类似的效率。
向量容器具有自动扩展容器的功能,即每次插入一个新元素都是“分配新空间-复制元素-释放原空间”,这样效率会低,但是实际分配的空间则会大于所需要的空间。
另外当要删除元素时,多出的闲置空间并不会被释放,因为再插入新元素时可能会重新占用这些空间,即有两个概念:向量容器的容量(capacity)为空间所能存储的最多元素的大小,向量容器的大小(size)为容器实际存储元素的个数。
向量容器中插入新元素时,插入位置之后的元素都要被顺序地往后移动,并且插入位置越靠前,执行插入所需的时间就越多,但在向量尾部插入元素的效率就比较高,因此在总体上向量容器的插入操作效率并不高。
在向向量容器中插入元素时,如果插入操作引起了向量容器的扩展,那么执行插入之前的所获得的一切迭代器、指针、引用都会失效,因为空间被重新分配了,元素的内存地址发生了变化。
如果插入操作没有引起向量容器的扩展,那么只有处于插入位置之后的迭代器、指针、引用失效,因为只有插入位置后面的元素被移动了。
向量容器中删除元素时与插入相类似,即被删除元素越靠前,所需时间就越多,但是删除操作不会引起容器容量的改变,因此被删除元素之前的迭代器、指针、引用都能够继续使用,被删除元素之后的
则会失效。
2.1.2、deque双端队列的实现以及访问特点
deque与vector不同,它是双端开口的,是一种支持向两端插入数据并支持随机访问的容器。双端队列中的数据被表示为一个分段数组,容器中的元素分段存放在一个个大小固定的数组中,此外容器还需要维护一个存放这些数组首地址的索引数组。如图所示
分段数组的大小是固定的,并且它们的首地址被连续地存储在索引数组中,因此可以对双端队列进行随机访问。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QUHJOi98-1616423357705)(C++%E5%A4%8D%E4%B9%A0(%E5%88%9D%E8%AF%86STL)]/qq_pic_merged_1609041824548.jpg)
注意:deque容器在首尾进行添加、删除效率都很高,而在中间就不高,并且在首尾删除时只会让被删除元素的迭代器、指针、引用失效,不会影响其它元素,并且在删除元素之后,不使用的内存区块会被释放掉,这点与vector不一样。而在中间删除时则会使所有的迭代器、指针、引用失效。
deque容器的插入效率比较低,因为每插入一个数据,就需要将插入点到另外一段之间的所有元素向容器的另外一端移动,并且插入位置越靠近中间,所需时间越多,这样插入也会使所有的迭代器、指针、引用失效。
2.1.3、list列表的实现及访问特点
**列表是一种不能随机访问,但可以高效地在任意位置插入和删除元素的容器,列表容器一般实现为链表,并且是双向链表。**如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q0wJvVOM-1616423357706)(C++%E5%A4%8D%E4%B9%A0(%E5%88%9D%E8%AF%86STL)]/qq_pic_merged_1609043291908.jpg)
在列表中插入新的元素 只需要为新元素新建立一个新链表节点,并修改前后两个结点的指针,而不需要移动任何已有元素,因此效率很高,并且不会使任何已有的迭代器、指针、引用失效。
在列表中删除元素时,需要释放掉被删除元素所占用的空间,然后修改前后两个结点的指针,也不需要移动任何元素,因此效率很高,并且执行删除操作时,只会使指向被删除元素的迭代器、指针、引用失效。
即:
需要进行大量的随机访问操作时,使用vector
需要进行大量的随机插入或者删除操作时,使用list
2.2、vector类模板
vector是STL中一种自定义数据类型,包含在头文件中,定义如下:
template<typename T,class Allocator = allocator<T>>
class vector;
vetor是将元素放置于一个动态数组当中加以管理的容器,是最简单的序列式容器。
2.2.1、创建vecor对象
vector类模板中定义了不同的重载构造函数,因此vector对象的创建与初始化也有不同的方式,
1、指定容器大小
vector<元素类型> 对象名(容器大小)
<>中的类型是容器中元素的类型,例如就表示此容器中存储的是int类型的数据。
注意:vector对象在定义后所有元素都会被初始化,如果是基本数据类型,啧啧都会被初始化为0,如果是其它数据类型容器,则由类的默认构造函数初始化。
vector<int> v1(10);//创建一个容量大小为10的int类型容器v1
vector<string> v2(5);//创建一个容量为5的string类型容器v2
这种情况下,指定了容器大小,但是没有指定初始值,标准库就好自动创建一个初始值赋值给数组中的所有元素,这个初始值由元素类型而定。
2、指定初始值
vector<元素类型> 对象名(容器大小,元素初始值);
vector<int> v1(10,1);//10个元素的初始值都为1
vector<string> v2(3,"aa");//3个元素的初始值都为“aa"
3、列表初始化(C++11新标准)
vector<int> v1{1,2};//v1有两个元素1,2
vector<string> v2={"a","b","c"};//v2有3个元素,分别为字符串a b c
注意:列表初始化有两种方式,带”=“符号和不带”=“符号都可以。
4、初始化状态为空
vector对象可以初始化状态为空,即既没有指定容器大小,也没有指定初始值。
vector<int> v1;//创建一个空的int型对象
5、用vector对象来初始化另外一个容器对象
vector<int> v1(10,1);
vector<int> v1(v1);//用v1来初始化v2
Vector<int> v3=v2;//用v2数组给v3数组赋值
注意:用一个vector对象来初始化另外一个容器对象时,两个vector对象的元素类型必须相同,因为它调用的是vector的复制构造函数。
复制构造函数会创建一个临时对象来为容器复制,这样为造成效率低下,我们可以用emplace_back()函数代替,它可以直接给对象赋值。
访问vector对象可以用随机迭代器,也就是下标的形式来访问。
注意:空的vector容器不可以使用下标直接赋值,例如vector v; v[2]=2;是错误语句,因为此时v是空的,不包含任何元素,所有没有相对应的下标来给元素赋值。
2.2.2、emplace_back()函数
c++开发中我们会经常用到插入操作对stl的各种容器进行操作,比如vector,map,set等。在引入右值引用,转移构造函数,转移复制运算符之前,通常使用push_back()向容器中加入一个右值元素(临时对象)时,首先会调用构造函数构造这个临时对象,然后需要调用拷贝构造函数将这个临时对象放入容器中。原来的临时变量释放。这样造成的问题就是临时变量申请资源的浪费。
引入了右值引用,转移构造函数后,push_back()右值时就会调用构造函数和转移构造函数,如果可以在插入的时候直接构造,就只需要构造一次即可。这就是c++11 新加的emplace_back。
emplace_back函数原型:
template <class... Args>
void emplace_back (Args&&... args);
在容器尾部添加一个元素,这个元素原地构造,不需要触发拷贝构造和转移构造。而且调用形式更加简洁,直接根据参数初始化临时对象的成员。
一个很有用的例子:
#include <vector>
#include <string>
#include <iostream>
struct President
{
std::string name;
std::string country;
int year;
President(std::string p_name, std::string p_country, int p_year)
: name(std::move(p_name)), country(std::move(p_country)), year(p_year)
{
std::cout << "I am being constructed.\n";
}
President(const President& other)
: name(std::move(other.name)), country(std::move(other.country)), year(other.year)
{
std::cout << "I am being copy constructed.\n";
}
President(President&& other)
: name(std::move(other.name)), country(std::move(other.country)), year(other.year)
{
std::cout << "I am being moved.\n";
}
President& operator=(const President& other);
};
int main()
{
std::vector<President> elections;
std::cout << "emplace_back:\n";
elections.emplace_back("Nelson Mandela", "South Africa", 1994); //没有类的创建
std::vector<President> reElections;
std::cout << "\npush_back:\n";
reElections.push_back(President("Franklin Delano Roosevelt", "the USA", 1936));
std::cout << "\nContents:\n";
for (President const& president: elections) {
std::cout << president.name << " was elected president of "
<< president.country << " in " << president.year << ".\n";
}
for (President const& president: reElections) {
std::cout << president.name << " was re-elected president of "
<< president.country << " in " << president.year << ".\n";
}
}
输出:
emplace_back:
I am being constructed.
push_back:
I am being constructed.
I am being moved.
Contents:
Nelson Mandela was elected president of South Africa in 1994.
**新版本的原型展示: void push_back(const value_type& x); void push_back(value_type&& x); <typename… Args> reference emplace_back(Args&&… args); 两者区别:push_back传入一个事先存在的元素对象,调用的是拷贝或移动构造来生成这个新压入的元素对象:construct(des, class_name&|&& x)) emplace_back:多个事先存在的对象,调用示义:construct(des, other_type &x|&&x, type_name &|&&,…) emplace_back传参不定,编译器需要在调用时才生成具体的实现,push_back只是emplace_back的两个偏移化版本! push_back只能用类中的拷贝或移动构造,而emplace_back还可以是类中的其他多参数的构造函数,这是优点也是缺点(代码翻倍)&&在普通函数中作为参数时,是万能引用,并不是右值引用,在函数体中会使用forward完美转发,调用时是复制或移动和你传的值有关,你传左值它就用左值版本,传右值就用右值版本,心里要有数。如果一个类没有上诉的拷贝或移动构造,则不能用于STL容器中,如果没有相应参数类型的构造实现,emplace_back编译不过,找不到它需要的对应构造函数… move和copy构造唯一区别:move时,指针属性只是简单的拷贝指针,而copy中,指针属性被拷贝的同时,它所指的具体内容也还需要深度copy下去…我们在实现类的两个构造时特别要注意这一点,move中要将旧对象的指针属性置nullptr,这意味着相应的对象不再适合使用了(它必须是临时对象的原因!转移后,内部的指针属性失效!) move和copy的性能对比:正如上说,move只是指针优化,如果类本身没有指针属性,则它不需要move,我们也不必强制move不可,copy和move在内置类型和简单类型(指无指针属性、即构造时不需要new)没区别
2.2.3、获取容器的容量和大小
vector提供了两个函数capacity()和size()来分别获取容器的容量和大小。
v.capacity();
v.size();
其中v为创建的向量对象。
2.2.4、预留容器容量reserve()
v.reserve(int elem);
elem为要预留容器的容量最小容量。
2.2.5、赋值函数assign()
v.assign(n,elem);//将n个elem元素赋值给容器
v.assign(begin,end);//将[begin,end)迭代器所指区间赋值给容器
注意:第二种赋值方式是左闭右开的形式。
2.2.6、访问容器的元素[]、at()
v[int idx];
v.at(int idx);//返回索引指向的数据,与[]用法相同
2.2.7、从尾部插入和删除元素push_back、pop_back
v.push_back(type elem& t);
v.pop_back();
push_back()函数是向vector容器末尾添加元素,即先创建一个空的vector对象,再调用push_back()函数向其中添加元素,而pop_bakc()用于弹出vector容器末尾的一个元素。
注意:由于vector是可自由扩展容器大小的容器,因此很容易因为没有初始化容量而导致浪费大部分时间用于自动申请空间上,因此最好先定义一个大小足够的空vector对象,再在运行时向其中添加元素。
2.2.8、获取头部和尾部元素
v.front();
v.back();
它们分别返回容器的头尾元素的引用。
v.begin();
v.end();
begin和end则返回头尾元素的迭代器(相当于指针),并且begin返回第一个元素的迭代器,end返回最后一个元素的下一个位置的迭代器。
2.2.9、插入和删除元素
v.insert(pos,elem);//在pos位置上插入元素elem,返回新数据的位置
v.insert(pos,n,elem);//在pos位置上插入n个elem元素,无返回值
v.insert(pos,begin,end);//在pos位置上插入[begin,end)区间的数据,无返回值
v.erase(pos);//移除pos位置上的元素,返回下一个数据的位置
v.erase(begin,end);//移除[begin,end)区间的数据,返回下一个严肃的位置
注意:insert和erase函数中的位置只能是由begin()或者end()返回的迭代器来指示,而不能用纯粹的数字来作为参数。
2.2.10、翻转函数reverse(begin,end)
2.2.11、判空函数empty()
2.2.12、重新设置容器大小函数resize()
V.resize(n);
v.resize(n,elem);
注意:当容器原来大小小于要设置的大小n时,会扩容,相反则会销毁多余部分。
例如:
/*vector容器示例
vector<int> v;
v.empty(); // 判断容器v是否是空。v.empty() == 1表示v为空
v.push_back(10); // 添加元素10到v
v.pop_back(); // 从v中取出元素,容器中的该元素就删除了
v.size(); // 获取容器的大小
sort(v.begin(), v.end()); // 对v中元素排序,sort函数参见sort的用法
// sort(v.begin(), v.end(), cmp); // 需要定义cmp函数。
*/
#include<iostream>
#include<vector>
#include<list>
#include<algorithm>
#include <iterator>
#include <numeric>
using namespace std;
void print(vector<int> myv)
{
vector<int>::iterator it;
for (it = myv.begin(); it != myv.end(); it++) //通过迭代器访问容器值
cout << *it << " ";
cout << endl;
return;
}
void assignfun1(vector<int>& v) //给容器赋值,参数需要用引用或指针
{
int arr[] = { 21, 4, 55, 22, 46, 79, 9, 5, 78, 34, 100 };
v.assign(arr, arr + sizeof(arr) / sizeof(int)); //用数组给v容器赋值
return;
}
void assignfun2(vector<int>& v) //给容器赋值,参数需要用引用或指针
{
int i;
for (i = 0; i < 10; ++i)
{
v.push_back(i); //
}
return;
}
template<typename T>
class Multi //类模板,用于for_each函数中
{
private:
T value;
public:
Multi(const T& v) :value(v) {} //构造函数
void operator()(T& elem) const { elem *= value; } //重载()运算符
};
void VectorFun()//vector容器的使用
{
int i, k;
vector<int> v;
vector<int>::iterator itv;
assignfun1(v);
//assignfun2(v);
for (i = 0; i < 10; ++i)
{
cout << v[i] << " "; // 可以用下标来访问这个容器,跟数组类似
}
cout << endl;
cout << "使用迭代器来输出数据:\n";
print(v);
cout << "翻转数据:" << endl;
reverse(v.begin(), v.end()); // reverse函数功能:翻转字符串 ,翻转数组, 用于STL的翻转。
print(v);
//排序
cout << "排序:" << endl;
sort(v.begin(), v.end());//调用sort()算法将容器中元素从小到大排列 //less<int>(),其他:equal_to, not_equal_to,less_equal,greater_equal
print(v);
sort(v.begin(), v.end(), greater<int>());//调用sort()算法将容器中元素从大到小排列 //greater<int>()
print(v);
//查找
cout << "查找:" << endl;
cout << "input num=";
cin >> k;
itv = find(v.begin(), v.end(), k);
//find()算法用于在[begin, last)区间查找value元素是否存在。
//如果存在,就返回指向这个元素的迭代器,如果不存在,就返回end。
if (itv != v.end())
cout << "find ok=" << *itv << endl;
else
cout << "No find.\n";
//for_ each()函数
cout << "对每个元素乘以5,结果为:\n";
for_each(v.begin(), v.end(), Multi<int>(5));
print(v);
//累加
cout << "累加:" << endl;
cout << "累加和=" << accumulate(v.begin(), v.end(), 0) << endl;
//需要头文件 //#include <numeric>
return;
}
void printlist(list<int> myl)
{
list<int>::iterator it;
for (it = myl.begin(); it != myl.end(); it++) //通过迭代器访问容器值
cout << *it << " ";
cout << endl;
return;
}
void ListFun() //list容器的操作
{
list<int> mylist;
int arr[] = { 21, 4, 55, 22, 46, 79, 9, 5, 78, 34, 100 };
mylist.assign(arr, arr + sizeof(arr) / sizeof(int)); //用数组给v容器赋值
cout << "\nList容器:\n";
printlist(mylist);
//排序,list容器本身有sort函数
cout << "从小到大排序:\n";
mylist.sort();//升序
printlist(mylist);
cout << "从大到小排序:\n";
mylist.sort(greater<int>());//降序
printlist(mylist);
return;
}
int main()
{
//vector容器的使用
VectorFun();
//list容器的使用
ListFun();
return 0;
}
//容器中存储结构体数据(对象数据)的操作方法
#include<iostream>
#include<vector>
#include<list>
#include<algorithm>
#include <iterator>
#include <numeric>
//#include <string>
using namespace std;
struct student
{
int num;
string name;
char sex;
int score;
};
void assignfun1(vector<student>& v) //给容器赋值,参数需要用引用或指针
{
student arr[] = { {1001,"name1",'F',90},{1002,"name2",'M',95},{1003,"name3",'M',91} };
v.assign(arr, arr + sizeof(arr) / sizeof(student)); //用数组给v容器赋值
return;
}
void assignfun2(vector<student>& v) //给容器赋值,参数需要用引用或指针
{
int i;
student s1, s2;
s1.num = 2001;
s1.name = "name21";
s1.sex = 'F';
s1.score = 92;
s2.num = 2002;
s2.name = "name22";
s2.sex = 'M';
s2.score = 96;
v.push_back(s1);
v.push_back(s2);
return;
}
void print(vector<student> myv)
{
vector<student>::iterator it;
for (it = myv.begin(); it != myv.end(); it++) //通过迭代器访问容器值
cout << it->num << " " << it->name << " " << it->sex << " " << it->score << "\n";
cout << endl;
return;
}
bool cmpnum(student a, student b) //按num由大到小
{
return (a.num > b.num);
}
bool cmpscore(student a, student b) //按score由小到大
{
return (a.score < b.score);
}
class findnum
{
public:
findnum(int num) :cmp_num(num) {}
bool operator ()(student s)
{
return cmp_num == s.num;
}
private:
int cmp_num;
};
class findname
{
public:
findname(string cmp_string) :cmp_name(cmp_string) {}
bool operator ()(student s)
{
return cmp_name == s.name;
}
private:
string cmp_name;
};
void findprint(vector<student>::iterator itv)
{
cout << "find ok=";
cout << itv->num << " " << itv->name << " " << itv->sex << " " << itv->score << "\n";
return;
}
int main()
{
int i, k;
string namestr;
vector<student> v;
vector<student>::iterator itv;
assignfun1(v);
cout << "使用迭代器来输出数据:\n";
print(v);
for (i = 0; i<v.size(); ++i)
{
cout << v[i].num << " " << v[i].name << " " << v[i].sex << " " << v[i].score << "\n"; // 可以用下标来访问这个容器,跟数组类似
}
cout << endl;
assignfun2(v);
print(v);
cout << "翻转数据:" << endl;
reverse(v.begin(), v.end()); // reverse函数功能:翻转字符串 ,翻转数组, 用于STL的翻转。
print(v);
//排序
cout << "排序:" << endl;
sort(v.begin(), v.end(), cmpnum); //cmpnum是比较函数
print(v);
sort(v.begin(), v.end(), cmpscore);
print(v);
//查找
cout << "查找:" << endl;
cout << "input num=";
cin >> k;
itv = find_if(v.begin(), v.end(), findnum(k));
//find()算法用于在[begin, last)区间查找value元素是否存在。
//如果存在,就返回指向这个元素的迭代器,如果不存在,就返回end。
if (itv != v.end())
findprint(itv);
else
cout << "No find.\n";
cout << "input name=";
cin >> namestr;
itv = find_if(v.begin(), v.end(), findname(namestr));
if (itv != v.end())
findprint(itv);
else
cout << "No find.\n";
return 0;
}
verse(v.begin(), v.end()); // reverse函数功能:翻转字符串 ,翻转数组, 用于STL的翻转。
print(v);
//排序
cout << "排序:" << endl;
sort(v.begin(), v.end(), cmpnum); //cmpnum是比较函数
print(v);
sort(v.begin(), v.end(), cmpscore);
print(v);
//查找
cout << "查找:" << endl;
cout << "input num=";
cin >> k;
itv = find_if(v.begin(), v.end(), findnum(k));
//find()算法用于在[begin, last)区间查找value元素是否存在。
//如果存在,就返回指向这个元素的迭代器,如果不存在,就返回end。
if (itv != v.end())
findprint(itv);
else
cout << "No find.\n";
cout << "input name=";
cin >> namestr;
itv = find_if(v.begin(), v.end(), findname(namestr));
if (itv != v.end())
findprint(itv);
else
cout << "No find.\n";
return 0;
}