1. 程式人生 > >仿函式、謂詞、介面卡、內建物件配合常見演算法的使用

仿函式、謂詞、介面卡、內建物件配合常見演算法的使用

在前面幾篇中,已經有過好幾次的仿函式結合遍歷、查詢等演算法的使用了,這邊再進行歸納一下。

仿函式(函式物件)

前面已經說過了,仿函式的特點有:

  • 是個類,不是個函式,可以說成是函式物件。
  • 過載()。
  • 內部可以宣告一些變數,儲存狀態,如宣告一個整型變數記錄呼叫次數。
  • 仿函式通常不宣告建構函式和解構函式,因此構造和析構時不會發生問題,避免了函式呼叫時的執行問題。
  • 可內聯編譯。 使用函式指標就不大可能了,因為不知道函式指標的具體內容到底適不適合內聯。
  • 模板函式使得通用性更強。

一元、二元仿函式

所以一元二元,就是指過載函式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

以上。請指教。