C++STL的基本学习(三)——函数对象、谓词、适配器

前言

c++的STL这边,还有一点点的内容

 

 

 

 

 


函数对象

函数对象的概念

重载函数调用操作符的类,其对象常称为函数对象(function object),即他们是行为类似函数的对象,也叫仿函数(functor),没错,上一篇文章里set容器和map容器自定义排序规则时,我们就用过了它。

其实就是重载“( )”运算符,使得类对象可以像函数那样调用。

注意:

  1. 函数对象(仿函数)是一个类,不是一个函数
  2. 函数对象重载了“( )”操作符,使它可以像函数那样调用

 

假设某个类有一个重载的operator(),而且重载的operator()要求获取一个参数,我们就将这个类称为“一元仿函数(unary functor)”;相反,如果重载的operator()要求获取两个参数,就称这个类为“二元仿函数(binary functor)”

 

使用案例

class MyPrint {
public:
	void operator()(string val) {
		cout << val<<endl;
	}
};
int main(void) {
	MyPrint p1;
	p1("I'm batman!");
	return 0;
}

函数对象可以像普通函数一样接收参数调用,函数对象可以保证函数调用的状态,可以说函数对象超越了函数的概念。

 

优越性体现

那么仿函数比普通函数的优越性体现在哪里呢?

如果我们有一个vector,我们对vector中元素处理,并且我们要记录调用了几次处理的函数,那我们应该怎么做呢?

int num = 0;
void print(string str) {
	cout << str << endl;
	num++;
}
void test() {
	vector<string> v;
	v.push_back("去年今日此门中,");
	v.push_back("人面桃花相映红。");
	v.push_back("人面不知何处去,");
	v.push_back("桃花依旧笑春风。");

	for_each(v.begin(), v.end(), print);
}
int main(void) {
	test();
	cout << num;
	return 0;
}

我们定义了全局变量来记录,使用全局变量的话,当项目越做越大会造成命名混乱等情况,在使用多线程的时候还要去加锁减锁从而避免值的不固定,这会使得代码非常混乱。

如果使用仿函数的话:

class MyPrint {
public:
	void operator()(string val) {
		cout << val<<endl;
		num++;
	}
public:
	int num = 0;
};

void test() {
	vector<string> v;
	v.push_back("去年今日此门中,");
	v.push_back("人面桃花相映红。");
	v.push_back("人面不知何处去,");
	v.push_back("桃花依旧笑春风。");

	MyPrint mp;
	for (vector<string>::iterator it = v.begin();
		it != v.end(); it++) {
		mp(*it);
	}
	cout << mp.num << endl;
}
int main(void) {
	test();
	return 0;
}

两次的输出都是:

去年今日此门中,
人面桃花相映红。
人面不知何处去,
桃花依旧笑春风。
4

后者我们使用了仿函数之后,在每个类中都有自己的状态变量,这样就不必去创造全局变量了。

注意:为什么函数对象的实验不用for_each而用了老老实实的for循环

如果要调用你会这么做吧?

void test() {
	vector<string> v;
	v.push_back("去年今日此门中,");
	v.push_back("人面桃花相映红。");
	v.push_back("人面不知何处去,");
	v.push_back("桃花依旧笑春风。");

	MyPrint mp;
	for_each(v.begin(), v.end(), mp);
	cout << mp.num << endl;
}

我们可以进入for_each这个函数的底层:

template<class _InIt,
	class _Fn> inline
	_Fn for_each(_InIt _First, _InIt _Last, _Fn _Func)
	{	// perform function for each element [_First, _Last)
	_Adl_verify_range(_First, _Last);
	auto _UFirst = _Get_unwrapped(_First);
	const auto _ULast = _Get_unwrapped(_Last);
	for (; _UFirst != _ULast; ++_UFirst)
		{
		_Func(*_UFirst);
		}

	return (_Func);
	}

看不懂没关系,重点我们发现,形参的第三个参数_Fn并不是引用传递(&),而是值传递,所以如果你把类对象传进来,底层运作是对你传进来的类对象的拷贝对象去运作,所以如果你就按照上面那么写,最后的num的值是0。

