仿函式、謂詞、介面卡、內建物件配合常見演算法的使用
在前面幾篇中,已經有過好幾次的仿函式結合遍歷、查詢等演算法的使用了,這邊再進行歸納一下。
仿函式(函式物件)
前面已經說過了,仿函式的特點有:
- 是個類,不是個函式,可以說成是函式物件。
- 過載()。
- 內部可以宣告一些變數,儲存狀態,如宣告一個整型變數記錄呼叫次數。
- 仿函式通常不宣告建構函式和解構函式,因此構造和析構時不會發生問題,避免了函式呼叫時的執行問題。
- 可內聯編譯。 使用函式指標就不大可能了,因為不知道函式指標的具體內容到底適不適合內聯。
- 模板函式使得通用性更強。
一元、二元仿函式
所以一元二元,就是指過載函式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
以上。請指教。