11.30 C++ 自定義比較:仿函數、函數與重載操作符

cpp 模板泛型編程

cpp 比 c 方便不少不光因為其支持面向對象支持class,同樣還因為其支持泛型編程,有方便的STL庫。泛型要比宏強大的多,是一種設計更巧妙的編譯期動態機制,類型安全,使得一些通用算法的封裝變得十分方便。模板操作的是類型,特化的時候編譯器會做類型推導,這是模板一個核心特徵。

根據C++標準,當一個模板不被用到時它就不應該被具體化。對於cpp 編譯器是如何特化,編譯成最終代碼,用到了 惰性求值 和 模式匹配。這篇文章簡單介紹了這兩個原理:學習Haskell。

對於實際的使用,記住下面兩點:

函數模板的模板參數是隱式的,編譯器根據傳入值的類型來推導模板參數的類型,函數模板的參數不能有默認值。

類模板的模板參數是顯式的,使用一個類模板時必須指明其使用的參數,類模板的模板參數可以有默認值。

這裡主要說一下 C++ 標準庫,尤其是 STL 涉及比較操作時的常用比較器。這裡是STL源代碼。

對 sort, stable_sort 傳定製比較函數

傳入函數編譯器來做類型推導,進行模板特化。看sort函數的模板聲明:

// 可以看出,排序要求容器支持隨機訪問迭代器,類似於數組的那種下標偏移訪問
// 這裡 _Compare 是類型, __comp 是實例,調用 sort 需要傳入的就是 __comp 實例
template <class>
inline void sort(_RandomAccessIter __first, _RandomAccessIter __last,
_Compare __comp)/<class>

一個例子如下:

typedef struct tagNode {
int index;
int value;
} Node;

bool comp(const Node &a, const Node &b)
{
return a.value > b.value;
}

int main()
{
Node a[5] = { {1, 11}, {3, 33}, {2, 22}, {5, 55}, {4, 44} };
// 編譯器會進行類型推導做模板特化 <class>
sort(a, a + 5, comp);
return 0;
}/<class>

對 map, set, priority_queue 傳入仿函數

仿函數functor的英文解釋為something that performs a function,即其行為類似函數的東西。C++中的仿函數是通過在類中重載()運算符實現,使你可以像使用函數一樣來創建類的對象。要求傳入仿函數的地方也很好理解,一般C++模板,尖括號<>裡面放的是類型,自然這需要比較器的時候傳入的也是比較器的類型,這就是用到仿函數的地方。

首先,看一下 STL 源碼的模板聲明:

// map 和 set 底層存儲結構式都是紅黑樹
// Forward declarations of operators == and template <class> class _Compare __STL_DEPENDENT_DEFAULT_TMPL(less<_key>),
class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class map;

template <class>),
class _Alloc = __STL_DEFAULT_ALLOCATOR(_Key) >
class set;

// priority_queue 有優先級,要求元素可比較。queue 和 priority_queue 默認的底層存儲結構也不同
// queue 默認用的是 deque 雙端隊列,priority_queue 用的是 vector
// priority_queue 實現使用的默認比較是 operator< ,是最大堆數據結構,即隊列頭元素值最大
template <class> class _Sequence __STL_DEPENDENT_DEFAULT_TMPL(deque<_tp>) >
class queue;

template <class> class _Sequence __STL_DEPENDENT_DEFAULT_TMPL(vector<_tp>),
class _Compare
__STL_DEPENDENT_DEFAULT_TMPL(less<typename>)
class priority_queue; // 注意點:如果自己傳入自己的仿函數比較,那麼第二個存儲類型也要傳入不能缺省/<typename>/<class>/<class>/<class>/<class>

對於 priority_queue 用法,這篇文章裡的例子很不錯STL裡的priority_queue用法。
一個注意點就是:如果使用自定義比較,使用仿函數,那麼使用時必然也要傳入第二個模板類型參數,要麼都缺省,要麼都傳入。下面給一個例子:

#include <queue>
#include <cstdio>

using namespace std;

typedef struct tagNode
{
int index;
int value;
} Node;

/* 針對某種類型的自定義比較仿函數,cpp 中 struct 相當於全部 public 的 class */
struct classcomp
{
/* const 這裡編譯沒有約束,但是對於明確的不可變參加上更嚴謹 */
bool operator() (const Node& a, const Node& b) const
{
return a.value > b.value;
}
};

int main()
{
Node a[5] = { {1, 11}, {3, 33}, {2, 22}, {5, 55}, {4, 44} };

// classcomp 必須是針對 Node 類型的比較仿函數,第二個缺省存儲結構也不能少
priority_queue<node>, classcomp> priQue;

for(unsigned int i = 0; i < sizeof(a)/sizeof(a[0]); ++i) {
priQue.push(a[i]);
}
while(!priQue.empty()) {
const Node& topNode = priQue.top();
// Node topNode = priQue.top() // 也可以結構體直接 = 號賦值,底層相當於 memcpy, Linux 內核就有這麼用的
// const Node *ptopNode = &priQue.top(); // 如果不想拷貝,cpp 推薦用引用, 即上面的寫法
printf("index is %d, value is %d\\n", topNode.index, topNode.value);
priQue.pop();
}

return 0;
}/<node>/<cstdio>/<queue>

同樣給個例子:

#include <queue>
#include <cstdio>

using namespace std;

// 重載運算符 讓cpp代碼的表達力有很大提升,比如map重載[]可以翻遍用[]獲取指定key的value
// 還有如果定義了一些矩陣運算什麼的,重載 * 就更加方便靈活了

struct Node
{
int index;
int value;
/* 注意點:這裡的 const 不可少編譯約束 */
bool operator < (const Node& compNode) const
{
return this->value < compNode.value;
}
};


int main()
{
Node a[5] = { {1, 11}, {3, 33}, {2, 22}, {5, 55}, {4, 44} };

priority_queue<node> priQue; //Node 類型重載了 < ,變得像 int 等基本數據類型一樣可比較了

for(unsigned int i = 0; i < sizeof(a)/sizeof(a[0]); ++i) {
priQue.push(a[i]);
}
while(!priQue.empty()) {
const Node& topNode = priQue.top();
printf("index is %d, value is %d\\n", topNode.index, topNode.value);
priQue.pop();
}
return 0;

}/<node>/<cstdio>/<queue>

總結

對於 cpp 中需要自定義比較時,如果以後的操作比較都是定的,可以用重載,否則還是用自定義比較函數和仿函數。還有就是STL 中優先級隊列priority_queue, 自定義仿函數是模板的第三個類型,如果自定了那麼第二個模板參數也要傳入,一般用vector就可以。

最後,如果你想學C/C++可以私信小編“01”獲取素材資料以及開發工具和聽課權限哦!


分享到:


相關文章: