《C++ Primer》學習筆記(十四):重載運算與類型轉換
當一個重載的運算是成員函數時,this
綁定到左側運算對象。成員運算符函數的(顯式)參數數量比運算對象的數量少一個。
//一個非成員函數的等價調用
data1 + data2; //普通的表達式
operator+(data1, data2); //等價的函數調用
data1 += data2; //基於“調用的表達式”
data1.operator+=(data2); //對成員運算符函數的等價調用
注意:因爲使用重載的運算符本質上是一次函數調用,所以這些關於運算對象求值順序的規則無法應用到重載的運算符上。特別是,邏輯與運算符、邏輯或運算符和逗號運算符的運算對象求值順序規則無法保留下來。除此之外,&&
和||
運算符的重載版本也無法保留內置運算符的短路求值屬性,兩個運算對象總是會被求值。因此不建議重載它們,因爲可能使得用戶一直習慣的求值規則不再適用。
下面的準則有助於我們在將運算符定義爲成員函數還是普通的非成員函數做出選擇:
- 賦值(
=
)、下標([]
)、調用(()
)和成員訪問箭頭(->
)必須是成員函數。 - 複合賦值運算符(例如
+=
)一般來說應該是成員,但並非必須,這一點與賦值運算符略有不同。 - 改變對象狀態的運算符或者與給定類型密切相關的運算符,如遞增、遞減和解引用運算符,通常應該是成員。
- 具有對稱性的運算符可能轉換任意一端的運算對象,例如算術、相等性、關係和位運算符等,因此它們通常應該是普通的非成員函數。
當我們把運算符定義爲成員函數時,它的左側運算對象必須是運算符所屬類的一個對象。
string s = "world";
string t = s + "!";//正確:我們能把一個const char *加到一個string對象中
string u = "hi"+s; //如果+是string的成員,則產生錯誤。因爲"hi"+s等價於"hi".operator+(s),而"hi"的類型是const char *,這是一種內置類型,沒有這個成員函數
輸入和輸出運算符
與iostream
標準庫兼容的輸入輸出運算符必須是普通的非成員函數,而不能是類的成員函數。爲了讀寫類的非公有數據成員,一般聲明爲友元。
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)
{
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
operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum += rhs; //如果類定義了相關的複合賦值運算符,一般使用複合賦值運算符來實現算術運算符
return sum;
}
bool operator==(const A &lhs, const A &rhs)
{
return lhs.unm == rhs.num;
}
bool operator!=(const A &lhs, const A &rhs)
{
return !(lhs==rhs);
}
注意:如果存在唯一一種邏輯可靠的<
定義,則應該考慮爲這個類定義<
運算符。如果類還同時包含==
,則當且僅當<
的定義和==
產生的結果一致時才定義<
運算符。
賦值運算符
除了拷貝賦值運算符和移動賦值運算符外,標準庫vector
還定義了接受花括號內的元素列表作爲參數的賦值運算符。爲了與內置類型的賦值運算符保持一致,這個賦值運算符也返回其左側運算對象的引用。
class StrVec{
public:
StrVec &operator=(std::initializer_list<std::string>);
}
StrVec &operator=(std::initializer_list<std::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;
}
複合賦值運算符通常情況下也定義爲類成員。
//作爲成員的二元運算符,左側運算對象綁定到隱式的this指針
Sales_data& Sales_data::operator+=(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
下標運算符
**下標運算符必須定義爲成員函數。**最好同時定義下標運算符的常量版本和非常量版本,當作用於一個常量對象時。下標運算符返回常量引用以確保我們不會爲返回的對象賦值。
class StrVec{
public:
std::string& operator[](std::size_t n)
{return elements[n];}
const std::string& operator[](std::size_t n) const
{return elements[n];}
private:
std::string *elements; //指向數組首元素的指針
}
遞增和遞減運算符
定義遞增和遞減運算符的類應該同時定義前置和後置兩個版本, 定義爲類成員。後置版本接受一個額外的(不被使用)的int
類型的形參。
class StrBlobPtr{
public:
//遞增和遞減運算符
StrBlobPtr& operator++(); //前置++
StrBlobPtr operator++(int); //後置++
StrBlobPtr& operator--();//前置--
StrBlobPtr operator--(int);//後置--
}
StrBlobPtr& StrBlobPtr::operator++()
{
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
StrBlobPtr StrBlobPtr::operator++(int)
{
//此處無需檢查有效性,調用前置運算符時才需要檢查
StrBlob ret = *this; //記錄當前的值
++*this; //前置++需要檢查遞增的有效性
return ret;
}
StrBlobPtr& StrBlobPtr::operator--()
{
--curr;
check(curr, "decrement past begin of StrBlobPtr");
return *this;
}
StrBlobPtr StrBlobPtr::operator--(int)
{
//此處無需檢查有效性,調用前置運算符時才需要檢查
StrBlob ret = *this; //記錄當前的值
--*this;//前置--需要檢查遞增的有效性
return ret;
}
成員訪問運算符
class StrBlobPtr{
public:
std::string& operator*() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr]; //*p是對象所指的vector
}
std::string* operator->() const
{
//將實際工作委託給解引用運算符
return & this->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()
標準庫定義的函數對象
表示運算符的函數對象類常用來替換算法中的默認運算符。例如排序算法默認使用operator<
來將序列升序排列,如果要執行降序排列,可以傳入一個greater
類型的對象。
sort(svec.begin(), svec.end(), greater<string>());
標準庫規定其函數對象對於指針同樣適用。
vector<string *> nameTable; //指針的vector
//錯誤:nameTable中的指針彼此之間沒有關係,所以<產生未定義的行爲
sort(nameTable.begin(), nameTable.end(),
[](string *a, string *b) {return a<b;});
//正確:標準庫規定指針的less是定義良好的
sort(nameTable.begin(), nameTable.end(), less<string *>());
可調用對象與function
//列舉了可調用對象與二元運算符對應關係的表格
//所有可調用對象都必須接受兩個int、返回一個int
//其中的元素可以是函數指針、函數對象或者是lambda
map<string, function<int(int,int)>> binops = {
{"+", add}, //函數指針
{"-", std::minus<int>()}, //標準庫函數對象
{"/", divide()}, //用戶定義的函數對象
{"*", [](int i, int j) {return i*j;}), //未命名的lambda
{"%", mod} }; //命名了的lambda對象
注意不能(直接)將重載函數的名字存入function
類型的對象中,解決二義性的方法是存儲函數指針而非函數名字:
int (*fp) (int, int) = add;
binnops.insert( {"+", fp} );