1. 程式人生 > >C++重載運算簡介

C++重載運算簡介

... alua ast 認識 希望 ive his 輸入流 資源

本文基於《C++ Primer(第5版)》中14章和《More Effective C++》條款7,整理而成。

其實寫這篇博客之前,內心還是很忐忑的,因為,博主的水平很有限,視野比較窄,要是在理解書的過程中有了偏差,給讀到這篇博客的人以錯誤的認識,那罪過就大了。再次聲明本文僅是簡介,若是有錯誤的地方歡迎留言指出。

個人認為運算符最重要的是:使用與內置類型一致的含義。

一、基本概念

  • 當運算作用於類類型的運算對象時,可以通過運算符重載重新定義該運算符的含義。

重載的運算符是具有特別名字的函數,它們的名字由關鍵字 operator和其後要定義的運算符號共同組成。其包括:返回類型、參數列表以及函數體。

重載運算符函數的參數數量與該運算符作用的運算對象一樣多,如:一元運算符有一個參數,二元運算符有兩個。這裏值得註意的是,當運算符函數時成員函數時,則它的第一個(左側)運算對象綁定到隱式的this指針上,因此,成員運算符函數(顯式)參數數量比運算符的運算對象總數少一個,但實際上總數不變。

1、某些運算符不應該被重載

不能被重載的運算符有 :

. .* :: ?:
new delete sizeof typeid
static_cast dynamic_cast const_cast reinterpret_cast

能被重載但最好不要重載的運算符有:

(1)邏輯與&&,邏輯或||

邏輯與運算符和邏輯或運算符都是先求左側運算對象的值再求右側運算對象的值,當且僅當左側運算對象無法確定表達式的結果時,才會計算右側運算對象的值,這種策略稱為短路求值(short-circuit evaluation)。

《More Effective C++》給出的例子,若重載operator &&,下面的這個式子:

1 if(expression1 && expression2) ...

會被編譯器視為以下兩者之一:

1 //假設operator&& 是個成員函數
2 if(expression1.operator&&(expression2)) ...
3 4 //假設operator&& 是個全局函數 5 if(operator&&(expression1,expression2)) ...

即無法保留內置運算符的短路求值屬性,兩個運算對象總是被求值。

(2)逗號 ,

逗號操作符的求值順序是從左往右依次求值,逗號運算符的真正的結果是右側表達式的值。若是打算重載逗號運算符,就必須模仿這樣的行為,但是,無法執行這些必要的模仿。求值順序和返回結果同時滿足才行。

2、選擇成員函數或者非成員函數

當我們定義重載的運算符是,必須首先決定是將其聲明為類的成員函數還是聲明為一個普通的非成員函數。(有關成員函數和非成員函數,見成員函數與非成員函數的抉擇)。下面有些準則有助於我們選擇:

(1)賦值(=)、下標([ ])、調用( ( ))、和成員訪問箭頭(->)運算符必須是成員;

(2)復合賦值運算符一般來說應該是成員,但並非是必須的;

(3)改變對象狀態的運算符或者與給定類型密切相關的運算符,如,遞增、遞減和解引用運算符,通常是成員;

(4)具有對稱性的運算符可能轉換任意一段的運算符對象,如,算術、相等性、關系和位運算等,因此它們通常應該是普通的非成員函數。

二、各種重載

1、重載輸出運算符<<和輸入運算符>>

(1)重載輸出運算符<<

通常情況下,輸出運算符的第一個形參是一個非常量ostream對象的引用,之所以ostream是非常量是因為向流寫入內容會改變其狀態,而該形參是引用時因為我們無法直接復制一個ostream對象;第二個形參一般是一個常量的引用,是引用的原因是希望避免復制實參,而之所以該形參可以是常量是因為(通常)打印對象不會改變對象的內容。另外,為了與其他輸出運算符保持一致,返回ostream形參。

1 ostream &operator<<(ostream &os,const Sales_data &item)
2 {
3     os<<item.isbn()<<" "<<item.avg_price();
4     return os;
5 }

(2)重載輸入運算符>>

通常,輸入運算符的第一個形參是運算符將要讀取的流的引用,第二形參是將要讀入的(非常量)對象的引用。該運算符通常會返回某個給定流的引用。第二個形參之所以必須是個非常量時因為輸入運算符本身的目的就是將數據讀入到這個對象中。

 1 istream &operator>>(istream &is,Sales_data &item)
 2 {
 3     double price;
 4     is>>item.bookNo>>item.units_sold>>price;
 5     if(is)      //必須處理可能失敗的情況
 6     {
 7         item.revenue=item.units_sold*price;
 8     }
 9     else        //若失敗,則對象唄賦予默認狀態
10     {
11         item=Sales_data();
12     }
13     return is;
14 }

輸入時的可能發生以下錯誤:

當流含有錯誤類型的數據時讀取操作可能失敗;當讀取操作達到文件末尾或者遇到輸入流的其他錯誤是也會失敗。

