C++重載運算符與STL有序容器

  重載運算符,是C++語言特色之一。對於構造數據類型來說,通過運算符的重載,可以使程序代碼更加簡潔清晰,功能更加豐富。

  本文不過多地介紹運算符重載和STL,只是介紹一下STL有序容器與重載運算符之間的一點小應用。下面的代碼我都簡單寫了,實際上應該做好封裝的。

重載運算符

爲什麼要重載運算符

1. 爲了代碼書寫方便

  比方說,我們定義一個複數類,由於複數類是我們自己構造的數據類型,它是無法簡單地通過'+'來實現複數加法的,因爲編譯器和計算機根本不知道咋加,所以我們需要手動實現一個add()方法來加,但是在C++中,我們可以通過重載運算符,來實現直接使用'+'連接兩個複數來進行加法運算。假設有複數類(結構體):

struct Complex
{
    int a,b;
};
  我們用a,b分別代表複數的實部和虛部,我們知道複數相加實際就是實部和虛部分別相加,我們先來寫一下add方法吧:

Complex add(Complex v1, Complex v2)
{
    Complex res;
    res.a = v1.a + v2.a;
    res.b = v1.b + v2.b;
    return res;
}
  這樣,我們就可以通過add方法來實現兩個複數相加得到一個新的複數了。

  當然更簡單的方法是重載'+'加號:

Complex& operator + (const Complex &v1, const  Complex &v2)
{
    Complex res;
    res.a = v1.a + v2.a;
    res.b = v1.b + v2.b;
    return res;
}
  可以看到,內部代碼其實是一樣的,只不過頭部的聲明有變化而已,其實它也是個函數,實際上就是告訴編譯器,我要重載+加號,它的兩邊都是Complex類型,並且他們運算後的結果也是Complex類型,然後函數體裏定義了他們的運算過程。這樣我們就可以直接使用'+'來連接兩個複數進行運算了。
  試想一下,假設有Complex a, b, c;,那麼c = a.add(b);和c = a + b;哪一段代碼更直觀易懂呢?答案肯定是後者。

  當然我們也可以重載減號啊乘號啊等等……當然,我們也可以定義'-'爲加法運算,'+'爲乘法運算,反正只要你開心,怎麼定義都可以,不必拘泥於運算符本來的含義,重載嘛,本來就是給符號賦予新的生命。

  就像我們日常書寫中,習慣把'^'符號當做“冪”,而在計算機中這個符號卻是異或,當然對於基本數據類型(int, double, char, long, float等)來說,編譯器已經把它們的符號定義固定死了,我們無法更改,但是對於構造類型,我們有絕對的決定權,所以我們可以通過重載運算符讓某些符號實現我們自己想要的功能。

  拿線性代數中的矩陣來說,我們知道矩陣是有冪運算的,那麼假設有矩陣a,要求a的b次方,我們在程序中直接寫a^b肯定是不行的,但是我們可以重載呀,假設有類Matrix(就不詳細寫了),我們可以這樣寫運算符重載的頭部:

Matrix& operator ^ (Matrix &m, int &n)
{
    ......
}
  這樣重載過後,我們假設有Matrix a; int b; 然後我們就可以a^b來得到a矩陣的b次冪了。有興趣的可以看一下我之前寫過的矩陣快速冪的模板,裏面就重載了倆矩陣的基本操作:傳送門>>

2. 爲了使用有序容器或排序

  這點類似java裏實現Comparator接口來使對象可入有序容器。對於內置數據類型如int來說,它的大小關係是確定的,比如1和6,我們明確地知道1小6大,計算機、編譯器也都知道。但是對於構造類型來說,比如構造一個學生類,問學生a和學生b誰大誰小?What?什麼大?年齡?身高?體重?XX?所以計算機編譯器無法確定構造類型的大小關係,他們的關係,是創造這個類型的編碼人來定義確定的,所以這時候我們就需要通過書寫方法或者重載運算符來告知編譯器該如何確定他們的大小關係,例如下面這個結構體(類):

struct student
{
    int age;
    int height;
    int weight;
};
  那麼,我們認爲,通過年齡來定義學生的大小,年齡相等的,就通過身高比較,身高相同的,就通過體重比較。那麼,如果我們通過方法體來寫,應該這樣:

int stucmp(student a, student b)
{
    if(a.age != b.age)
        return a.age < b.age ? -1 : 1;
    if(a.height != b.height)
        return a.height < b.height ? -1 : 1;
    if(a.weight != b.weight)
        return a.weight < b.weight ? -1 : 1;
    return 0;
}
  類比strcmp,差不多就是這個道理,我們可以通過調用該函數來比較出學生的大小,當然更直觀簡便的方式是重載運算符,看下面:

