文章目錄
- 基本概念
- 調用
- 選擇作為成員還是非成員
- 輸入和輸出運算符
- 算術和關系運算符
- 相等和不等運算符
- 賦值運算符
- 下標運算符
- 遞增和遞減運算符
- 成員訪問運算符
- 函數調用運算符
- lambda是函數對象
- 標準庫定義的函數對象
- 可調用對象與function
- 重載、類型轉換與運算符
- 類型轉換運算符
- 避免有二義性的類型轉換
基本概念
重載的運算符是具有特殊名字的函數:它們的名字由關鍵字operator
和其后要定義的運算符號共同組成。其參數數量和該運算符的運算數量一樣多。除了重載的函數調用運算符operator()
之外,其他重載運算符不能含有默認參數。
如果一個運算符函數是成員函數,則它的第一個運算對象綁定到this指針。對于一個運算符函數來說,它或者是類的成員,或者至少含有一個類類型的參數:
int operator+(int, int); //err
我們可以重載大多數已有的運算符,而無權發明新的運算符號。重載后運算符和內置運算符的優先級和結合律保持一致。
一般不建議重載逗號、取地址、邏輯與和邏輯或運算符。
if (a && b) { ... }
如果 a 為 false,則 b 根本不會計算。
但一旦重載了 operator&&, operator||
,這個短路規則 完全丟失,編譯器會強制先算兩個操作數,再傳給重載函數。
int x = (f1(), f2()); // 保證先執行 f1,再執行 f2
如果重載了 operator,,它就變成了你自定義的邏輯,而失去了“順序保證”這個語言特性。
取地址則是對于類類型已經有了內置的含義。
調用
//直接調用
data1 + data2;//間接調用
operator+(data1, data2); //普通運算符
data1.operator+(data2); //成員運算符
選擇作為成員還是非成員
- 賦值(=),下表([]),調用(()),成員訪問符(->)等必須是成員;
- 復合運算符一般是成員;
- 改變對象狀態的運算符或者給定類型密切相關的運算符,如遞增、遞減和解引用運算符,通常是成員;
- 具有對稱性的運算符,例如算數、相等性、關系和位運算符等,通常是非成員;
- 輸入輸出運算符必須是非成員(如果是成員的話,不符合第一個操作對象是輸入輸出)。
輸入和輸出運算符
//輸出
ostream &operator<<(ostream &os, const Sales_data &item);
Sales_data data;
cout << data;//輸入
istream &operator>>(istream &is, Sales_data &item)
{double price;is >> item.bookNo >> item.units_sold >> price;if (is)item.revenue = item.units_sold * price;elseitem = Sales_data();return is;
}
cin >> data;
輸入運算符通常會在還會進行錯誤處理。
算術和關系運算符
如果定義了算術運算符,一般也會定義一個對應的復合運算符:
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{Sales_data sum = lhs;sum += rhs;return sum;
}
相等和不等運算符
相等運算符和不等運算符中的一個應該把工作委托給另外一個:
bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{return lhs.isbn() == rhs.isbn() && lhs.units_sold == rhs.units_sold &&lhs.revenue == rhs.revenue;
}bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{return !(lhs == rhs);
}
賦值運算符
拷貝賦值和移動賦值可以把類的一個對象賦值給該類的另外一個對象。此外,類還可以定義其他賦值運算符以使得別的類型作為右側運算對象:
//標準庫vector類還定義了第三種賦值運算符,該運算符接受花括號元素列表作為參數
vector<string> v;
v = {"a", "an", "the"};
下標運算符
如果一個類包含下標運算符,則通常會定義兩個版本:一個返回普通的引用,另一個則是類的常量成員返回常量引用:
class StrVec
{
public:std::string& operator[](std::size_t n){return element[n];}const std::string& operator[](std::size_t n) const{ return element[n];}
private:std::string *element;
}
遞增和遞減運算符
定義遞增和遞減運算符的類應該同時定義前置和后置版本。這些運算符通常被定義為成員。
后置版本接受一個額外的int形參,該形參一般不被使用:
class StrBlobPtr
{
public://前置StrBlobPtr& operator++();StrBlobPtr& operator--();//后置StrBlobPtr operator++(int);StrBlobPtr operator--(int);
}//顯示調用
StrBlobPtr p(a1);
p.operator++(0); //后置
p.operator++(); //前置
成員訪問運算符
解引用運算符首先檢查curr是否在合理范圍內,如果是,則返回curr所指對象的一個引用;箭頭運算符調用解引用并返回其結果地址:
class StrBlobPtr
{std::string& operator*() const{auto p = check(curr, "dereference past end");return (*p)[curr];}std::string* operator->() const{return & this->operator*();}
}
值得注意的是,這兩個運算符都定義成了const成員,這是因為獲取一個元素并不會改變該對象的狀態。
StrBlob a1 = {"a", "an", "the"};
StrBlobPtr p(a1);
*p = "okay"; //給a1的首元素賦值
cout << p->size() << endl; //打印4,okay的大小
cout << (*p).size() << endl; //等價
和大多數其他運算符一樣,我們能令operator*完成任何指定的操作。箭頭運算符則不是這樣,它只能擁有成員訪問這個最基本的含義。形如point->mem的表達式來說,point必須是指向對象的指針或者是一個重載了operator->的類的對象:
(*point).mem; //point為指針
(point.operator->())->mem; //point為重載了operator->的對象
函數調用運算符
如果類重載了函數調用運算符,則我們可以像使用函數一樣使用該類的對象。一個類可以定義多個不同版本的調用運算符,相互之間在參數數量或類型上有所區別。
如果定義了函數調用運算符,則該類的對象稱作函數對象。
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;
}
函數對象常常作為泛型算法的實參:
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
vs中的對象打印到cerr中,并換行分割。
lambda是函數對象
當我們編寫一個lambda后,編譯器會將該表達式翻譯成一個未命名類的未命名對象。該類中含有一個重載函數調用運算符:
stable_sort(words.begin(), words.end(), [](const string &a, const &b){return a.size() < b.size();});//該lambda等價于該類的一個未命名對象
class ShorterString
{
public://lambda默認值捕獲,因此生成的唯一成員函數是const//如果是引用捕獲,則不是constbool operator()(const string &a, const &b) const{return a.size() < b.size();}
};//等價lambda表達式寫法
stable_sort(words.begin(), words.end(), ShorterString());
- 值捕獲:產生的類必須為每個值捕獲的變量建立對應的數據成員,同時構建構造函數:
auto wc = find_if(words.begin(), words.end(), [sz](const string &a){ return a.size >= sz;});class SizeComp
{
public:SizeComp(size_t n) : sz(n) {}bool operator()(const string &s) const{return s.size() >= sz;}
private:size_t sz;
};auto wc = find_if(words.begin(), words.end(), SizeComp(sz));
- 引用捕獲:不用生成成員變量和構造函數。
標準庫定義的函數對象
標準庫定義了一組表示算術運算符、關系運算符和邏輯運算符的類,每個類分別定義了一個執行命名操作的調用運算符。
sort(vec.begin(), vec.end(), greater<string>());
可調用對象與function
C++中有幾種可調用的對象:函數、函數指針、lambda表示式、bind創建的對象以及重載了函數調用運算符的類。兩種不同類型的可調用對象可以同享一種調用形式,例如:int(int, int)
。
可以通過function表示不同類型的可調用對象:
int add(int i, int j) {return i +j;}
auto mod = [](int i ,int j) {return i % j;};
struct divide
{int operater()(int i, int j) {return i / j;}
};map<string, function<int(int, int)>> binops =
{{"+", add},{"-", std::minus<int>()},{"/", divide()},{"*", [](int i, int j){return i * j;}},{"%", mod}
};
值得注意的是,同名可調用對象有重載時,不能存入function類型的對象中。可通過函數指針或者lambda表示式消除二義性。
重載、類型轉換與運算符
轉換構造函數和類型轉換運算符共同定義了類類型轉換。
類型轉換運算符
類型轉換運算符時類的一種特殊成員函數,它負責將一個類類型的值轉換成其它類型:operator type() const;
,滿足下列條件
- 一個類型轉換函數必須是類的成員函數;
- 不能聲明返回類型;
- 形參列表必須為空;
- 函數通常是const的。
class SmallInt
{
public:SmallInt(int i = 0) : val(i) {}//雖然沒有指定返回類型,但實際會返回type類型operator int() const {return val;}
private:int val;
}
其中構造函數能夠將算術類型轉化為SmallInt,而類型轉換運算符能夠將SmallInt類型轉化為算術類型:
SmallInt si;
si = 4; //int->SmallInt
si + 3; //SmallInt->int
有時隱式的類型轉換看上去會覺得困擾,例如,當istream含有bool的類型轉換時,下面的代碼可以正常編譯:
int i = 42;
cin << i; //cin -> bool -> int
為了防止這樣的情況,可以定義顯示的類型轉換運算符:
class SmallInt
{
public:explicit SmallInt(int i = 0) : val(i) {}explicit operator int() const {return val;}
};SmallInt si = 3;
si +3; //err
static_cast<int>(si) + 3; //ok
值得注意的是,如果表達式被用作條件,則編譯器會將顯示的類型轉換自動應用于它,即顯示的類型轉換將被隱式的執行:
- if、while、do的條件語句部分;
- for 語句頭的條件表達式;
- 邏輯非、或、與的運算對象;
- 條件運算符的條件表達式。
大部分類都定義了向bool轉化的顯示類型轉換運算符,例如IO類型:
while (std::cin >> value)
避免有二義性的類型轉換
如果定義了一組類型轉換,它們的轉換源或者轉換目標可以通過其他類型轉換聯系在一起,則會產生二義性問題:
- 最好只創建一個算術類型的轉換
//最好只創建一個算術類型的轉換
struct A
{A(int = 0);A(double);operator int() const;operator double() const;
};void f2(long double);
A a;
f2(a); //err,二義性,不知道用哪一個進行類型轉換long lg;
A a2(lg); //err,不知道用哪一個構造函數
- 最好不要在兩個類之間構建相同的類型轉換:
struct B;
struct A
{A() = default;A(const B&);
};struct B
{operator A() const;
};A f(const A&);
B b;
A a = f(b); //err//可以顯示指定
A a1 = f(b.operator A());
A a2 = f(A(b));