C++函數物件詳解(附帶範例)

2020-07-16 10:04:26
如果一個類將()運算子過載為成員函數,這個類就稱為函數物件類,這個類的物件就是函數物件。函數物件是一個物件,但是使用的形式看起來像函數呼叫,實際上也執行了函數呼叫,因而得名。

下面是一個函數物件的例子。
#include <iostream>
using namespace std;

class CAverage
{
public:
    double operator()(int a1, int a2, int a3)
    {  //過載()運算子
        return (double)(a1 + a2 + a3) / 3;
    }
};

int main()
{
    CAverage average;  //能夠求三個整數平均數的函數物件
    cout << average(3, 2, 3);  //等價於 cout << average.operator(3, 2, 3);
    return 0;
}
程式的輸出結果是:
2. 66667

()是目數不限的運算子,因此過載為成員函數時,有多少個引數都可以。

average 是一個物件,average(3, 2, 3) 實際上就是 average.operator(3, 2, 3),這使得 average 看上去像函數的名字,故稱其為函數物件。

函數物件應用範例1:在 accumulate 演算法中的應用

STL 中有以下實現“累加”功能的演算法(函數模板):

template <class InIt, class T, class Pred>
T accumulate(InIt first, InIt last, T val, Pred op);

該模板的功能是對 [first, last) 中的每個疊代器 I 執行 val = op(val, *I),返回最終的 val。在 Dev C++ 中,numeric 標頭檔案中 accumulate 的原始碼如下:
template <class InIt, class T, class Pred>
T accumulate(InIt first, Init last, T init, Pred op)
{
    for (; first != last; ++first)
        init = op(init, *first);
    return init;
};
此模板被範例化後,op(init, *first)必須要有定義,則 op 只能是函數指標或者函數物件。因此呼叫該 accmulate 模板時,形參 op 對應的實參只能是函數名、函數指標或者函數物件。

下面的程式通過 accumulate 模板求一個 vector 中元素的平方和,其中用到了函數物件。
#include <iostream>
#include <vector>
#include <numeric> //accumulate 在此標頭檔案定義
using namespace std;
template <class T>
void PrintInterval(T first, T last)
{ //輸出區間[first,last)中的元素
    for (; first != last; ++first)
        cout << *first << " ";
    cout << endl;
}
int SumSquares(int total, int value)
{
    return total + value * value;
}
template<class T>
class SumPowers
{
private:
    int power;
public:
    SumPowers(int p) :power(p) { }
    const T operator() (const T & total, const T & value)
    { //計算 value的power次方,加到total上
        T v = value;
        for (int i = 0; i < power - 1; ++i)
            v = v * value;
        return total + v;
    }
};
int main()
{
    const int SIZE = 10;
    int a1[] = { 1,2,3,4,5,6,7,8,9,10 };
    vector<int> v(a1, a1 + SIZE);
    cout << "1) "; PrintInterval(v.begin(), v.end());
    int result = accumulate(v.begin(), v.end(), 0, SumSquares);
    cout << "2) 平方和:" << result << endl;
    result = accumulate(v.begin(), v.end(), 0, SumPowers<int>(3));
    cout << "3) 立方和:" << result << endl;
    result = accumulate(v.begin(), v.end(), 0, SumPowers<int>(4));
    cout << "4) 4次方和:" << result;
    return 0;
}
程式的輸出結果如下:
1)1 2 3 4 5 6 7 8 9 10
2)平方和:385
3)立方和3025
4)4次方和:25333

第 37 行,第四個引數是 SumSquares 函數的名字。函數名字的型別是函數指標,因此本行將 accumulate 模板範例化後得到的模板函數定義如下:
int accumulate(vector <int>::iterator first, vector <int>::iterator last, int init, int(*op)(int, int))
{
    for (; first != last; ++first)
        init = op(init, *first);
    return init;
}
形參 op 是一個函數指標,而op(init, *first)就呼叫了指標 op 指向的函數,在第 37 行的情況下就是函數 SumSquares。

第 39 行,第四個引數是 SumPowers<int>(3)。SumPowers 是類別範本的名字,SumPowers<int> 就是類的名字。類的名字後面跟著建構函式的參數列,就代表一個臨時物件。因此 SumPowers<int>(3) 就是一個 SumPowers<int> 類的臨時物件。

