入门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的返回值使用。