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

把函数作为对象是程序设计的新思维。STL 通过重载类中的 operator() 函数实现函数对象功能,不但可以对容器中的数据进行各种各样的操作,而且能够维护自己的状态。因此,与标准 C 库函数相比,函数对象更为通用。

本章将介绍函数指针的使用、函数对象的定义、引入目的、使用方法。

C++98 标准和C++11标准下 STL 内置函数对象的详细介绍、适配器类的使用。包括 bind1st bind2nd not1 not2 mem_fun mem_fun_ref ptr_fun bind ref cref 的使用。

函数指针

函数指针是指向函数的指针变量,在程序编译时,每一个函数都有一个入口地址,那么这个指向这个函数的函数指针便指向这个地址。函数指针主要由以下两方面的用途:调用函数和用作函数参数。

函数指针的声明方法
数据类型标识符 (指针变量名) (形参列表); 
一般函数的声明为: 
int func(int x); 
而一个函数指针的声明方法为: 
int (*func) (int x); 
(*func)的括号是必要的,告诉编译器声明的是函数指针而不是一个具有返回类型为指针的函数,如果不加括号,int* func (int x)则变成了返回值为 int * 的 func(int x) 函数; 后面的形参则根据函数形参而定。函数地址可以通过函数名或者对函数名取址获得,简单示例如下所示:

#include 
using namespace std;
int Add(int x,int y)
{
	return x+y;
}
int main()
{
	int (*f)(int x,int y);    //声明一个函数指针
	f=Add;    //函数指针赋值
	//也可以写成: f=&Add;
	cout<

为何引入函数对象

首先看下面这段代码,使用 for_each 算法求保存在向量中的整数之和:
#include 
#include 
#include 
using namespace std;
int sum=0;
void f(int x)
{
	sum+=x;
}
int main()
{
	vector v;
	for(int i=0;i<100;i++)
	{
		v.push_back(i);
	}
	for_each(v.begin(),v.end(),f);
	cout<
for_each()函数定义于 中,前两个参数为限定范围的迭代器,最后一个参数为一个函数指针,for_each()根据迭代器限定范围每次取出一个值作为参数执行第三个参数指向的函数。在本例中,即从 v.begin() 开始依次取出值 x,执行 f(x);直到 v.end() 为止。注意到,为了实现这个求和功能,使用了一个 sum 全局变量,并且 f(int) 是一个全局函数。随着 C++ 面向对象的思想的普及和发展,绝大多数的功能都封装在了类中,实现模块化编程。那么上述函数将被封装成如下形式:
class Sum
{
private:
	int sum;
public:
	Sum(){sum=0;}
	void f(int x)
	{
		sum+=x;
	}
	int GetSum(){return sum;}
};
封装性有了,那么该如何方便地调用 Sum::f() 方法呢? 推而广之,如何调用所需类中的所需函数呢?这是一个十分关键的问题。函数对象的使用能够简介快速地使用所需类中对象的函数。

什么是函数对象(也称仿函数)

函数对象是重载了 operator() 的类的一个实例,operator() 是函数调用运算符。一个简单的函数对象实例如下:
class Sum
{
private:
	int sum;
public:
	Sum(){sum=0;}
	void operator()(int x)
	{
		sum+=x;
	}
	int GetSum(){return sum;}
};
注意!与之前的区别在于重载了 operator() 而不是 f() 函数。

使用函数对象实现 for_each 算法求保存在向量中的整数之和:
#include 
#include 
#include 
using namespace std;
class Sum
{
private:
	int sum;
public:
	Sum(){sum=0;}
	void operator()(int x)
	{
		sum+=x;
	}
	int GetSum(){return sum;}
};

int main()
{
	vector v;
	for(int i=0;i<100;i++)
	{
		v.push_back(i);
	}
	Sum s=for_each(v.begin(),v.end(),Sum());
	cout<
此时用 Sum s 来接收 for_each() 返回的最终结果。STL 中定义了很多函数对象供编程者使用,包括大量算法,在本文后面会详细介绍。


函数对象分类

标准 C++ 库根据 operator() 参数个数为 0 个、1 个、 2 个将函数对象加以分类。主要有以下 5 种类型:
发生器:    一种没有参数且返回一个任意类型值的函数对象,例如随机数发生器。
一元函数:    一种只有一个任意类型的参数,且返回一个可能不同类型值的函数对象。
二元函数:    一种有两个任意类型的参数,且返回一个可能不同类型值的函数对象。
一元判定函数:    返回 bool 型的一元函数。
二元判定函数:    返回 bool 型的二元函数。

可以看出,STL 中函数对象最多仅适用于两个参数,但这已经足够完成相当强大的功能,使用 STL 的函数对象时,需要头文件。 

C++98标准 与 C++11标准下对于函数对象的使用有较大改变,下面将介绍一下两者的区别与使用,这里我推荐使用 C++11标准下的函数对象。想详细了解两者的童鞋请参阅本文底部链接。

C++98标准下函数对象使用详解

一元函数

STL 中的一元函数基类是一个模板类,其原型如下:
template
struct unary_function
{	
typedef _Result result_type;
};
两个模板参数,_Arg 为输入参数类型,_Result 为返回类型。

二元函数

STL 中的二元函数基类也是一个模板类,其原型如下:
template
struct binary_function
{	
typedef _Arg1 first_argument_type;
typedef _Arg2 second_argument_type;
typedef _Result result_type;
};
用户虽然自定义的函数对象也能正确编译并使用,但是最好继承上述两者之一,因为对于继承者,STL 能对其进行二次扩展,比如可以使用函数适配器对其进行再次封装,而自己定义的函数对象则缺乏扩展性。
使用二元函数使学生成绩升序排列输出实例:
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

class student
{
private:
	string name;
	int grade;
public:
	student(string name,int grade)
	{
		this->name=name;
		this->grade=grade;
	}
	friend ostream& operator << (ostream& o,const student& s)
	{
		cout<grade < s.grade;
	}
};

template 
class binary_sort : public binary_function<_Arg1,_Arg2,bool>
{
public:
	bool operator()(_Arg1 a1,_Arg2 a2)
	{
		return a1 < a2;
	}
};

int main()
{
	vector v;
	v.push_back(student("秦始皇",80));
	v.push_back(student("康熙",60));
	v.push_back(student("李世民",90));

	sort(v.begin(),v.end(),binary_sort());    //利用二元函数排序
	copy(v.begin(),v.end(),ostream_iterator(cout));    //输出
	return 0;
}
输出:
康熙    60
秦始皇  80
李世民  90

系统函数对象:

STL 提供了部分内建函数对象,分为算数类、关系运算类和逻辑运算类,具体如下。
STL 标准函数对象表:

C++ STL 基础及应用(7) 函数对象(仿函数)_第1张图片

下面给出 plus 的用法,其他的读者可以自行类比探究:
#include 
#include 
#include 
#include 
using namespace std;

class Complex
{
private:
	float r;    //实数部分
	float v;    //虚数部分
public:
	Complex()
	{
		this->r=0.0f;
		this->v=0.0f;
	}
	Complex(float r,float v)
	{
		this->r=r;
		this->v=v;
	}
    friend ostream& operator << (ostream& o,const Complex& c)
	{
		cout<r+c.r,this->v+c.v);
		return temp;
	}
};

int main()
{
	//两个复数相加
	Complex c1(1,1);
	Complex c2(2,2);
	Complex rusult1=plus()(c1,c2);
	cout< v;
	v.push_back(c1);
	v.push_back(c2);
	v.push_back(c3);
	v.push_back(c4);
	v.push_back(c5);
	Complex c;
	Complex result2=accumulate(v.begin(),v.end(),c,plus());
	cout<
输出:
3+3i
15+15i

accumulate() 为累加数值函数,将在后面的章节介绍。
可以发现,当只有两个复数相加时,使用内建函数对象并不方便,不如直接用 Complex c=c1+c2 来的方便,但是当它与 STL 的算法结合起来时 ,它的优势就体现出来了,如程序中累加5个复数,如果是100个,10000个呢?所以单独地看 STL 的数据结构可能会觉得它臃肿,但是当它与 STL 的算法结合起来时,将所向披靡。

函数适配器 bind1st bind2nd not1 not2 mem_fun mem_fun_ref ptr_fun

顾名思义,函数适配器能够将函数(函数对象)做一定的转换,编程另一个功能大致的函数(函数对象)。就像生活中的电源适配器一样,将三口插座通过电源适配器变成两口插座方便使用。比如你想要计算数组 int a[] 中小于 4 的个数,那么你会用到 less() 函数,但是如何体现要比较的 4 呢?这时候用函数适配器就能很好的解决。另外,STL 中绝大多数算法归根结底是调用功能类中重载的 operator() 运算符来实现的,然而功能类中还有很多普通的成员函数,STL 本身不能直接调用,需要通过函数适配器进行转换之后才能调用。

C++98 函数适配器分类表:
C++ STL 基础及应用(7) 函数对象(仿函数)_第2张图片

函数适配器使用如下:
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

class student
{
private:
	string name;
	int number;
public:
	student(string name,int number)
	{
		this->name=name;
		this->number=number;
	}
	bool show()
	{
		cout<7;
}
bool g(int a,int b)
{
	return a>b;
}

int main()
{
	int a[]={10,9,8,7,6,5,4,3,2,1};
	int n1=count_if(a,a+sizeof(a)/sizeof(int),bind1st(less(),4));
	int n2=count_if(a,a+sizeof(a)/sizeof(int),bind2nd(less(),4));
	int n3=count_if(a,a+sizeof(a)/sizeof(int),not1(bind2nd(less(),4)));

	sort(a,a+sizeof(a)/sizeof(int),not2(less()));
	copy(a,a+sizeof(a)/sizeof(int),ostream_iterator(cout," "));

	int n4=count_if(a,a+sizeof(a)/sizeof(int),ptr_fun(f));
	int n5=count_if(a,a+sizeof(a)/sizeof(int),bind2nd(ptr_fun(g),8));

	student s1("秦始皇",1001);
	student s2("乾隆",1002);
	vector v1;
	v1.push_back(s1);
	v1.push_back(s2);

	vector v2;
	v2.push_back(&s1);
	v2.push_back(&s2);

	for_each(v1.begin(),v1.end(),mem_fun_ref(&student::show));
	for_each(v2.begin(),v2.end(),mem_fun(&student::show));

	cout<输出:
10 9 8 7 6 5 4 3 2 1
name:秦始皇     number:1001
name:乾隆       number:1002
n1=6 n2=3 n3=7 n4=3 n5=2

程序解释如下:
(1)less() 原型为:
template
struct less : public binary_function<_Ty, _Ty, bool>
{
bool operator()(const _Ty& _Left, const _Ty& _Right) const
{
return (_Left < _Right);
}
};
即该函数比较两个参数,若第一个参数小于第二个参数则返回 true。bind1st(less(),4))将 less() 函数的第一个参数绑定为 4,将其适配成为一个新的函数,count_if()函数将迭代器范围内的元素传入该新的函数并且计数返回几个 true,因此该行语句的意义即为计数数组中比 4 的元素个数,5~10一共6个数,所以 n1=6。
(2)bind2nd(less(),4)) 即是将 less() 的第二个参数绑定为4,那么该行语句实际计数的是小于 4 的数,因此 n2=3。
(3)not1() 对一元函数结果取反(返回 true 时改为 false,反之则反),因此 n3 即为大于等于 4 的元素个数,n3=7。
(4)not2() 对二元函数结果取反,因此 sort(a,a+sizeof(a)/sizeof(int),not2(less())) 即为按从大到小排序。
(5)ptr_fun() 为普通函适配器,使一个普通函数能够被 STL 中的函数、算法调用。如代码中的 bind2nd(ptr_fun(g),8)) 若写成 bind2nd(g,8)) 则为错误,因为 STL 不接受未经 ptr_fun() 转换的函数。
(6)mem_fun_ref、mem_fun 是类的成员函数适配器,两者的区别在于:若集合是基于对象的,形如 vector,则用 mem_fun_ref;若集合是基于对象指针的,形如 vector,则用 men_fun。
注意!取成员函数地址时需要加上 "&" 符号不能省略,代码接近最后两行 for_each() 里面 (&student::show) 写成 (student::show) 则编译器会报错。

C++11标准下函数对象使用详解

C++11 函数适配器表:

C++ STL 基础及应用(7) 函数对象(仿函数)_第3张图片

系统函数对象、mem_fn() 与 mem_fun() 使用类似,not1()/not2() 与C++98标准里的 not1()/not2() 使用类似,就都不再赘述了。

function

我们知道,在C++中,可调用实体主要包括函数,函数指针,函数引用,可以隐式转换为函数指定的对象,或者实现了opetator()的对象(即函数对象)。C++11中,新增加了一个std::function对象,std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(我们知道像函数指针这类可调用实体,是类型不安全的)。

bind()

C++98中,有两个函数 bind1st() 和 bind2nd(),它们分别可以用来绑定函数对象的第一个和第二个参数,它们都是只可以绑定一个参数。各种限制,使得 bind1st() 和 bind2nd() 的可用性大大降低。C++11中,提供了std::bind(),它绑定的参数的个数不受限制,绑定的具体哪些参数也不受限制,由用户指定,这个 bind() 才是真正意义上的绑定,有了它,bind1st() 和 bind2nd() 就没啥用武之地了,因此在 C++11 中不推荐使用 bind1st() 和 bind2nd() 了。

function/bind()使用如下:

#include 
#include 
using namespace std;

int Myminus(int x,int y)
{
	return x-y;
}

class MyNum
{
private:
	int a,b;
public:
	MyNum(){a=4;b=5;}
	MyNum(int a,int b)
	{
		this->a=a;
		this->b=b;
	}
	int Add(){return a+b;}
	int Cal(int c,int d){return a+b+c+d;}
};

int main()
{
	function f1=bind(Myminus,placeholders::_1,placeholders::_2);
	int r1=f1(3,2);
	cout< f2=bind(Myminus,placeholders::_1,4);
	int r2=f2(7);
	cout< f3=bind(Myminus,7,5);
	int r3=f3();
	cout< f4=bind(Myminus,placeholders::_2,placeholders::_1);
	int r4=f4(7,4);
	cout< f5=bind(&MyNum::Add,num5);
	int r5=f5();
	cout< f6=bind(&MyNum::Add,placeholders::_1);
	int r6=f6(num6);
	cout< f7=bind(&MyNum::Cal,placeholders::_1,placeholders::_2,placeholders::_3);
	int r7=f7(num7,3,4);
	cout<
输出:
1
3
2
-3
9
7
10

程序解释如下:
(1)placeholders 为名词空间,本身也位于名词空间 std 中,std::placeholders::_1 表示一个参数占位符,bind(Myminus,placeholders::_1,placeholders::_2) 表示适配后的函数有两个参数,第一个参数对应原函数的第一个参数,第二个参数对应原函数的第二个参数,bind(Myminus,placeholders::_1,4) 表示适配后的函数只有一个参数,第一个参数对应原函数的第一个参数,第二个参数绑定为4。bind(Myminus,placeholders::_2,placeholders::_1) 很明显就是表示适配后的函数有两个参数,第一个参数对应原函数的第二个参数,第二个参数对应原函数的第一个参数。
(2)适配一个函数之后,要用 function 对象来接收,如 function,表示用形如 int(int,int) 的函数构造的 function 对象,这里值得一提的是,如果为了简便,可以直接使用 auto 关键字来做类型推导,比如主函数中的第一句可以用下面这句等价替换: auto f1=bind(Myminus,placeholders::_1,placeholders::_2); 后面的语句一样全部可以用 auto。但使用 auto 可能会造成代码阅读上的障碍,看个人喜好抉择。
(3)在适配类成员函数时,bind() 函数的第一个参数为该类的一个对象,若不用参数占位符时,需要直接传入这个对象。仔细查看 f6 和 f7 的使用就能够理解。

ref()/cref()

这两个函数用来将参数绑定为引用,其中 ref() 将参数绑定为普通引用,cref()将参数绑定为 const 引用,直接看下面实例:
#include      
#include    
using namespace std;

void f(int &a,int &b,const int &c)
{
	cout<<"运行函数中:	"<
输出:
运行函数前:     6       7       8
运行函数中:     1       7       8
运行函数后:     6       8       8

程序解释:
(1) a 为 int 形,在 bind() 中是传值,因此传进 f() 时里面实际操作的是 a 的拷贝的引用,因此在"函数运行中"输出的是被改变的拷贝值:1, 而不是真正的 a,真正的 a 一直是 6。
(2) b 为 int 形,ref(b) 则是 b 的引用,bind() 以此为参数,因此在"在函数运行中"输出的是真正的 b 而不是拷贝,而 f() 也就能够改变 b 的值,所以最后 b 执行了 ++ 操作变成了 8。
(3) c 为 int 形,cref(c) 则是 c 的 const 引用,即 const int& 类型,因此无法被改变,程序中有一句 //++c,若把注释去掉则编译器会报错,因为 ++ 试图改变一个 const 值。

关于 C++98/C++11 函数对象相关内容详细介绍:

http://www.cplusplus.com/reference/functional/

关于 C++98/C++11 函数对象的相关讨论:

http://stackoverflow.com/questions/22386882/why-unary-function-binary-function-is-removed-from-c11
http://www.zhihu.com/question/30558389

你可能感兴趣的:(C++,C++,STL,STL,C++,函数对象,仿函数,functional)