輸入運算符應該負責從錯誤中恢復。

(3)輸入輸出運算符必須是非成員函數

若是類的成員,它們的左側運算對象將是我們類的一個對象:

1 Sales_data data;
2 data<<cout;     //如果operator<<是Sales_data的成員

假如輸入輸出運算符是某個類的成員,則它們也必須是istream或ostream的成員,然而,這個類屬於標準庫,並且我們無法給標準庫中的類添加任何成員。

2、算術和關系運算符

通常,把算術和關系運算符定義成非成員函數以允許對左側或右側的運算對象進行轉換,因為這些運算符一般不需要改變運算對象的狀態,所以形參都是常量的引用。

(1)算術運算符

算術運算符通常會計算它的兩個運算對象並得到一個新值,這個值有別於任意一個運算對象,常常位於一個局部變量之內,操作完成之後返回該局部變量的副本作為其結果。

1 Sales_data operator+(const Sales_data &lhs,const Sales_data &rhs)
2 {
3     Sales_data sum=lhs;
4     sum+=rhs;
5     return sum;
6 }

(2)相等運算符

C++中的類通過定義相等運算符來檢查兩個對象是否相等,會對比對象的每一個數據成員,只有當所有的對應的成員都相等時,才認為兩個對象相等。

 1 bool operator==(const Sales_data &lhs,const Sales_data &rhs)
 2 {
 3     return lhs.isbn()==rhs.isbn()&&
 4            lhs.units_sold==rhs.units_sold&&
 5            lhs.revenue==rhs.revenue;
 6 }
 7 
 8 bool operator !=(const Sales_data &lhs,const Sales_data &rhs)
 9 {
10     return !(lhs==rhs);
11 }

3、賦值運算符

賦值運算符必須定義為成員函數

(1)拷貝賦值

拷貝賦值運算符接受一個與其所在類相同類型的參數:

1 class Foo
2 {
3 public:
4     Foo &operator=(const Foo&);     //賦值運算符
5     //...
6 }

(2)移動賦值運算符

移動賦值運算符不拋出任何異常,將它標記為noexecpt,類似拷貝賦值運算符,移動賦值運算符必須正確處理自賦值:

 1 StrVec &StrVec::operator=(StrVec &&rhs) noexecpt
 2 {
 3     if(this !=&rhs)     //直接檢測自賦值
 4     {
 5         free();         //釋放已有元素
 6         elements=rhs.elements;      //從rhs接管資源
 7         first_free=rhs.first_free;
 8         cap=rhs.cap;
 9         rhs.elements=rhs.first_free=rhs.cap=nullptr;    //將rhs置於可析構狀態
10     }
11     return *this;
12 }

(3)標準庫vector類定義的第三種賦值運算符,該運算符接受花括號內的元素列表作為參數,如:

1 vector<string> v;
2 v={"a","an"};

運算符添加到StrVec類中時:

1 StrVec &StrVec::operator=(initiallizer_list<string> il)
2 {
3     //alloc_n_copy分配內存空間並從給定範圍內拷貝元素
4     auto data=alloc_n_copy(il.begin(),il.end());
5     free();         //銷毀對象中的元素並釋放內存空間
6     elements=data.first;    //更新數據成員使其指向新空間
7     first_free=cap=data.second;
8     return *this;
9 }

4、遞增和遞減運算符

(1)定義前置遞增、遞減運算符

它們首先調用check函數檢驗StrBolbPtr是否有效,若是,接著檢查給定的索引值是否有效,若是check函數沒有拋出異常,則運算符返回對象的引用。

 1 class StrBlobPtr
 2 {
 3 public:
 4     StrBlobPtr& operator++();
 5     StrBlobPtr& operator--();
 6 }
 7 
 8 StrBlobPtr& StrBlobPtr::operator++()
 9 {
10     //若curr已經指向容器的尾後位置,則無法遞增它
11     check(curr,"increment past end of StrBlobPtr");
12     ++curr;
13     return *this;
14 }
15 
16 StrBlobPtr& StrBlobPtr::operator--()
17 {
18     --curr;
19     check(curr,"decrement past begin of StrBlobPtr");
20     return *this;
21 }

(2)定義後置遞增、遞減運算符

和前置比較後置版本接受一個額外的(不使用)int 類型的形參。

 1 class StrBlobPtr
 2 {
 3 public:
 4     StrBlobPtr& operator++(int);
 5     StrBlobPtr& operator--(int);
 6 }
 7 
 8 StrBlobPtr& StrBlobPtr::operator++(int)
 9 {
10     StrBlobPtr ret=*this;   //記下當前值
11     ++*this;                //需要前置++檢查是否有效
12     return *ret;            //返回之前記錄的狀態
13 }

另外,其他運算符見書《C++ Primer(第5版)》。

C++重載運算簡介