Essential c++ 第三章
这一章内容主要教你如何使用容器,以及如何脱离变量类型和容器类型来完成一个函数
1.前置知识
1.1 下标操作的实质
1.1.1 下标操作的介绍
假设一个数组
int a[3]={1,2,3};
a[2]是一种下标操作,代表取序列为2的数字。这种操作的实质是什么呢?
那我们就从数组的实质开始介绍。数组实际上是内存上一段连续的空间,按照变量大小进行分割,由此产生的一种容器类型。
如果我们尝试输出 :
cout<<a<<endl;
实际上会输出数组a的首地址,包括数组在函数间进行传递实质上也是用这个首地址进行传递的。
所以下标操作 a[2] 实际上是一种地址偏移运算和提领运算的组合,即
a[2] 等价于 *(a+2)。
这种地址偏移运算并不是单纯的地址数加2,而是考虑指针类型的偏移。比如假设a的首地址为1000,一些计算机中int为4位,那么执行 *(a+2) 操作之后,实际上地址指向的是1000+2x4 = 1008;
1.2 迭代器(泛型指针)
迭代器也是一种地址的偏移运算,不过与1.1中的偏移运算不同,迭代器不是直接对内存地址的修改,而是抽象了一层。下层以内存表的形式定义好了下一个内存地址指向什么地方。迭代器进行增加后,便是读取这个内存表的下一个地址在什么地方,就决定了在哪个地方进行操作。
迭代器与地址偏移操作相比,实际上是解决了泛型算法中,如果读取内存不连续的容器的数据的问题。比如容器list,list是储存数据时,内存是不连续的,随机存放的,各个元素直接通过前向指针和后向指针进行访问。因此,使用地址偏移操作,并不能够读取list的下一个元素,并且这种操作是不合法的。迭代器就能够通过简单的偏移操作,读取下一个元素的地址。迭代器的出现,为脱离容器类型而存在的算法的实现提供了可能。
1.3 函数对象
1.3.1 什么是函数对象
函数对象实际上是在一个类中,将函数调用符 () 进行了重载。除了使用上非常像函数之外,更多的是,具有类的特性,因此与函数相比,多了三个优点:
- 因为类可以实例化,因此函数对象可以当做变量进行定义
- 因为类可以包含数据元素,因此函数对象可以增加中间储存变量,便于查看函数的状态
- 使用函数对象在函数间进行传递,与函数指针相比,函数对象可以变成inline的,能够省去函数指针进行地址调用时候产生的额外消耗。函数对象运行效果更高。
1.3.2 函数对象的定义和使用方法
这里定义一个打印容器元素的函数对象
template <typename inputIterator>
class myOutput
{
public:
void operator()(inputIterator first, inputIterator last)
{
for (; first != last; first++)
{
cout << (*first) << endl;
}
}
};
函数对象可以实例化再使用,也可以匿名使用,下面为两种用法的举例
定义了类myOutput,在里面重载了(),因此定义了类的实例a之后,使用运算符(),就可以当函数使用
//1 这就是函数对象
vector<int> ivec = { 1,2,3,8,5,6 };
myOutput<vector<int>::iterator> a;
a(ivec.begin(), ivec.end());
cout << endl;
//2 也可以通过下面这种方法,匿名调用函数对象
myOutput<vector<int>::iterator>()(ivec.begin(), ivec.end());
cout << endl;
1.3.3 stl函数调用函数对象的举例
这里举例transform函数,transform函数可以对指定的两个容器进行指定的操作,并且输出到指定位置,因此transform函数具有五个参数
transform(
t1.begin(),
t1.end(),
t2.begin(),
t3.begin(),
multiply<int>()
);
- 前两个参数意义是对哪个容器进行操作,迭代器类型
- 第三个参数意义是和谁进行操作,传入迭代器的首地址
- 第四个参数意义是是操作完了以后存放到哪里,传入输出容器的迭代器首地址
- 第五个参数意义是进行什么操作,是一个函数对象或者函数指针类型,用于描述两个容器之间要做什么。
下面举例为使用两个容器进行乘法操作的例子
//定义
template<typename T>
class multiply
{
public:
T operator()(T a, T b)
{
return a * b;
}
};
//使用
vector<int> t2 = { 2,3,4,5 };
vector<int> t3(4);
transform(t1.begin(), t1.end(), t2.begin(),t3.begin(), multiply<int>());
for (int i = 0; i < t3.size(); i++)
{
cout << t3[i] << endl;
}
1.3.4 谓词
返回值为bool的函数对象被称为谓词,在stl很多函数都有所应有,比如条件查找函数find_if。
find_if函数具有三个参数
iterator find_if(iterator start,iterator end, condition);
前面两个是要查找的数列的首末迭代器,第三个参数是一个函数对象。当数列中的元素送入函数对象,返回值为true时,返回这个元素的迭代器。
下面举例为通过find_if 函数查找容器中n的倍数的数字。
//01 定义函数对象
template<typename num>
class multiple
{
public:
int val;
//因为需要指定要找谁的倍数,所以类中需要一个额外的变量
//构造函数
multiple(int val):val(val){}
//函数对象
bool operator()(num a)
{
return (a % val == 0);
}
};
//02 使用函数对象
// 谓词-返回值为bool的函数对象,被称为谓词。
//参数列表为一个变量时,叫做一元谓词,参数列表为两个变量时,叫做二元谓词,容器类很多函数都使用谓词作为参数传入
//下面一个例子要介绍如果在一个数列中找到第一个4的倍数
//02-1这个例子使用了含有构造函数的函数对象
cout<<*(find_if(ivec.begin(), ivec.end(), multiple<int>(4)))<<endl;
//02-2含有构造函数的函数对象的使用
multiple<int> mul(4);//需要往构造函数里面加入一个数
mul(8);//调用构造函数
//02-3 匿名的
multiple<int>(4)(8);//返回8是否为4的倍数
1.4 绑定适配器
绑定适配器运行需要头文件
一种绑定适配器的作用是减少函数对象的变量个数,让二元运算变成一元运算。c++中其实已经定义了很多个默认的函数对象,用来代替常用的运算符号,比如加减乘除、逻辑运算等等。而这些函数对象都是需要两个变量的,比如比较运算,而使用绑定适配器固定前一个数或者后一个数字,就变成了一元操作,让输入的变量与固定的变量进行比较,并返回bool
- 绑定前一个数使用bind1st(val,function object);
- 绑定后一个数使用bind2nd(function object,val);
比如查找数列中大于5的变量,可以用find_if这么写
find_if(
iter.begin(),
iter.end(),
bind2nd(greater<int>,5);
)
另外一种绑定适配器是谓词运行结果结果取反的,not1对非二进制操作谓词取反,not2对二进制操作谓词取反,举例
not1(bind2nd(greater<int>,5));
2.容器
容器包含顺序容器list,vector,deque和关联容器map,set。是泛型算法操作的主要对象,用来储存各种数据的。
2.1 容器的共性操作
2.1.1 容器的定义方法
有五种定义方法
- 产生空容器 vector a;
- 指定大小 vector a(10);
- 指定大小和默认值 vector a(10,0);
- 截取某容器的一部分 vector a(ivec.begin(),ivec.end());
- 复制 vector a(ivec);
2.1.2 插入操作
三种插入操作-insert函数
- iterator insert(iterator pos,typename val) 在某个地址前插入
- void insert(iterator pos,int count,typename val) 在某个地址前插入count个数字
- iterator insert(iterator pos,iterator start,iterator end) 在指定地址前插入一段序列
举例:
insert(it,5);
2.1.3 删除操作
两种删除操作-erase函数
- 删除某一个位置 iterator erase(iterator pos)
- 删除某一组元素 iterator erase(iterator start,iterator end) 左删右不删
举例:
erase(it);
2.1.4 特殊操作
- 在最后加入或者删除一个元素push_back()和pop_back()
- 在前面加入或者删除一个元素push_front()/pop_front()
备注:只有list和deque能用,list不能用 - 读取最前或者最后的元素 front()/back()
举例:
vec.push_back(1);
2.1.5 其他操作
- 容器判断是否相等==和!=
- =容器赋值(assignment)
- 判断容器是否为空empty()
- 容器大小size()
- 容器清空clear()
- 容器查找 find() 返回值为符号要求的元素的迭代器
2.2 顺序容器
顺序容器包括vector、deque和list三种。需要分别的头文件才能使用//
vector储存地址连续,往容器最后插入元素效率高,如果往中间插入的话,需要将后面的元素全部重新复制,才能插入,效率很低。但是搜索起来非常快。适合用来保存数列
list储存地址是随机的,因此往容器中任何一个位置插入和删除元素都很方便。但是由于存储随机,读取时候要全部遍历,非常耗时间。时候用来从档案中读取数据。
deque储存地址连续,但是与vector不同,往容器最前端插入和删除数据效率最高。如果往容器前端插入元素,末端删除元素,选择deque是最好的。
2.3 关联容器
需要分别的头文件才能使用
2.3.1 map
map叫做字典,能够按照索引寻找数值,比如
map<string,value> imap;
string words;
cin>>words;
imap[words]++;
2.3.2 set
set是一种关键词表,可以与map配合使用。比如用map统计一篇文章的词频,可以通过set设定不想统计的词,如果set中包含这个词,map就跳过,不加人其中。
3. 泛型算法
泛型算法就是脱离变量类型和容器类型,来实现各自操作的算法
3.1 四种常用的泛型算法
-
搜索无序列表中的某个元素 find()
- iterator find(iterator start,iterator end,typename val)
-
搜索有序列表中的某个元素 binary_search()
- 只能用于有序集合的搜索,数列需要先按大小进行排序,排序这一过程需要由程序员进行
- 比find更快,但是前提是你用的数列是排序过的
- bool binary_search(iterator start,iterator end,typename val)
-
搜索某个元素出现了几次 count()
- int (iterator start,iterator end,typename val)
-
搜索是否存在某个子序列 search()
- iterator search(iterator1 start,iterator1 end,iterator2 start,iterator2 end)
- 如果包含子序列返回第一个元素的iterator
- 如果不包含子序列,返回end
3.2 应用举例-寻找容器中满足要求的子列
#include<iostream>
#include<vector>
#include<list>
#include<functional>
#include<algorithm>
using namespace std;
//01 方法一
vector<int> myFind1(const vector<int>& ivec, int val)
{
vector<int> local_vec;
vector<int>::const_iterator it = ivec.begin();
vector<int>::const_iterator end = ivec.end();
while ((it = (find_if(it, end, bind2nd(greater<int>(), val)))) != end)
{
local_vec.push_back(*it);
it++;
}
return local_vec;
}
//02 方法二
vector<int> myFind2(const vector<int>& ivec, int val)
{
//原始数列进行排序
vector<int> local_vec(ivec);
sort(local_vec.begin(), local_vec.end());
vector<int>::iterator iter = find_if(local_vec.begin(), local_vec.end(), bind2nd(greater<int>(), val));
local_vec.erase( local_vec.begin(),iter);
return local_vec;
}
int main()
{
vector<int> ivec1 = { 1,8,3,3,4,4,5,10,4,7 };
//01 方法一,进行逐个的查找
vector<int> dst_ivec1 = myFind1(ivec1, 5);
for (size_t i=0; i < dst_ivec1.size(); i++)
{
cout << dst_ivec1[i] << endl;
}
cout << endl;
//02 方法二,排序后删除不需要的部分
vector<int> dst_ivec2 = myFind2(ivec1, 5);
for (size_t i = 0; i < dst_ivec2.size(); i++)
{
cout << dst_ivec2[i] << endl;
}
return 0;
}
4. 容器设计不需要考虑容量大小的设计技巧
4.1 什么是iterator inserter
使用iterator inserter把赋值符号取代为其他各种操作,可以不需要考虑容器的最大容量,也不需要提前设定容器大小。
比如如果容器不初始化大小,使用它的迭代器作为输出容器会报错,但是定义了初始化大小就可能不够用,iterator inserter就是为了解决这个问题的
需要头文件
4.2 iterator inserter有几种类型
- back_inserter(vec)
- 只有有push_back的容器才能使用
- 以向容器后插入操作代替赋值符号
- inserter(vec,vec.end())
- 有两个参数,第一个是哪个容器,第二个是从哪开始插入
- 以向容器指定点插入代替赋值符号
- front_inserter(lst)
- 只适用于list和deque
- 以push_front操作代替赋值操作
4.3 iterator inserter使用举例
举例:
copy(src.begin(),src.end(),back_inserter(dst));
通过这样写,dst容器进行复制时,不使用赋值符号,而改用push_back进行,这样dst就不需要提前设定大小了。
一个比较完整的例子:
将上文中的filter函数的传入参数稍微修改了一下,输出容器使用了back_inserter,这样的话里面的赋值操作在程序运行时被改为push_back
#include<iostream>
#include<algorithm>
#include<vector>
#include<list>
#include<functional>
#include<iterator>
using namespace std;
template<typename inputIterator, typename outputIterator, typename valType, typename comp>
outputIterator filter(inputIterator first, inputIterator last, outputIterator at, valType val, comp pred)
{
while ((first = find_if(first, last, bind2nd(pred, val))) != last)
{
cout << "发现了一个!" << *first << endl;
*at++ = *first++;
}
return at;
}
int main()
{
vector<int> ivec = { 1,4,5,2,6,7,8 };
list<int> ilist = { 1,4,6,7,8,9,12,3,4 };
//int dst_ivec[10];
vector<int> dst_ivec;
filter(ivec.begin(), ivec.end(), back_inserter(dst_ivec), 5, less<int>());
cout << endl;
filter(ilist.begin(), ilist.end(), back_inserter(dst_ivec), 5, greater<int>());
return 0;
}
5. 来自标准输入输出和文件输入输出的迭代器操作
通过迭代器的方法读取标准输出输出和文件中的内容
- 标准输入输出
#include<iostream>
#include<iterator>
#include<vector>
#include<algorithm>
using namespace std;
int main()
{
vector<string> words;
//输入迭代器
istream_iterator<string> it(cin);
istream_iterator<string> eof;
//将输入迭代器内容放置到向量中
copy(it, eof, back_inserter(words));
//排序
sort(words.begin(), words.end());
ostream_iterator<string> os(cout, " ");
copy(words.begin(), words.end(), os);
return 0;
}
- 文件输入输出
#include<iostream>
#include<iterator>
#include<vector>
#include<algorithm>
#include<fstream>
using namespace std;
int main()
{
ifstream in_file("1.txt");
ofstream out_file("2.txt");
vector<string> words;
//输入迭代器
istream_iterator<string> it(in_file);
istream_iterator<string> eof;
//将输入迭代器内容放置到向量中
copy(it, eof, back_inserter(words));
//排序
sort(words.begin(), words.end());
ostream_iterator<string> os(out_file, " ");
copy(words.begin(), words.end(), os);
return 0;
}