編譯器在編譯此行時,會將 accumulate 模板範例化成以下函數:
int accumulate(vector<int>::iterator first, vector<int>::iterator last, int init, SumPowers<int> op)
{
    for (; first != last; ++first)
        init = op(init, *first);
    return init;
}
形參 op 是一個函數物件,而op(init, *first)等價於:

op.operator()(init, *first);

即呼叫了 SumPowers<int> 類的 operator() 成員函數。

對比 SumPowers 和 SumSquares 可以發現,函數物件的 operator() 成員函數可以根據物件內部的不同狀態執行不同操作,而普通函數就無法做到這一點。因此函數物件的功能比普通函數更強大。

函數物件應用範例2:在sort演算法中的應用

STL 中的排序模板 sort 能將區間從小到大排序。sort 演算法有兩個版本。第一個版本的原型如下:

template <class_Randlt>
void sort(_Randlt first, _RandIt last);

該模板可以用來將區間 [first, last) 中的元素從小到大排序,要求 first、last 是隨機存取疊代器。元素比較大小是用<進行的。如果表示式a<b的值為 true,則 a 排在 b 前面;如果a<b的值為 false,則 b 未必排在 a 前面,還要看b<a是否成立,成立的話 b 才排在 a 前面。要使用這個版本的 sort 演算法,待排序的物件必須能用<運算子進行比較。

sort 演算法第二個版本的原型如下:

template <class_Randlt, class Pred>
void sort(_Randlt first, _RandIt last, Pred op);

這個版本和第一個版本的差別在於,元素 a、b 比較大小是通過表示式op(a, b)進行的。如果該表示式的值為 true,則 a 比 b 小;如果該表示式的值為 false,也不能認為 b 比 a 小,還要看op(b, a)的值。總之,op 定義了元素比較大小的規則。下面是一個使用 sort 演算法的例子。
#include <iostream>
#include <algorithm>  //sort演算法在此標頭檔案中定義
using namespace std;
template <class T>
void Printlnterva1(T first, T last)
{  //用以輸出 [first, last) 區間中的元素
    for (; first != last; ++first)
        cout << *first << " ";
    cout << endl;
}
class A
{
public:
    int v;
    A(int n) : v(n) {}
};
bool operator < (const A & a1, const A & a2)
{  //過載為 A 的 const 成員函數也可以,過載為非 const 成員函數在某些編譯器上會出錯
    return a1.v < a2.v;
}
bool GreaterA(const A & a1, const A & a2)
{  //v值大的元素作為較小的數
    return a1.v > a2.v;
}
struct LessA
{
    bool operator() (const A & a1, const A & a2)
    {  //v的個位數小的元素就作為較小的數
        return (a1.v % 10) < (a2.v % 10);
    }
};
ostream & operator << (ostream & o, const A & a)
{
    o << a.v;
    return o;
}
int main()
{
    int a1[4] = { 5, 2, 4, 1 };
    A a2[5] = { 13, 12, 9, 8, 16 };
    sort(a1, a1 + 4);
    cout << "1)"; Printlnterva1(a1, a1 + 4);  //輸出 1)1 2 4 5
    sort(a2, a2 + 5);  //按v的值從小到大排序
    cout << "2)"; Printlnterva1(a2, a2 + 5);  //輸出 2)8 9 12 13 16
    sort(a2, a2 + 5, GreaterA);  //按v的值從大到小排序
    cout << "3)"; Printlnterva1(a2, a2 + 5);  //輸出 3)16 13 12 9 8
    sort(a2, a2 + 5, LessA());  //按v的個位數從小到大排序
    cout << "4)"; Printlnterva1(a2, a2 + 5);  //輸出 4)12 13 16 8 9
    return 0;
}
編譯至第 45 行時,編譯器將 sort 範例化得到的函數原型如下:

void sort(A* first, A* last, bool (*op)(const A &, const A &) );

該函數在執行過程中,當要比較兩個元素 a、b 的大小時,就是看 op(a, b) 和 op(b, a) 的返回值。本程式中 op 指向 GreaterA,因此就用 GreaterA 定義的規則來比較大小。

