天天看點

我的C++primer長征之路:重載運算與類型轉換重載運算與類型轉換

重載運算與類型轉換

文章目錄

  • 重載運算與類型轉換
      • 基本概念
      • 重載的例子
      • 重載、類型轉換與運算符

基本概念

基本格式

傳回類型 operator 運算符 (參數清單){

}
           

重載運算符參數數目與該運算符作用的運算對象個數一樣,例如重載=運算符,其參數清單中的參數數目應該為2。

但是,如果重載運算符為類的成員函數,那麼其左側的運算對象會綁定到隐式的this指針上,是以成員函數運算符的參數個數比運算符操作對象個數少1個。

重載的運算符的優先級與内置類型的對應運算符的優先級一緻。

定義為成員函數OR非成員函數?

  • 指派=、下标[]、函數調用()、成員通路運算符->必須是成員函數。
  • 符合指派運算符(+= -=…)一般是成員函數,但非必須。
  • 改變對象狀态的運算符或者與給定類型密切相關的運算符,遞增++、遞減–、解引用*等一般應該是成員運算符。
  • 具有對稱性的運算符、如算術運算符、關系運算符、位運算符等應該是非成員運算符。

重載的例子

輸入輸出運算符>> 和 <<

一般來說,輸入輸出運算符的重載形式為:stream& operator 輸入輸出流運算符 (流普通引用, 操作對象);

輸入輸出運算符必須是非成員函數。因為它的傳回對象是一個流的引用而非自定義的類對象引用。

//以Sales_data類為例
ostream &operator (ostream& os, const Sales_data &item){
    os << item.isbn() << " " << item.units_sold << " "
    << item.revenue() << " " << item.avg_price();
    return os;
}

istream &operator (istream& is, Sales_data &item){ //與>>不同,這裡的第二個參數必須為非const得,因為讀入會改變操作對象。
    double price;
    is >> item.bookNo >> item.units_sold >> price;
    if(is){                                 //檢查輸入是否成功
        item.revenue = item.units.sold * price;
    }
    else{
        item = Sales_data();    //輸入失敗,賦予預設構造得值
    }
    return is;
}
           

重載輸入運算符與輸出運算符的一個不同就是輸入運算符需要確定輸入操作是否成功。

算數和關系運算符

相等運算符

//還是以Sales_data為例
bool operator == (const Sales_data &rhs, const Sales_data &lhs){
    return lhs.isbn() == rhs.isbn() && lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue;
}
//一般情況下定義了==也應該定義!=
bool operator != (const Sales_data &rhs, const Sales_data &lhs){
    return !(lhs == rhs);
}
           

關系運算符

#include<string>
#include<iostream>

using namespace std;

class Person{
    public:
    Person(string name, int age) : name(name), age(age) {}
    bool operator < ( const Person& rhs);
    private:
    string name;
    int age;
};

bool Person::operator < (const Person& rhs){
    return this->age < rhs.age;
}

int main(){
    Person a("Jack", 23);
    Person b("Rose", 22);
    cout << (a < b);
    return 0;
}
           

定義一個關系運算符 < 會使自定義類對象在調用标準庫算法時很便利。

指派運算符

指派運算符必須是成員函數。複合指派運算符不一定必須是成員函數,但是為了與内置類型的複合指派運算符保持一緻,一般定義為成員函數。

//從給定的初始化清單指派
Person& Person::operator = (initializer_list<string> il){
    //alloc_n_copy配置設定記憶體空間和拷貝給定範圍内的元素
    auto data = alloc_n_copy(il.begin(), il.end());

    //如果對象之前配置設定有記憶體,先釋放它,Person類沒有,是以不釋放。

    name = data.first;
    age = data.second;
    return *this;
}
           

initializer_list是一種模闆類型,其中 的元素永遠是常量值,無法改變。

il.begin();  //初始化清單首元素
il.end();
il.size();
           

下标運算符

下标運算符必須是成員函數。為與原始下标保持一緻,一般傳回所通路元素的引用。

class StrVec{
    public:
    std::string& operator [](std::size_t n){
        return element[n];
    }
    //一般來說定義常量版本和非常量版本
    const std::string& operator [](std::size_t n){
        return element[n];
    }
    private:
    std::string* element;
};
           

遞增遞減運算符

  1. 前置運算符與後置運算符的差別是,後置運算符的參數清單需要有一個int形參。
  2. 一般先定義前置運算符,之後在定義後置運算符時可以利用定義好的前置運算符進行遞增或遞減。
  3. 前置運算符傳回該類型的引用,而後置運算符傳回一個臨時對象。
    class MyNumber{
        public:
        MyNumber(int n) :num(n) {}
        //前置傳回類型引用
        MyNumber& operator ++(){
            ++num;
            return *this;
        }
        MyNumber& operator --(){
            --num;
            return *this;
        }
        //後置傳回該類型的臨時對象,參數清單有int形參。
        MyNumber operator ++(int){
            MyNumber tmp = *this;
            ++num;
            return tmp;
        }
        MyNumber operator --(int){
            MyNumber tmp = *this;
            --num;
            return tmp;
        }
        public:
        int get_num(){
            return num;
        }
        private:
        int num = 0;
    };
               

其實重載的運算符可以顯式地調用,也就是以一般的函數形式進行調用,但是一般不這樣用。

成員通路運算符

  • 箭頭運算符->必須是成員函數,解引用運算符一般也是成員函數。
  • 重載的箭頭運算符永遠都是執行通路成員的操作,而重載的其他運算符可以執行與預設運算符不一樣的操作。

重載函數調用運算符

作用就是讓類的對象像函數一樣進行調用。

必須是成員函數。

struct AbsInt{
    int operator() (int val){
        return val < 0 ? -val : val;
    }
};

int i = -100;
AbsInt abs;
int a = abs(i);
           

如果一個類定義了函數調用運算符,那麼這個類的對象一般稱為函數對象。而函數對象通常可以作為泛型算法的實參。

vector<string> vs = {"Jack", "Rose"};
class PrintString{
    public:
    PrintString(ostream& o = cout, char c = ' ') : os(o), sep(c){}
    void operator ()(const string &s) const {
        os << s << sep;
    }
    private:
    ostream &os;
    char sep;
};
PrintString printer;
printer(s);  //cout中列印s,以空格為分隔符

//調用for_each算法,将vs中的字元逐一輸出到cerr中,以換行符為分隔符。
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
           

lambda表達式是函數對象

定義一個lambda表達式後,編譯器其實将該表達式翻譯成了一個未命名類的未命名對象。也就是說該未命名的類重載了函數調用運算符 () 。

stable_sort(words.begin(), words.end(), 
            [](const string &a, const string &b) {return a.size() < b.size();});
//預設情況下lambda表達式不能該變它捕獲的變量。

//這裡的lambda表達式類似于下面:
class ShorterString{
    public:
    bool operator ()(const string &s1, const string &s2){
        return s1.size() < s2.size();
    }
};

//上面的stable_sort相當于:
stable_sort(words.begin(), words.end(), ShorterString());
//stable_sort每次比較兩個字元串時,會“調用”這個函數對象。
           

還有,lambda表達式以引用捕獲變量時,其産生的類中不會生成對應的資料成員。若是以值捕獲方式捕獲變量時,其生成的類中會生成對應的資料成員,同時建立構造函數以拷貝的值來初始化該資料成員。生成的類不含預設構造函數、指派運算符以及預設析構函數,預設的拷貝構造函數/移動構造函數視捕獲的資料成員類型而定。書本P574.

标準庫定義的函數對象

定義在functional頭檔案中。

一般情況下能使用标準庫的函數對象就盡量使用标準庫的函數對象。

vector<string*> nameTable;
//錯誤,不能直接比較兩個指針的位址,未定義錯誤。
sort(nameTable.begin(), nameTable.end(), 
        [](string *a, string *b){return a < b;})
//正确,标準庫規定指針的less是定義過的。
sort(nameTable.begin(), nameTable.end(), less<string*>());
           

對于關聯容器,因為關聯容器使用less<key_type>來對元素排序,是以可以直接定義元素類型為指針的set或者map而無需聲明less。

可調用對象與function

可調用對象包含:函數、函數指針、lambda表達式、bind建立的對象、重載了函數調用運算符的類。

不同的類型可能具有相同的調用形式。例如

int add(int i, int j) {return i + j;}

auto mod = [] (int i, int j) {return i % j;}

struct divide{
    int operator() (int i, int j){
        return i / j;
    }
};
//他們的調用形式都是
int (int, int)

//用一個map來存儲函數指針
map<string, int(*)(int, int)> binops;
binops.insert({"+", add});    //正确,add是一個正确類型的函數指針。
//錯誤, divide和mod的調用形式與add一樣,但類型不是int(*)(int, int)
binops.insert({"/", divide})
           

解決方法就是用functional頭檔案中的function。

map<string, function<int(int, int)>> binops;
binops.insert({"+", add});
binops.insert({"/", divide()});  //重載了()的函數對象
binops.insert({"%", mod});

//可以這樣調用,function類型重載了調用運算符
binops["+"](1, 2);  ///調用add函數,輸出3。
           

重載的函數不能直接放入function中。

//重載了add,無法區分
int add(int i, int j);
Sales_data add(const Sales_data&, const Sales_data&);

map<string, function<int(int, int)>> binops;
binops.insert({"+", add}); //錯誤,不能确定是那個add
           

解決方法,可以用:

  1. 函數指針。
  2. lambda表達式
int (*fp)(int, int) = add;  //指向的是第一個add。
binops.insert({"+", fp});

binops.insert({"+", [](int i, int j){return add(1, 2)}});
           

重載、類型轉換與運算符

一個形參的非explicit的構造函數隐式地定義了一種從形參類型到該類類型的隐式轉換規則。

類型轉換運算符

  • 必須定義為成員函數。
  • 沒有傳回類型。
  • 形參清單必須為空。
  • 通常應該是const成員函數。
class SmallInt{
    public:
    SmallInt(int i = 0) : val(i){
        if(i <0 || i > 255){
            throw std::out_of_range("Bad SmallInt value");
        }

    }
    operator int() const {return value;}  //定義了轉換為int的類型轉換運算符。

    private:
    std::size_t val;
};

SmallInt si;
si = 3.14;   //先内置類型double轉換成int,之後調用SmallInt(int)構造函數。
si + 3.14;   //int()先将si轉換成int型,之後内置的Int型轉換成double與3.14相加。
           

因為類型轉換是***隐式***進行的,無法給函數傳遞實參,是以也就形參清單就為空。

注意避免過度使用類型轉換運算符。一般來說,類很少定義類型轉換運算符,除了bool之外。

隐式的類型轉換會存在意想不到的問題,是以引入了顯式的類型轉換運算符。也就是在轉換運算符前加上explicit。

class SmallInt{
    public:
    explicit operator int() const {return val;}  //顯式類型轉換
};

SmallInt si;
//這種情況下,需要顯式地聲明類型轉換才會進行類型的轉換。
int ans = static_cast<int>(si) + 3;  //顯式地調用operator int()
           

一般來說,顯式聲明的類型轉換運算符需要顯式地調用,隻有一個例外:表達式被用作條件時,顯式的類型轉換将會被隐式地執行。

  1. 在if、while以及do語句的條件部分。
  2. for語句的條件表達式。
  3. 邏輯與或非(&&,||,!)。
  4. 條件運算符(? : )的條件部分。
if (si > 10){  //si被隐式地轉換成int型。
    //...
}
           

對于IO流,無論什麼時候在條件中使用流,都會使用流定義的operator bool類型轉換運算符。

而且一般來說, bool類型轉換應該定義成explicit的,因為它一般隻用在條件語句中。

避免有二義性的類型轉換

不要定義多種從類型A到類型B的轉換路徑,一般隻要一對一的轉換,否則在類型轉換時,編譯器不知道調用哪種轉換方式。(書本P584)

總的來說,除了顯式地定義向bool的轉換之外,盡量避免定義類型轉換函數和非顯式的單形參的構造函數。

繼續閱讀