C++ STL初识(详解+示例代码):仿函数


函数调用需要使用"()",这个“()”叫做 函数调用用运算符。在面向对象编程世界里,一切皆为对象,对象是程序的基本单元。那么这个可调用的函数名,被称为 可调用对象

在C++中除了函数可以调用之外,重载了operator()的类,也是可以调用的,也可成为可调用对象

C++中的可调用对象有以下几种:

– 函数(function)

– 函数指针(function pointer)

– 仿函数(Functor)

– lambda表达式

– bind 函数封装的函数对象

函数和函数指针不用多说,这一节我们先从仿函数开始把~

仿函数(函数对象)

什么是仿函数

仿函数又称为函数对象是一个能行使函数功能的类,仿函数是定义了一个含有operator()成员函数的对象,可以视为一个一般的函数,只不过这个函数功能是在一个类中的运算符operator()中实现,是一个函数对象,它将函数作为参数传递的方式来使用。

写一个简单类,除了维护类的基本成员函数外,只需要重载 operator() 运算符这样既可以免去对一些公共变量的维护,也可以使重复使用的代码独立出来,以便下次复用。

STL 中也大量涉及到仿函数,有时仿函数的使用是为了函数拥有类的性质,以达到安全传递函数指针、依据函数生成对象、甚至是让函数之间有继承关系、对函数进行运算和操作的效果。比如 STL 中的容器 set 就使用了仿函数 less ,而 less 继承的 binary_function,就可以看作是对于一类函数的总体声明,这是函数做不到的。

我们通过一个示例来引入什么是仿函数

首先,我们先来看这么一个小李子:

struct Foo
{
	void operator()()
	{
		cout << __FUNCTION__ << endl;
	}
};

int main()
{
	Foo a;
	//定义对象调用
	a.operator()();
	//直接通过对象调用
	a();
	//通过临时对象调用
	Foo()();
}

我们在一个结构体(或者说在类)中,重载了一个括号运算符,那么我们有几种方法可以调用这个函数呢?

  • 第一种方法: 定义一个结构体对象,然后显式访问这个函数:operator() ,注意函数调用,后面再加上一个括号。
  • 第二种方法:通过结构体对象直接访问函数。
  • 第三种方法:通过结构体匿名对象,访问并调用函数。

在这里插入图片描述

这三种方法我们都可以调用这个重载运算符函数,我们首先了解这三种方法,这样有什么用呢,我们接下来的调用仿函数会用到。

仿函数示例

问题:

分别统计一个vector 中每个元素等于数字3,大于数字3,小于数字3的次数:

普通写法

如果我们没有学过仿函数,并且也不知道C++ STL算法,我们或许会这样用:

int equal_count(const vector<int>::iterator& a, const vector<int>::iterator& b,
	const int& val)
{
	int count_num = 0;
	for (auto it = a; it != b; it++)
	{
		if (*it == val)
		{
			count_num++;
		}
	}
	return count_num;
}

int greater_count(const vector<int>::iterator& a, const vector<int>::iterator& b,
	const int& val)
{
	int count_num = 0;
	for (auto it = a; it != b; it++)
	{
		if (*it > val)
		{
			count_num++;
		}
	}
	return count_num;
}

int less_count(const vector<int>::iterator& a, const vector<int>::iterator& b,
	const int& val)
{
	int count_num = 0;
	for (auto it = a; it != b; it++)
	{
		if (*it < val)
		{
			count_num++;
		}
	}
	return count_num;
}
int main()
{
	vector<int> a{ 1,2,3,4,4,5,6,8,7};
	int num1 = equal_count(a.begin(), a.end(), 3);
	cout << "等于3:" << num1 << "个" << endl;
	int num2 = greater_count(a.begin(), a.end(), 3);
	cout << "大于3:" << num2 << "个" << endl;
	int num3 = less_count(a.begin(), a.end(), 3);
	cout << "小于3:" << num3 << "个" << endl;
}

我们创建了三个函数,分别以不同的功能,调用这三个不同的函数,这样确实简单直接,而且运行结果正确:
在这里插入图片描述
但是我们不禁思考一下,这样的函数会不会过于长了,他们不仅长,而且他们的功能都是类似的,我们为何要单独在写三个函数呢?不仅我觉得烦,我觉得你们往下翻发现这么长应该也挺烦的,所以,我们尝试修改一下,我们能否用一个函数的主体,然后写三个子函数,来传入主体函数?
可以的,这就是函数的可调用对象写法:

进阶写法(可调用对象)

利用可调用对象的写法:

/*
可调用对象
*/
template <class FUN>
int count_if(const vector<int>::iterator& a, const vector<int>::iterator& b,FUN func)
{
	int count_num = 0;
	for (auto it = a; it != b; it++)
	{
		if (func(*it))
		{//传进去的是值
			count_num++;
		}
	}
	return count_num;
}
bool _equal(int a)
{
	return a == 3;
}
bool _greater(int a)
{
	return a > 3;
}
bool _less(int a)
{
	return a < 3;
}
int main()
{
	vector<int> a{ 1,2,3,4,4,5,6,8,7};
	int num1 = count_if(a.begin(), a.end(), _equal);
	cout << num1 << endl;
	int num2 = count_if(a.begin(), a.end(), _greater);
	cout << num2 << endl;
	int num3 = count_if(a.begin(), a.end(), _less);
	cout << num3 << endl;

这样,我们的函数就看起来清晰多了,我们使用了一个模板参数用作函数指针,再写三个简单的子函数,在主体函数中调用三个简单的子函数,当条件为true时,便可以起到统计次数的作用。而且在可调用函数的版本中,我们还可以使用lamdba表达式的形式:

int num1 = count_if(a.begin(), a.end(), [](const int& data) {return data == 3; });
cout << num1 << endl;
int num2 = count_if(a.begin(), a.end(), [](const int& data) {return data > 3; });
cout << num2 << endl;
int num3 = count_if(a.begin(), a.end(), [](const int& data) {return data < 3; });
cout << num3 << endl;

显然可调用对象这个方法,比第一种有着极大的提升,我们不用写三个很长的函数,而是根据他们的共性:都具有统计的功能,只改变子函数的功能,就可以完成这些功能。 我们的代码看起来漂亮多了,但是还有没有办法可以优化呢(事实上,这个方法已经够完美了),可以的,接下来使用我们的仿函数的写法:

高阶写法(仿函数)

template <class FUN>
int count_if(const vector<int>::iterator& a, const vector<int>::iterator& b,FUN func)
{
	int count_num = 0;
	for (auto it = a; it != b; it++)
	{
		if (func(*it))
		{//传进去的是值
			count_num++;
		}
	}
	return count_num;
}

/*
仿函数
*/
struct Equal
{
	int val;
	Equal(const int& val) :val(val) {}
	bool operator()(const int& a)
	{
		return a == val;
	}
};

struct Greater
{
	int val;
	Greater(const int& val) :val(val) {}
	bool operator()(const int& a)
	{
		return a > val;
	}

};

struct Less
{
	int val;
	Less(const int& val) :val(val) {}
	bool operator()(const int& a)
	{
		return a < val;
	}
};
int main()
{
	vector<int> a{ 1,2,3,4,4,5,6,8,7};
	/*
	仿函数 传递数值的方式
	*/
	Less less(10;	//可以创建对象来调用
	int num1 = count_if(a.begin(), a.end(), Equal(4));	//匿名对象
	cout << num1 << endl;
	int num2 = count_if(a.begin(), a.end(), Greater(3));	//匿名对象
	cout << num2 << endl;
	int num3 = count_if(a.begin(), a.end(), less);		//临时对象
	cout << num3 << endl;
}

在这里插入图片描述

这就是我们的仿函数的写法了,表面上看也挺长的,看起来还不如第二种方式,直接在函数中调用的方式,但是,你有没有发现第二种方式,有一个很麻烦的缺点:我们每次修改数值,比如我们想让他统计大于5,大于10,等于10,小于20. …等等等等,这些不同的数值会怎么办。请回顾一下刚才第二种方式是如何处理的:我们在一个函数中就已经把数值写死了,或者说我们在函数调用时,只会看到调用的函数名称(可调用对象,传递给函数形参的是函数名称),并不会看到实际比较的数值。

但是,可以看到仿函数:我们可以任意指定是大于,小于,还是等于任意数值,并且我们重载了括号运算符,我们可以看到数值,回想起来,我们在最开始的例子:调用类中重载括号运算符的例子。
我们可以使用像上述示例中,类名+数值,指的是创建一个匿名对象;我们也可以创建一个类对象来进行调用。

对仿函数的思考

我们应该熟悉并且使用这种方式,因为在我们的STL阶段,仿函数非常常见,而且标准库中也有非常多的仿函数:

#includ 头文件
在这里插入图片描述

包括小于大于,乘法,除法,加法,减法,都具有指定的仿函数与之功能对应,我们可以很轻松的使用他们,比较常见的是在算法中,直接操纵容器,算法我们以后再说。

我们可以看到仿函数的共性:都具有重载的括号运算符,都具有特定的功能。
比如我们刚刚实现的大于等于,小于等操作,在标准库中都有直接定义的版本,就如同上图所示,他们在 #include 中定义,我们可以在任何算法中直接使用:

实现我们刚才vector容器的排序

sort(a.begin(), a.end(),less<int>());
	for (auto& x : a)
	{
		cout << x << " ";
	}

我们调用了仿函数的less,调用这个仿函数,就实现了对容器的排序:
在这里插入图片描述

仿函数的优点

如果可以用仿函数实现,那么你应该用仿函数,而不要用CallBack。原因在于:

  • 仿函数可以不带痕迹地传递上下文参数。而CallBack技术通常使用一个额外的void*参数传递。这也是多数人认为CallBack技术丑陋的原因。

  • 仿函数技术可以获得更好的性能,这点直观来讲比较难以理解。

仿函数作用

仿函数通常有下面四个作用:

  • 作为排序规则,在一些特殊情况下排序是不能直接使用运算符<或者>时,可以使用仿函数。
  • 作为判别式使用,即返回值为bool类型。
  • 同时拥有多种内部状态,比如返回一个值得同时并累加。
  • 作为算法for_each的返回值使用。
posted @ 2022-09-22 22:45  hugeYlh  阅读(906)  评论(0编辑  收藏  举报  来源