編譯至第 47 行時,編譯器將 sort 範例化得到的函數原型如下:

void sort( A* first, A* last, LessA op);

該函數在執行過程中,當要比較兩個元素 a、b 的大小時,就是看 op(a, b) 和 op(b, a) 的返回值。本程式中,op(a, b) 等價於 op.opeartor(a, b),因此就用 LessA 定義的規則來比較大小。

STL 中定義了一些函數物件類別範本,都位於標頭檔案 functional 中。例如,greater 模板的原始碼如下:
template <class T>
struct greater
{
    bool operator()(const T& x, const T& y) const{
        return x > y;
    }
};
假設有以下陣列:

int a[4] = {3, 5, 34, 8};

要將該陣列從大到小排序,則只需寫:

sort( a, a+4, greater<int>() );

要使用 greater 模板,須確保>運算子本來就有定義,或經過了適當的過載。

list 容器的 sort 成員能將元素從小到大排序。它也有兩個版本:一個是沒有引數的函數,比較大小用<運算子;另一個是函數模板,原型如下:

template <class Pred>
void sort(Pred op);

sort 函數允許自定義比較大小的規則,即 op(x, y) 為真就認為 x 比 y 小。例如,假設有:

list<int> lst;

如果希望將 lst 中的元素按其整數數值從大到小排序,只需寫:

lst.sort( greater<int>() );

在使用關聯容器和許多演算法時,都可以用函數物件來定義比較大小的規則,以及其他一些規則和操作。

STL 中的函數物件類別範本

STL 中有一些函數物件類別範本,如表 1 所示。
表1:STL 中的函數物件類別範本
函數物件類別範本 成員函數 T operator ( const T & x, const T & y) 的功能
plus <T> return x + y;
minus < > return x - y;
multiplies <T> return x * y;
divides <T> return x / y;
modulus <T> return x % y;
  成員函數 bool operator( const T & x, const T & y) 的功能
equal_to <T> return x == y;
not_equal_to <T> return x! = y;
greater <T> return x > y;
less <T> return x < y;
greater_equal <T> return x > = y;
less_equal <T> return x <= y;
logical_and <T> return x && y;
logical_or <T> return x || y;
  成員函數 T operator( const T & x) 的功能
negate <T> return - x;
  成員函數 bool operator( const T & x) 的功能
logical_not <T> return ! x;

例如,如果要求兩個 double 型變數 x、y 的乘積,可以寫:

multiplies<double> () (x, y)


less 是 STL 中最常用的函數物件類別範本,其定義如下:
template <class_Tp>
struct less
{
    bool operator() (const_Tp & __x, const_Tp & __y) const
    { return __x < __y; }
};
要判斷兩個 int 變數 x、y 中 x 是否比 y 小,可以寫:

if( less<int>()(x, y) ) { ... }

引入函數物件後 STL 中的“大”、“小”和“相等”概念

前面提到過,預設情況下,STL 中的容器和演算法比較元素的大小是通過<運算子進行的。通過 10.3.4 節可知,sort 和 list::sort 都可以通過一個函數物件或函數自定義比較元素大小的規則。例如以下的 sort 版本:

template <class_RandIt, class Pred>
void sort(_RandIt first, _RandIt last, Pred op);

實際呼叫 sort 時,和 op 對應的實參可以是一個函數物件或者函數的名字。sort 在執行過程中用 op(x, y) 比較 x 和 y 的大小,因此可以將 op 稱為自定義的“比較器”。

關聯容器中的元素是從小到大排序的。使用關聯容器時,也可以用自定義的比較器取代<運算子,以規定元素之間的大小關係。STL 中還有許多演算法都可以自定義比較器。在自定義比較器 op 的情況下,以下三種說法是等價的:
  • x 小於 y。
  • op(x, y) 的返回值為 true。
  • y 大於 x。

同樣地,對關聯容器的 find 和 count 成員函數以及其他一些在有序區間上的 STL 演算法而言,在自定義比較器 op 的情況下,x和y相等op(x, y)和op(y, x)都為假是等價的。