bool operator < (const student &a, const student &b)
{
    if(a.age != b.age)
        return a.age < b.age;
    if(a.height != b.height)
        return a.height < b.height;
    return a.weight < b.weight;
}
  跟上面的代碼類似,看起來也很像一個函數,對於這個函數,我們有三點需要注意。

  第一,返回值

  可以看出,這裏返回值是bool,布爾型,我們知道,對於比較運算符來說,它所組成的表達式的結果是bool值,比如1<2,true,6<3,false,所以重載'<',返回值得是bool。

  第二,運算符

  要重載哪個運算符,就要寫上operator關鍵字,然後跟上要重載的算符。

  第三,參數列表

  我們知道,小於號是雙目運算符,那麼我們參數列表裏就需要兩個參數從左到右分別代表了小於號的左右操作數,也就是說,我們聲明瞭這個“函數”,實際上就是告訴編譯器,我們要告訴你a<b是如何定義的。

  如果這個“函數”返回值時true,代表小於號成立;否則就是不成立。這樣重載完成後,假設有student a,b;,我們就可以簡單地通過a<b來得到ab的大小關係了。
  當我們重載了比較運算符後,我們C++STL內置的一些有序容器就可以使用這些構造類型了,因爲如果你不重載規定的比較符的話,編譯器無法得知構造類型的大小關係,自然就不存在有序這一說,只有重載了比較運算符,編譯器纔可以通過你重載的運算符確定大小關係,自然就可以實現有序。這些下面會繼續介紹。

  C++可重載的運算符很多,我們可以根據自己實際的需要來重載運算符,其實重載運算符是相當靈活的一個功能,我們可以隨意定義它的返回值和功能,但是不能改變它的單雙目性質。不可以重載基本數據類型的運算符。


STL有序容器和算法

算子

  在介紹它們之前,我們先介紹兩種重要的算子類,lessgreater,它們是泛型的。我們先來看一下它們兩個的頭文件裏的原型:

/// One of the @link comparison_functors comparison functors@endlink.
  template<typename _Tp>
    struct greater : public binary_function<_Tp, _Tp, bool>
    {
      bool
      operator()(const _Tp& __x, const _Tp& __y) const
      { return __x > __y; }
    };

  /// One of the @link comparison_functors comparison functors@endlink.
  template<typename _Tp>
    struct less : public binary_function<_Tp, _Tp, bool>
    {
      bool
      operator()(const _Tp& __x, const _Tp& __y) const
      { return __x < __y; }
    };
  可以看出,它們兩個實際上是兩個已經封裝好的大小比較而已,至於它們內部爲什麼要重載()一對括號,這是STL內部的事情,爲了使用起來可以像函數一樣(下面會用例子解釋到),我們現在只需要知道,less封裝了小於,greater封裝了大於。

  通過原型也不難看出,想要使用這兩個泛型類,就必須重載對應的運算符,比如,拿上面的student來說,我們已經重載了小於,所以我們可以通過less<student> a;來聲明一個less對象a,但是greater<student> b;就會報錯:no match for 'operator>' (operand types are 'const student' and 'const student'),明顯可以看出來,student缺少對大於號的重載。

  那麼這兩個類到底有啥子用?爲什麼要把比較封裝起來?直接大於小於搞起來不好嗎?

  好,問得好,那麼我們繼續看下面的內容……

sort算法

  這是我們比較常用的一個排序算法,由於內部根據數據的組織形式靈活實現使用各種排序,所以性能已經是相當好的了。它的使用也相當簡單,傳入兩個參數,容器的開始指針(或迭代器)和容器的結束指針(或迭代器)即可。當然也可以對數組排序,對於數組來說,就是傳入首地址最後一個元素的下一位置地址。比如需要對int s[100]整個數組排序,只需sort(s, s + 100),執行完後s即變爲有序。

  sort默認是升序排序,也就是小的在前,如果我們要降序怎麼辦呢?別急,sort還有三個參數的版本,看一下原型:

template<typename _RAIter, typename _Compare>
    void 
    sort(_RAIter, _RAIter, _Compare);
  我們看到第三個參數叫_Compare,顧名思義,也就是使用_Compare對象進行比較排序,我們給他取個不專業的名字“比較器”,也就是我們上面提到的less和great(其實還有很多,比如等於equal_to,大於等於greater_equal,小於等於less_equal等),當然,這個_Compare也是可以自己定義的,只要能體現出大小比較,就可以(可以是返回值爲bool型的兩個參數的函數,也可以是重載了()雙括號參數爲2個且返回值爲bool的一個類)。默認升序,也就是小的在前,使用了less,如果我們要升序排序,自然使用greater,那麼我們上面對於s[100]的排序就變成了sort(s, s + 100, greater<int>());,注意,因爲我們是在給sort傳參數,所以第三個參數要傳變量進去,所以不要寫成greater<int>而落掉後面的雙括號,因爲寫了括號是聲明,不寫括號就單純的是個類型名(注意greater<int>greater<int>() 的區別)。

  同樣地,我們要對student stu[100]排序的話 ,要升序排序,就要重載小於號,調用sort(stu, stu+100)或者sort(stu, stu+100, less<student>());如果要降序,則需要重載大於號並調用sort(stu, stu+100, greater<student>())。

