在前面几篇中,已经有过好几次的仿函数结合遍历、查找等算法的使用了,这边再进行归纳一下。
仿函数(函数对象)
前面已经说过了,仿函数的特点有:
- 是个类,不是个函数,可以说成是函数对象。
- 重载()。
- 内部可以声明一些变量,保存状态,如声明一个整型变量记录调用次数。
- 仿函数通常不声明构造函数和析构函数,因此构造和析构时不会发生问题,避免了函数调用时的运行问题。
- 可内联编译。 使用函数指针就不大可能了,因为不知道函数指针的具体内容到底适不适合内联。
- 模板函数使得通用性更强。
一元、二元仿函数
所以一元二元,就是指重载函数operator()的参数有几个,一个称为一元,两个称为二元。
仿函数作为一种 策略 使用,配合算法能够让我们获得想要的结果。
可能 策略 这个词初看不能理解,下面就看看例子吧。
在看例子之前先看看for_each的原型:
// 返回类型为仿函数类型,接受两个指针和一个仿函数
Function for_each (InputIterator first, InputIterator last, Function fn);
现在我要使用for_each算法,遍历vector容器,且还要输出它。
//仿函数
class MyPrint
{
public:
MyPrint() {m_Num = 0;}
int m_Num;
public:
//重载,策略为输出,然后次数+1
void operator() (int num)
{
cout << num << " ";
m_Num++;
}
};
int main()
{
vector<int> v;
for (int i = 0; i < 10; ++i)
{
v.push_back(i);
}
//调用
MyPrint my = for_each(v.begin(), v.end(), MyPrint());
cout << endl << my.m_Num << endl;
return 0;
}
看看输出结果:
可以看到我们已经正确输出了容器中的元素,且内部保存的状态也得以正确保存。看我们的仿函数,策略就是输出元素。
谓词
谓词是指普通函数或重载的operator()返回值是bool类型的函数对象(仿函数)。如果operator接受一个参数,那么叫做一元谓词,如果接受两个参数,那么叫做二元谓词,谓词可作为一个判断式。这个判断式,也可看做策略。
谓词和仿函数有几分相似,只是返回值确定为bool型的。用法也几乎一样。
谓词可分一元谓词和二元谓词。 根据各自策略不同,比如二元谓词可以用来做降序排列,这在前几篇都有讲过。只要规定一个排序规则即可。如:
class MyCmp
{
public:
bool operator()(int a,int b)
return a > b;
};
那一元排序也有自己的用途,比如我想在容器中找到大于5的数字:
class MyCmp
{
public:
bool operator()(int v)
return v > 5;
};
int main()
{
vector<int> v;
for (int i = 0; i < 10;i ++)
{
v.push_back(i);
}
vector<int>::iterator it = find_if(v.begin(), v.end(), GreaterThenFive());
if (it == v.end())
cout << "没有找到" << endl;
else
cout << "找到了: " << *it << endl;
return 0;
}
这里我们使用了find_if这个标准库算法,原型为:
InputIterator find_if (InputIterator first, InputIterator last, UnaryPredicate pred);
返回值为迭代器,前两个参数为起始和终止迭代器,最后一个参数即我们的策略:一元谓词。这边指定只能用一元谓词。
内建函数对象
你可能会想,那我们每次要改变策略,是不是每次都要写一个仿函数(函数对象),来得到我们想要的结果?那会不会太麻烦了点。其实STL内建了一些函数对象。分为:算数类函数对象,关系运算类函数对象,逻辑运算类仿函数。我们要用这些函数要引入头文件#include
下面介绍常用的一些内建函数对象,取几个来讲,用法都一样,根据自己想要的策略选择即可。
算术类函数对象
template<class T> T plus<T> //加法仿函数
template<class T> T minus<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> //取反仿函数
以上除了negate是一元运算,其它都是二元的。
举个例子:
#include <functional>
...
int main()
{
negate<int> n;
cout << n(10) << endl; // 结果为-10
plus<int> p;
cout << p(10,36) << endl; // 结果为46
divides<int> d;
cout << d(36,10) << endl; // 结果为3
return 0;
}
其它不再概述,一样。
关系类函数对象
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> //小于等于
这里的每一个函数对象都是一个二元仿函数。
我们之前用的升序排序,现在完全可以用内置函数对象替换:
int main()
{
vector<int> v;
for (int i = 0; i < 10; ++i)
v.push_back(i);
sort(v.begin(), v.end(), greater<int>());
auto it = v.begin();
for (; it != v.end(); ++it)
cout << *it << " ";
return 0;
}
我们把原本的仿函数改为了内建函数对象:
greater<int>()
这样就能实现从大到小排序了。
逻辑类函数对象
template<class T> bool logical_and<T> //逻辑与
template<class T> bool logical_or<T> //逻辑或
template<class T> bool logical_not<T> //逻辑非
除了logical_not是一元以外,均是二元。使用方式与上一致,不再累述。
适配器
现在着重介绍最难的适配器。
函数适配器bind1st和bind2nd
是什么?
VS的文档里面是这样描述bind2nd的:
一个辅助模板函数,它创建一个适配器,通过将二元函数的第二个参数绑定为指定值,将二元函数对象转换为一元函数对象。
也就是说我们使用bind2nd的目的在于:
对于只能使用一元仿函数的地方,我们可以用bind2nd绑定第二个参数,这样我们将二元仿函数就转换成一元仿函数,不过是绑定了个参数进去而已。
bind1st和bind2nd区别仅仅在于,前者是将绑定的参数作为二元仿函数的第一个参数,后者是将绑定的参数作为二元仿函数的第二个参数。
如何使用?
三步骤:
- 绑定一个参数。
- 仿函数(函数对象)要继承binary_function<参数1类型,参数2类型,仿函数返回类型>。
- 仿函数要加const修饰,防止改变任何东西。
下面举个例子。我们要用for_each遍历一个容器,每次遍历都输出结果为对应元素值加上一个我指定的数的和。for_each只能传递一元仿函数,所以,我要用bind2nd将我指定的数传递进去:
// 三步骤之继承,全看operator参数类型和返回值
class MyPrint2 :public binary_function<int, int, void>
{
public:
// 三步骤之const
void operator()(int v1, int v2) const
{
cout << "v1 = " << v1 << " v2 = " << v2 << " v1+v2 = " << (v1 + v2) << endl;
}
};
void test4()
{
vector<int>v;
for (int i = 0; i < 5; i++)
{
v.push_back(i);
}
cout << "请输入起始值:" << endl;
int x;
cin >> x;
// 三步骤之绑定
for_each(v.begin(), v.end(), bind1st(MyPrint2(), x));
cout << endl;
// 三步骤之绑定
for_each(v.begin(), v.end(), bind2nd( MyPrint2(),x ));
}
上面我用了两个for_each,一个使用bind1st绑定,另一个是用bind2nd绑定。结果如下:
可以看到,bind1st是把绑定值作为第一个参数,元素值作为第二个参数,而bind2nd正好相反。
取反适配器not1和not2
取反适配器not1就是一元仿函数取反,not2就是二元仿函数取反。
倘若有个仿函数是这样的,它想找到大于5的数:
class GreaterThen5{
public:
bool operator()(int a)
{
return a > 5;
}
};
int main()
{
vector<int>v;
for (int i = 0; i < 10; i++)
v.push_back(i);
auto it = find_if(v.begin(), v.end(), GreaterThen5());
if(it!=v.end())
cout << "值为:" << *it << endl;
return 0;
}
这样我们能正确找到,但是现在我们突然改了方案,不能修改仿函数的基础上要改成找到小于5的数,这个时候取反适配器就发挥了用途。
还是老规则,三步骤,一步不能少:
// 继承和const
class GreaterThen5 : public unary_function<int,bool>
{
public:
bool operator()(int a) const
{
return a > 5;
}
};
int main()
{
vector<int>v;
for (int i = 0; i < 10; i++)
v.push_back(i);
// 使用not1
auto it = find_if(v.begin(), v.end(), not1(GreaterThen5()));
if(it!=v.end())
cout << "值为:" << *it << endl;
return 0;
}
其实还可以写成:
not1(bind2nd(Great<int>,5));
其中缘由就自己领悟吧哈哈。
函数指针适配器ptr_fun
上面介绍了仿函数的适配使用,现在就介绍普通函数的适配使用方法。
void MyPrint3(int a, int b)
{
cout << "和为:" << a + b << endl;
}
void test7()
{
vector<int>v;
for (int i = 0; i < 6; i++)
v.push_back(i);
for_each(v.begin(),v.end(),bind2nd(ptr_fun(MyPrint3),100));
}
注意点:
- 使用了bind2nd却不用遵循三步骤。
- ptr_fun(MyPrint3),直接把函数名称传进去就是一个函数对象了。
成员函数适配器
上面讲的是普通的野生函数,那如果是在类里面的成员函数呢?关键字:mem_fun_ref 。
class Person
{
public:
string _strName;
int _iAge;
Person(string strName, int iAge){
_strName = strName;
_iAge = iAge;
}
void ShowInfo(){
cout << "姓名:" << _strName << " 年龄:" << _iAge << endl;
//_strName = "hello";
}
};
int main()
{
vector <Person>v;
v.push_back(Person("aaa", 10));
v.push_back(Person("ccc", 30));
v.push_back(Person("bbb", 20));
v.push_back(Person("ddd", 40));
for_each(v.begin(), v.end(), mem_fun_ref(&Person::ShowInfo));
return 0;
}
记得要在mem_fun_ref里面取地址哦。
倘若我们现在vector容器中存放的是指针类型:
vector<Person *> v;
那我们只需要将mem_fun_ref改为:
mem_fun
以上。请指教。