怎么解决呢?for_each的底层虽然是值传递创建了拷贝,但是最后它将这个拷贝对象返回了,我们可以接收一下,就得到这个拷贝出来的真正被调用的对象了,最后代码如下:

void test() {
	vector<string> v;
	v.push_back("去年今日此门中,");
	v.push_back("人面桃花相映红。");
	v.push_back("人面不知何处去,");
	v.push_back("桃花依旧笑春风。");

	MyPrint mp;
	MyPrint mp2=for_each(v.begin(), v.end(), mp);
	cout << mp2.num << endl;
}

这样最后输出才是正确的4次。

 


谓词

谓词就是普通函数或重载的operator()返回值是bool类型的函数对象(仿函数)。如果operator接受一个参数,那么叫做一元谓词,如果接受两个参数,那么叫做二元谓词。谓词可以作为一个判断式。

 

 

 


内建函数对象

STL内建了一些函数对象。分为:算数类函数对象,关系运算类函数对象,逻辑运算类仿函数。

这些仿函数所产生的对象,用法和一般函数完全相同,当然我们还可以产生无名的临时对象来履行函数功能。使用内建函数对象,需要印入头文件 #include<functional>

6个算数类函数对象

除了negate是一元运算,其他都是二元运算

template<class T> T plus<T>  //加法仿函数
template<class T> T minute<T> //减法仿函数
template<class T> T multiplies<T>  //乘法仿函数
template<class T> T divides<T> //触发仿函数
template<class T> T modulus<T> //取模仿函数
template<class T> T negate<T>  //取反仿函数

6个关系运算类函数对象

每一种都是二元运算

template<class T> bool equal_to<T>  //等于
template<class T> bool not_equal_to<T>  //不等于
template<class T> bool greater<T> //大于
template<class T> bool greater_equal<T> //大于等于
template<class T> bool less<T> //小于
template<class T> bool less_equal<T> //小于等于

逻辑运算类运算函数

not为一元运算,其余为二元运算

template<class T> bool logical_and<T> //逻辑与
template<class T> bool logical_or<T> //逻辑或
template<class T> bool logical_not<T> //逻辑非

使用示例

int main(void) {
	plus<int> myPlus;
	cout<<myPlus(10,25)<<endl;
	return 0;
}
输出:35

 

 


函数对象适配器(function adapter)

概念

函数对象适配器是完成一些配接工作,这些配接包括绑定(bind)、否定(negate),以及对一般函数或成员函数的修饰,使其成为函数对象,重点掌握函数对象适配器(红色字体)

适配器adapter这个词,学过java的朋友应该也眼熟,没错,java中有个适配器的概念是说一种类,对相应接口进行了处理的类

绑定适配器

仿函数适配器 bind1st bind2nd 绑定适配器

我们来实现一个需求,给一个函数对象传如一个值后,再输出这个值:

struct MyPrint{
	void operator()(int v) {
		cout << v << endl;
	}
};
void test01() {
	vector<int> v;
	for (int i = 0; i < 10; i++) {
		v.push_back(i);
	}
	for_each(v.begin(), 
		v.end(), 
		MyPrint()); //仿函数匿名对象
}

这代码已经没有难度了,但是如果需求变了,现在要求输出的值必须是传入的值加上100,那应该怎么做呢?再operator()的输出中加上100吗?那如果我要改成两百,那岂不是还得修改这个类?

这样并不是聪明的做法,如果给重载的()再加一个形参呢?我们再for_each中转到实现,然后再转到内部for_each的实现,会发现底层的调用只有一个形参位置,所以我们也不能添加形参,那该怎么做呢?

这里我们就可以使用适配器绑定了

struct MyPrint:public binary_function<int,int,void>{
	//函数后加const:该函数对成员变量只读
	void operator()(int v,int val) const { 
		cout << v +val<< endl;
	}
};
void test01() {
	vector<int> v;
	for (int i = 0; i < 10; i++) {
		v.push_back(i);
	}
	int num = 100;
	for_each(v.begin(), 
		v.end(), 
		bind2nd(MyPrint(), num)); //仿函数匿名对象
}