priority_queue優先隊列

  對於優先隊列,這裏沒什麼要講的,我們只講使用。優先隊列是有序的容器,隊頭始終是整個隊列中最大或最小的元素,既然提到了最大最小,所以對於隊列中的元素,一定要有確定的大小關係。優先隊列原型部分如下:

template<typename _Tp, typename _Sequence = vector<_Tp>,
	   typename _Compare  = less<typename _Sequence::value_type> >
    class priority_queue
    {
        
        ......
        
    };
  可以看出,泛型部分的聲明,第二部分和第三部分都有默認值,分別是vector和less,這說明優先隊列默認情況下時用vector組織,並且以less爲比較運算規則,也就是較大元素在前,這點要區別於sort排序。如下:

    priority_queue(int) q1; // 大的在前
    priority_queue<int, vector<int>, less<int> > q2; // 大的在前,要注意最後<int>與>這裏要隔開一個空格,不然會被當做右移符號>>處理
    priority_queue<int, vector<int>, greater<int> > q3; // 小的在前
    priority_queue(int, greater<int> ) q4; // 報錯,這樣寫編譯器會認爲greater是第二參數,不匹配

  如果我們要使得優先隊列變成較小元素在前,自然就要使第三個參數變成greater。注意,按照參數默認值的規則,第二第三參數要麼寫2不寫3要麼都不寫要麼都寫不存在只寫3而不寫2的(因爲計算機不知道你寫的到底是2還是3,它只認從左到右依次取參數的順序)。
  可以看出,less和greater決定了有序容器和算法的排序規則,它們是兩個泛型類,也是用來比較兩數據的工具,使用它們之前必須要先對源數據模型進行小於號和大於號的重載,這與java中的Comparator泛型接口是一個道理的,只有我們對構造類型定義清楚了它的比較規則,我們的其他算法或泛型容器纔可以正確處理我們的數據。

總結

  算子那部分的問題,現在已經可以解答了。我們可以看出,stl給我們提供了很多的算法和容器,我們要使得stl更加靈活,就要指定它的一些屬性,那屬性是如何指定的?無非兩種,一種是在聲明的時候指定泛型類型,如上面的優先隊列;一種是在調用的時候指定參數,如sort排序。而這兩種方式一個要求指定類型一個要求傳入實參,而如果我們要指定排序規則,難不成要指定一個單純的小於號或者傳入一個小於號?這顯然是不符合編程規範的,如果我們以字符傳入,那麼可能會導致運行時錯誤(比如傳入非法字符),如果我們以符號傳入,好像沒有這種操作。所以我們要將其封裝成類,在需要的時候指定其類名或者聲明一個對應的對象


  下面我手寫一個使用了less或greater等內置比較器的泛型冒泡排序,可能會有助於大家理解本章內容:

template <typename _Iter, typename _Compare>
void bubble_sort(_Iter first, _Iter last, _Compare compare)
{
    for(int i = 0 ; i < (int)(last - first) ; i++)
    {
        for(_Iter j = first ; j < (_Iter)(last - i - 1) ; j++)
        {
            if(!compare(*j,*(j + 1))) // 這裏使用比較器,像調用函數一樣
            {
                swap(*j, *(j + 1)); // 交換兩數
            }
        }
    }
}
  這樣我們如果要對int數組排序,可以這樣:

    int s[] = {1,4,3,8,6,5,3,0};
    bubble_sort(s, s + 8, less<int>()); // 升序排序
    bubble_sort(s, s + 8, greater<int>()); // 降序排序
  看到上面的排序代碼比較那部分,現在可以知道爲什麼less和greater原型裏要重載()雙括號了吧?就是爲了讓less和greater的實例像函數一樣用括號括起來使用,像這樣less<int> cmpLess; cmpLess(a,b)greater<int> cmpGreater; cmpGreater(a,b),這樣的好處是,_Compare可以使用函數來自定義,這樣就算_Compare傳入的是個函數,也可以正常執行,例如:

bool cmp(int a, int b)
{
    return a > b;
}
  然後排序的時候就可以這樣寫:

bubble_sort(s, s + 8, cmp); // 自定義函數的降序

  這樣就更加靈活,可以直接通過函數來聲明排序的大小關係優先級,也是比較方便的。


  也可以手寫一個類似less和greater的類,重載一下()雙括號:

struct cmpGreater
{
    bool operator () (const int &a, const int &b) const
    {
        return a > b;
    }
};
  然後排序可這樣寫:(注意細節

bubble_sort(s, s + 8, cmpGreater()); // 注意雙括號!
  跟上面一樣,這裏是函數調用,是傳參數,傳實參,參數要麼是函數名(函數指針),要麼是變量(對象),所以不加雙括號就變成了類型,是不合法的,加了括號纔是個變量(對象)。


  本章內容可能比較抽象,加上感覺我自己講的也比較亂糟糟……是臨時插入的一章,感覺在日常編寫C++代碼時,STL用的還是比較多的。


  如有表述不明或錯誤的地方,歡迎指正和交流。也歡迎大家繼續跟進數據結構與算法的相關學習博客~




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章