C++ 學習筆記之(14) - 重載運算與類型轉換
在C++ 學習筆記之(4)-表達式、運算符與類型轉換中記錄了C++語言中定義的大量運算符和內置類型的自動轉換規則,並且當運算符作用於類類型時,可以通過運算符重載重新定義該運算符的含義。同時,也能自定義類類型之間的轉換規則,即和內置類型的轉換一樣,類類型轉換隱式的將一種類型的無錫愛那個轉換成另一種類型對象
基本概念
- 重載運算符函數的名字由關鍵字
operator
和其後的運算符號共同組成,也包括返回類型、參數列表和函數體 - 重載運算符函數的參數數量與該運算符作用的運算對象數量一樣多
- 除了重載的函數調用運算符
operator()
外,其他重載運算符不能含有默認實參 - 當一個重載的運算符是成員函數時,
this
綁定到左側運算對象,顯示參數數量比運算對象數量少一個 - 當運算符作用於內置類型的運算對象時,無法重載
調用重載運算符函數
// 一個非成員運算符函數的等價調用 data1 + data2; // 普通表達式 operator+(data1, data2); // 等價的函數調用
&&
、||
和,
,這三種運算符重載後求值順序規則無法保留,且前兩個運算符重載版本中短路求值屬性失效重載運算符儘量使用與內置類型一致的含義
重載運算符作爲成員函數還是非成員函數的準則
- 成員函數
- 必須:賦值
=
、下標[]
、調用()
和成員訪問箭頭->
運算符 - 一般:複合賦值運算符
+=, -=
、遞增遞減++, --
和解引用*
運算符 - 普通非成員函數
- 具有對稱性的運算符可能轉換任意一端的運算對象,比如算數、相等性、關係和位運算符等
輸入和輸出運算符
IO
標準庫分別使用>>
和<<
執行輸入和輸出操作。IO
庫定義了用其讀寫內置類型的版本,而類類型的版本需要自定義。
重載輸出運算符<<
- 通常,輸出運算符的第一個形參爲一個非常量
ostream
對象的引用,因爲ostream
對象不可拷貝,並會改變。第二個形參爲一個常量的引用,只打印不改變。返回ostream
形參 - 輸出運算符儘量減少格式化操作,輸出運算符應該主要打印對象的內容而非控制格式
- 輸入輸出運算符必須是非成員函數,且被聲明爲類的友元
ostream *operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() <<" " << item.units_sold;
return os;
}
重載輸入運算符>>
- 通常,輸入運算符的第一個形參是輸入流的引用,第二個爲讀入到的對象的非常量引用。返回某個給定流的引用。
- 輸入運算符必須處理輸入可能失敗的情況,而輸出運算度不需要
istream &operator>>(istream &is, Sales_data &item)
{
double price; // 不需要初始化,因爲先讀入數據到 price,後使用
is >> item.bookNo >> item.units_sold >> price;
if(is) // 檢查輸入是否成功
item.revenue = item.units_sold * price;
else
item = Sales_data(); // 輸入失敗:對象被賦予默認的狀態
return is;
}
算數和關係運算符
通常,算數和關係運算符被定義成非成員函數以允許對左側和右側的運算對象進行轉換,且這些運算符一般不需要改變運算對象,故形參都爲常量引用
- 若類定義了算術運算符,一般也會定義對應的複合賦值運算符,且一般用複合賦值運算符實現算數運算符
==
與!=
運算符一般同時存在,且應該把工作委託給另一個,即一個負責實際比較,另一個調用該運算符
賦值運算符
除了拷貝賦值和移動賦值運算符,類還可以定義其他賦值運算符,以使用其他類型作爲右側運算對象
StrVec &StrVec::operator=(initializer_list<string> il) { // alloc_n_copy 分配內存空間並從給定範圍內拷貝元素 auto data = alloc_n_copy(il.begin(), il.end()); free(); // 銷燬對象中元素並釋放內存空間 elements = data.first; // 更新數據成員使其指向新空間 first_free = cap = data.second; return *this; }
複合賦值運算符一般爲成員函數,且返回其左側運算對象的引用
下標運算符
- 下標運算符
operator[]
必須是成員函數 - 若類包含下標運算符,則通常定義兩個版本:一個返回普通引用,另一個爲類的常量成員並返回常量引用
遞增和遞減運算符
- 定義遞增和遞減運算符的類應該同時定義前置版本和後置版本,且應被定義成類的成員
- 前置運算符應該返回遞增或遞減後對象的引用
- 後置版本接受一個額外的(不被使用)
int
類型的形參,以區別前置版本 - 後置運算符應該返回對象的原值(遞增或遞減之前的值),返回的是值而非引用
class A{
public:
A& operator++(); // 前置運算符
A operator++(int); // 後置運算符
}
// 顯示調用
A pa;
pa.operator++(0); // 調用後置版本的 operator++
pa.operator++(); // 調用前置版本的 operator++
成員訪問運算符
- 箭頭運算符必須是類的成員,解引用運算符通常也是類的成員
- 重載的箭頭運算符必須返回類的指針或自定義了箭頭運算符的某個類的對象
函數調用運算符
- 函數調用運算符必須是成員函數。一個類可以定義多個不同版本的調用運算符,相互之間應該在參數數量或類型上有所區別。
- 函數對象:若類定義了函數調用運算符,則該類的對象被稱爲函數對象,即調用對象類似於調用函數
struct absInt{
int operator()(int val) const{ // 返回參數絕對值
return val < 0 ? -val : val;
}
}
int i = -42;
absInt absObj; // 含有函數調用運算符的對象
int ui = absObj(i); // 將 i 傳遞給 absObj.operator()
lambda
是函數對象
lambda
表達式被編譯器翻譯成一個未命名類的未命名對象lambda
表達式產生的類不含默認構造函數、賦值運算符以及默認析構函數;是否含有默認的拷貝/移動構造函數則通常要視捕獲的數據成員類型而定
// 獲得第一個指向滿足條件元素的迭代器,該元素滿足 size() is >= sz
auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; });
// 該 lambda 表達式產生的類形如
class SizeComp{
SizeComp(size_t n): sz(n) {} // 該形參對應捕獲的變量
// 該調用運算符的返回類型、形參和函數體都與 lambda 一致
bool operator()(const string &s) const { return s.size() >= sz; }
private:
size_t sz; // 該數據成員對應通過值捕獲的變量
}
auto wc = find_if(words.begin(), words.end(), SizeComp(sz)); // 等價於上述使用 lambda語句
標準庫定義的函數對象
標準庫定義了一組表示算術運算符、關係運算符和邏輯運算符的類,每個類分別定義了一個執行命名操作的調用運算符
plus<int> intAdd; // 可執行 int 加法的函數對
int sum = intAdd(10, 20); // 等價於 sum = 10 + 20
// 比較指針的內存地址來 sort 指針的 vector
vector<string *> nameTable; // 指針的 vector
// 錯誤:nameTable 中的指針彼此之間沒有關係,所以 < 將產生未定義的行爲
sort(nameTable.begin(), nameTable.end(), [](string *a, string *b) { return a < b; });
sort(nameTable.begin(), nameTable.end(), less<string*>()); // 正確:標準庫規定指針的 less 定義良好
可調用對象與function
C++語言中有集中可調用的對象:函數、函數指針、lambda
表達式、bind
創建的對象以及重載了函數調用運算符的類。
- 可調用對象也有類型,比如
lambda
有自己唯一的(未命名)類類型;函數及函數指針的類型則有返回值類型和實參決定等。 - 不同類型的調用對象可能共享同一種 調用形式(call signature)。
- 可用
function
標準庫模板表示不同類型卻共享調用形式的可調用對象
// 調用形式 int(int, int), 考慮不同類型的調用對象
int add(int i, int j) { return i + j; } // 普通函數
auto mod = [](int i, int j) { return i % j; }; // lambda, 其產生一個未命名的函數對象類
struct divide{
int operator()(int denominator, int divisor) { return deniminator / divisor; }
};
// 可通過 function 模板表示某種調用形式的可調用對象
function<int(int, int)> f1 = add; // 函數指針
function<int(int, int)> f2 = mod; // lambda
function<int(int, int)> f3 = divide(); // 函數對象類的對象
cout << f1(4, 2) << endl << f2(4, 2) << endl << f3(4, 2) << endl; // 6, 0, 2
重載、類型轉換與運算符
在C++ 學習筆記之(7)-類中可看到由實參調用的非顯示構造函數能夠通過隱式類型轉換將實參類型的對象轉換成類類型。對於類類型的轉換,可以通過定義轉換構造函數和類型轉換運算符共同定義
類型轉換運算符
類的一種特殊成員函數,可將一個類類型轉換爲其他類型
- 轉換的類型要作爲函數的返回類型,故不允許轉換爲數組或者函數類型,但允許轉換爲指針(包括數組指針以及函數指針)或者引用類型
- 類型轉換運算符沒有顯示的返回類型,也沒有形參,且必須定義成類的成員函數,一般爲
const
- 雖然編譯器只允許一步類類型轉換,但類類型轉換可以和標準(內置)類型轉換一起使用
class SmallInt{
public:
// 構造函數將算數類型的值轉換爲 SmallInt 對象
SmallInt(int i = 0): val(i)
{ if(i < 0 || i > 255) throw std::out_of_range("Bad SmallInt Value")}
// 類型轉換運算符將 SmallInt 對象轉換爲 int
operator int() const { return val; }
private:
std::size_t val;
}
SmallInt si;
// 算數類型轉換爲類類型
si = 4; // 首先將 4 隱式轉換成 SmallInt, 然後調用 SmallInt::operator=
// 類類型轉換爲算數類型
si + 3; // 首先將 si 隱式轉換成 int, 然後執行整數的加法
// 類類型轉換和標準內置類型轉換共同使用
SmallInt si2 = 3.14; // 內置類型轉換將 double 實參轉換成 int, 然後調用SmallInt(int)構造函數
C++11定義的 顯示的類型轉換運算符表示必須通過顯示的強制類型轉換調用
class SmallInt{ public: explicit operator int() const { return val; } // 編譯器不會自動執行這一類型轉換 // ... 其他與之前一致 }; SmallInt si = 3; // 正確:SmallInt 的構造函數不是顯示的 si + 3; // 錯誤:此刻需要隱式的類類型向算數類型轉換,但類的運算符是顯示的 static_int<int>(si) + 3; // 正確:顯示地請求類型轉換
若表達式被用作條件,則編譯器會自動應用顯示類型轉換,即下列情況會隱式執行顯示的類型轉換
if
、while
及do
語句的條件部分for
語句頭的條件表達式- 邏輯非運算符
!
、邏輯或運算符||
、邏輯與運算符&&
的運算對象 - 條件運算符
(? :)
的條件表達式
避免有二義性的類型轉換
若類中包含一個或多個類型轉換,則必須確保在類類型和目標類型之間只存在唯一一種轉換方式,否則,可能會有二義性
不要爲類提供相同的類型轉換,比如
A
類定義了接受B
類型對象的轉換構造函數,同時B
類定義了轉換目標是A
類的類型轉換運算符不要定義多個轉換規則,比如在類中最多隻定義一個與算數類型有關的轉換規則
struct A{ A(int = 0); // 最好不要創建兩個轉換預案都是算數類型的類型轉換 A(double); operator int() const; // 最好不要創建兩個轉換對象都是算數類型的類型轉換 operator double() const; }; void f2(long double); A a; f2(a); // 二義性錯誤:含義是f(A::operator int()) 還是 f(A::operator double()) ? long lg; A a2(lg); // 二義性錯誤:含義是 A::A(int)還是 A::A(double)? // 上邊兩種都無法精確匹配,故產生二義性 short s = 42; A s3(s); // 正確:使用 A::A(int) 把 short 提升成 int 優於把 short 轉換成 double
在使用兩個用戶定義的類型轉換時,若轉換函數之前或之後存在標準類型轉換,則標準類型轉換將決定最佳匹配是i哪一個
函數匹配與重載運算符
表達式中運算符的候選函數集既包括成員函數,也應該包括非成員函數
若一個類既提供了轉換目標是算數類型的類型轉換,也提供了重載運算符,則將遇到重載運算符與內置運算符的二義性問題
class SmallInt{ friend SmallInt operator+(const SmallInt &, const SmallInt &); public: SmallInt(int = 0); // 轉換源爲 int 的類型轉換 operator int() const { return val; } // 轉換目標是 int 的類型轉換 private: std::size_t val; }; SmallInt s1, s2; SmallInt s3 = s1 + s2; // 正確:使用重載的 operator+ int i = s3 + 0; // 錯誤:二義性錯誤
結語
一個重載的運算符必須是類的成員或者至少擁有一個類類型的運算對象。重載運算符的運算對象數量、結合律、優先級與對應的用於內置類型的運算符完全一致
若類重載了函數調用運算符operator()
,則該類的對象被稱作 函數對象, 且lambda
表達式是一種簡便的定義函數對象類的方式
在類中可以定義轉換源或轉換目標是該類型本身的類型轉換,這樣的轉換是自動執行。只接受單獨一個實參的非顯示構造函數定義了從實參類型到類類型的類型轉換;而非顯示的類型轉換運算符則定義了從類類型到其他類型的轉換