仿函数、谓词、适配器、内建对象配合常见算法的使用

在前面几篇中,已经有过好几次的仿函数结合遍历、查找等算法的使用了,这边再进行归纳一下。

仿函数(函数对象)

前面已经说过了,仿函数的特点有:

  • 是个类,不是个函数,可以说成是函数对象。
  • 重载()。
  • 内部可以声明一些变量,保存状态,如声明一个整型变量记录调用次数。
  • 仿函数通常不声明构造函数和析构函数,因此构造和析构时不会发生问题,避免了函数调用时的运行问题。
  • 可内联编译。 使用函数指针就不大可能了,因为不知道函数指针的具体内容到底适不适合内联。
  • 模板函数使得通用性更强。

一元、二元仿函数

所以一元二元,就是指重载函数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区别仅仅在于,前者是将绑定的参数作为二元仿函数的第一个参数,后者是将绑定的参数作为二元仿函数的第二个参数。

如何使用?

三步骤:

  1. 绑定一个参数。
  2. 仿函数(函数对象)要继承binary_function<参数1类型,参数2类型,仿函数返回类型>
  3. 仿函数要加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

以上。请指教。