然后就输出了100、101……

这里我们来介绍一下这俩的区别:

  • bind1st:将num绑定为函数对象的第一个参数
  • bind2nd:将num绑定为函数对象的第二个参数

这两的作用都是将二元函数对象转变成了一元函数对象。

 

取反适配器

我们来看一个新的案例,我们产生0~99的随机数,然后将这些随机数放到vector容器中,然后我们对vector容器中的元素进行排序。

struct MyCompare{
	bool operator()(int v1,int v2) { 
		return v1 > v2;
	}
};
struct MyPrint {
	void operator()(int v) {
		cout << v<<"  ";
	}
};
void test02() {
	vector<int> v;
	for (int i = 0; i < 10; i++) {
		v.push_back(rand()%100);
	}
	for_each(v.begin(), v.end(), MyPrint());
	cout << endl;
	sort(v.begin(), v.end(), MyCompare()); //匿名对象
	for_each(v.begin(), v.end(), MyPrint());
}

如果我们想要对结果取反,应该怎么做呢?

struct MyCompare:public binary_function<int,int,bool>{
	bool operator()(int v1,int v2) const { 
		return v1 > v2;
	}
};
struct MyPrint {
	void operator()(int v) {
		cout << v<<"  ";
	}
};
void test02() {
	vector<int> v;
	for (int i = 0; i < 10; i++) {
		v.push_back(rand()%100);
	}
	for_each(v.begin(), v.end(), MyPrint());
	cout << endl;
	sort(v.begin(), v.end(),not2 (MyCompare())); //匿名对象
	for_each(v.begin(), v.end(), MyPrint()); 
}

对于not1和not2:

  • not1:对一元谓词(谓词:返回值为bool的函数对象)进行取反
  • not2:对二元谓词进行取反

不管你的谓词里面是怎么实现的,总之取反后,返回true的变成了返回false。返回false的变成了true;

 

仿函数适配器

ptr_fun:将一个普通函数转成一个伪函数

//仿函数适配器 ptr_fun
void MyPrint03(int val,int val2) {
	cout << val << " "<<val2<<endl;
}
void test02() {
	vector<int> v;
	for (int i = 0; i < 10; i++) {
		v.push_back(i);
	}
	//把普通函数MyPrint03转成函数对象
	for_each(v.begin(), v.end(), bind2nd(ptr_fun(MyPrint03),10));
}

int main(void) {
	test02();  
	return 0;
}
输出:
0 10
1 10
2 10
3 10
4 10
5 10
6 10
7 10
8 10
9 10

 

成员函数适配器

成员函数适配器意在将一个类的内置函数传给for_each这种方法

class Person {
public:
	Person(int age, int id) :age(age), id(id) {}
	void show() {
		cout << "age:" << age << " id:" << id << endl;
	}
public:
	int age;
	int id;
};
void test() {
	/* 如果容器中存放的对象或者对象指针,我们for_each算法打印
	   的时候,调用类自己提供的打印函数
	*/
	vector<Person> v;
	Person p1(10, 1),p2(30,2),p3(50,3);
	v.push_back(p1);
	v.push_back(p2);
	v.push_back(p3);
	//格式:&类名::函数名
	for_each(v.begin(), v.end(),mem_fun_ref(&Person::show ));

	vector<Person*> v1;
	v1.push_back(&p1);
	v1.push_back(&p2);
	v1.push_back(&p3);
	for_each(v1.begin(), v1.end(), mem_fun(&Person::show));
}

mem_fun_ref 与 mem_fun:

  • 如果存放的是对象,使用mem_fun_ref
  • 如果存放的是对象指针,使用mem_fun

 

适配器解惑

  • 为什么即将适配器处理的类/结构体必须继承binary_function?

我也不知道,底层代码太底层了,只知道他是一个结构体,对你的类模板传入的三种类型进行了类名重定义,其他完全看不懂原理,如有大佬请赐教。

  • 为什么函数对象中的重载函数后面要有const

避免我们修改了父类binary_function的成员变量。

 

 

 


 

 

 

 

 

商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢

发表评论