ANSI/ISO C++ Professional Programmer's Handbook(3)

  摘自:http://sttony.blogspot.com/search/label/C%2B%2B

3


運算符重載


by Danny Kalev





簡介


內建運算符一樣可以被擴展以支持用戶定義類型。重載運算符預定義的含義是擴展而不是覆蓋它。雖然普通函數可以提供相同的功能,但是運算符重載提供了一種統一的表示習慣,這比普通的函數調用語法更清楚。例如



Monday < Tuesday; //重載了<
Greater_than(Monday, Tuesday);

運算符重載的歷史可以追述到早期的Fortran語言。Fortran,第一種高級程序設計語言,在50年代中後期以某種方式革命性的提出了運算符重載的概念。第一次,內建運算符如+-能適用於變量數據類型:整型、實型和複數型。到那時爲止,彙編語言——它甚至不支持運算符記號——是程序員的唯一選擇。Fortran的運算符重載限於一套固定的內建數據類型;他們不能被程序員擴展。基於對象的程序設計語言提供了用戶定義的重載運算符。在這樣的語言裏,將一套運算符和用戶定義的類型聯繫起來成爲可能。面向對象的程序設計語言一般也集成運算符重載。


C++中重新定義內建運算符含義的能力是引起批評的源頭。大部分向C++移植的C程序員認爲運算符重載和允許程序員增加、取消、改變語言關鍵字一樣危險。雖然可能帶來混亂,運算符重載還是成爲C++最基本的特性之一,並且泛型程序設計的基本要求(泛型程序設計在第十章“STL和泛型程序設計”討論)。今天,即使試圖不要運算符重載的語言也在加入這種特性。


本章探索運算符重載的好處,也討論潛在的問題。本章也討論適用於運算符重載的小規則。最後,演示運算符重載的特殊形式——轉換運算符。


運算符重載本質上來說是一個函數,其名字是由關鍵字operator打頭的運算符。例如



class Book
{
private:
long ISBN;
public:
//...
long get_ISBN() const { return ISBN;}
};
bool operator < (const Book& b1, const Book& b2) //重載運算符<
{
return b1.get_ISBN() < b2.get_ISBN();
}

運算符重載的唯一規則


C++對運算符重載只有很少的規則。例如,不禁止程序員重載運算符++來執行一個減少操作(但對於期望運算符++執行增量操作的用戶來說,不免感到驚奇)。這樣的濫用和雙關可以產生幾乎不能理解的神祕代碼風格。一般的,重載了運算符的源代碼是不貼近用戶的;因此,以一種意想不到、不直觀的方式重載運算符是不推薦的。


另一種極端——完全放棄運算符重載——也是不實際的選擇,因爲這意味着放棄這種數據抽象和泛型程序設計的重要工具。當你重載運算符以支持用戶定義類型時,推薦你保持相應內建運算符的基本語義。換句話說,重載運算符對操作數和有相同接口的內建運算符都有副作用。


成員和非成員


大多數重載運算符既可以申明爲非靜態的類成員也可以不是成員函數。下面的例子中,運算符==作爲非靜態的類成員重載:



class Date
{
private:
int day;
int month;
int year;
public:
bool operator == (const Date & d ); // 1:成員函數
};

作爲選擇的,它也能申明成一個friend函數(選擇成員函數還是友函數的標準在這一章討論):



bool operator ==( const Date & d1, const Date& d2); // 2:非成員函數
class Date
{
private:
int day;
int month;
int year;
public:
friend bool operator ==( const Date & d1, const Date& d2);
};

雖然如此,運算符[]()=,和->只能被申明爲非靜態類成員;這是爲了確保他們的第一個操作數是左值。


運算符的接口


當你重載運算符時,應該使其與它的內建副本有相同的接口。運算符的接口由用於的操作數組成,不管這些操作數是能被運算符改變的還是返回的結果。例如,考慮運算符==。它的內建版本可以接受廣泛的類型,包括intboolfloatchar以及指針。檢測運算符==的操作數是否相等的基礎計算過程是它的實現細節。然而,內建==運算符檢測左右操作數的一致性和返回一個bool值作爲結果的過程可以被一般化。注意運算符==沒有改變它的任何一個操作數是十分重要的;另外,在運算符==中操作數的順序是無關緊要的。重載運算符==也必須遵循這些行爲。


運算符的一致性


運算符==是二元和對稱的。==的重載版本也應該遵循這種限制。它接受兩個有相同類型的參數。實際上,常使用運算符==測試兩個有截然不同基礎類型的操作數的一致性,例如charint。但是,在這種情況下C++自動的對操作數應用integral promotion(整數提升);結果,在比較之前表面上不同的類型被提升成了相同的類型對稱性限制意味着運算==定義成函數而不是成員函數。爲了使你看清爲什麼,這兒有對兩種版本重載運算符 ==的比較:



class Date
{
private:
int day;
int month;
int year;
public:
Date();
bool operator == (const Date & d) const; // 1 不對稱的
friend bool operator ==(const Date& d1, const Date& d2); //2 對稱的
};
bool operator ==(const Date& d1, const Date& d2);

(1)中將重載運算符==申明爲成員函數與內建運算符==不一致,因爲它接受兩個不同類型的實參。編譯器將成員運算符==翻譯成下列形式:



bool Date::operator == (const Date *const, const Date&) const;

第一個參數是指向const對象的const this指針(記住,this總是一const指針;當成員函數也是const時它指向一個const對象)。第二個參數是const Date的引用。很明顯,兩種類型之間沒有標準的類型轉換存在。另一方面,友函數 版本接受兩個同類型的參數。支持友函數版本有實際的意義。STL算法依賴對稱版本的重載運算符 ==。例如,存儲不支持對稱版本運算符==對象的容器不能排序。


另一個例子是內建運算符+=,其也接受兩個參數,並改變左參數而不改變右參數。重載+=的接口需要反映它改變它的一個對象而保持另一個對象的事實。這通過申明函數爲const來反映,函數自己是非const成員函數。例如



class Date
{
private:
int day;
int month;
int year;
public:
Date();
//內建+=改變它的左操作數而不改變右操作數
//同樣的行爲在這裏保持
Date & operator += (const Date & d);
};

總的來說,每一個重載運算符都必須實現與它的內建運算符相同的接口。如何實現在定義基礎操作和隱藏細節方面是自由的——只要遵循其接口。


運算符重載的限制


就象前面提到的,重載運算符是一個函數,這個函數申明時以關鍵字operator開頭並緊接一個運算符id。運算符可以是下列之一:



new delete new[] delete[]
+ - * / % ^ & | ~
! = < > += -= *= /= %=
^= &= |= << >> >>= <<= == !=
<= >= && || ++ -- , ->* ->
() []

另外。下列運算符的一元和二元形式都能被重載:



+ - * &

重載運算符以同樣的方式從基類繼承。注意。這個規則不適用類隱含申明的而不是用戶申明的賦值運算符。 因此,基類賦值運算符總是被派生類的拷貝賦值運算符隱藏。(賦值運算符在第四章“特殊成員函數:默認構造器,拷貝構造器,銷燬器和分配運算符特殊成員函數:默認構造器,拷貝構造器,銷燬器和賦值運算符”討論)


重載運算符只能用於用戶自定義類型


重載運算符必須至少接受一個用戶自定義類型(運算符newdelete是例外——詳細參見第十一章“內存管理”)。這條規則保證只包含基本類型的表達式的含義不能被用戶改變。例如



int i,j,k;
k = i + j; //總是使用內建的=和+


創造新的運算符是不允許的


重載運算符擴展內建的運算符,所以你不能引入新的運算符到語言(轉換運算符不同於普通的運算符)。下面的例子試圖重載@運算符,但是因爲上面的原因不能編譯:



void operator @ (int); //非法,@不是內建運算符或類型名

優先級和參數個數


運算符的優先級和參數的個數都不能改變。例如,重載&&只能有兩個參數——和內建運算符&&一樣。另外,當運算符重載時它的優先級也不能改變。兩個或多個重載運算符的序列,例如t2<t1/t2,按內建運算符的優先級規則求解。因爲除法運算符的級別高於小於號,表達式總是被解釋成t2<(t1/t2)


默認參數


不象普通函數。重載運算符不能申明一個有默認值的參數(運算符()是這條規則的例外;稍後討論)。



class Date
{
private:
int day;
int month;
int year;
public:
Date & operator += (const Date & d = Date() ); //error,默認參數
};

這條規則可能很武斷,但這來源於內建運算符的行爲,他們從沒有操作數。


不能被重載的運算符


有一些運算符不能被重載。 因爲他們接受一個名字而不是一個對象作爲右值。這些運算符是:



  • 直接成員訪問,運算符.



  • 指向類指針成員,.*




  • 範圍決定,運算符::


  • 求佔內存大小,運算符sizeof


條件運算符?:也不能被重載。


另外,新的類型轉換運算符——static_cast<>dynamic_cast<>reinterpret_cast<>,和const_cast<>——和###預處理記號不能重載。


轉換運算符


發現C++代碼和C代碼一起使用是常見的。例如,以前用C語言寫的系統可以通過面向對象的接口重新包裝。這種用兩種語言構造的系統常常需要同時支持雙重的接口——一種適應面向對象環境,另一種滿足C的環境。實現特殊數字實體——比如複數和二進制數——和不足一字節數據的類也傾向於使用轉換運算符來使自己和基本類型平滑的交互。


字符串是一個需要雙接口的典型例子。字符串對象可能不得不用在僅僅支持以NULL結尾char數組 的上下文中。例如



class Mystring
{
private:
char *s;
int size;
public:
Mystring(const char *);
Mystring();
//...
};
#include <cstring>
#include "Mystring.h"
using namespace std;
int main()
{
Mystring str("hello world");
int n = strcmp(str, "Hello"); //編譯期錯誤:
//str不是const char *類型
return 0;
}

C++爲這種情況提供一種自動的類型轉換。轉換運算符可以被認爲是用戶自定義的類型轉換運算符;在需要特定類型的上下文中將這種對象轉換成不同的類型。轉換是自動的。例如



class Mystring //現在有轉換運算符
{
private:
char *s;
int size;
public:
Mystring();
operator const char * () {return s; } //將Mystring轉換成C的字符串
//...
};
int n = strcmp(str, "Hello"); //OK,自動將str轉換成const char *

轉換運算符與普通的重載運算符相比有兩點不同。第一,轉換運算符不需要返回值(void也不需要)。返回值由運算符的名字推出。


第二,轉換運算符不需要參數。


轉換運算符可以將對象轉換成任何類型,基本類型和用戶定義類型都行:



struct DateRep //以前的C代碼
{
char day;
char month;
short year;
};
class Date //面向對象包裝
{
private:
DateRep dr;
public:
operator DateRep () const { return dr;} //自動轉換成DateRep
};
extern "C" int transmit_date(DateRep); //基於C的通訊API
int main()
{
Date d;
//...使用d
//將數據對象作爲二進制流傳送到遙遠的客戶方
int ret_stat = transmit_date; //使用以前的通訊API
return 0;
}

標準轉換VS.用戶定義轉換


用戶定義轉換和標準轉換的交互能帶來不受歡迎的驚奇和副作用,因此必須謹慎使用。測試下面具體的例子。


顯式的只帶一個參數的構造器也是一個轉換運算符,它將參數轉換成這個類的對象。但編譯器必須解決一個重載函數調用時,它除了考慮標準的以外還要考慮象這樣用戶定義的。例如



class Numeric
{
private:
float f;
public:
Numeric(float ff): f(ff) {} //構造器也是一個float到Numeric的
//轉換運算符
};
void f(Numeric);
Numeric num(0.05);
f(5.f); //OK,調用void f(Numeric)。Numeric的構造器將
//參數轉換成Numeric對象

假設你稍後又增加了一個重載版本的f()



void f (double);

現在相同的函數調用有不同的解決:



f(5.f); //現在調用f(double)而不是f(Numeric)

這是因爲float被自動提升爲double,以匹配一個函數特徵。這是一個標準類型轉換。另一方面,floatNumeric的轉換是一個用戶定義轉換。用戶定義轉換級別低於標準轉換——在重載函數確定中;結果函數調用解決不同。


因爲這些現象,轉換運算符收到強烈的批評。一些程序設計課禁止使用他們。但是,轉換運算符對在雙接口的程序中是——有時不可避免——一個有用的工具,就象你看到的。


後綴和前綴運算符


對於早期的類型,C++區別++x;x++;,也區別--x;x--;。在這些情況下,對象也必須區別前綴和後綴重載運算符(例如,作爲優化的度量。參見第十二章“優化你的代碼”)。後綴運算符申明瞭一個虛假的int參數,而他們的前綴版本沒有。例如



class Date
{
public:
Date& operator++(); //前綴
Date& operator--(); //前綴
Date& operator++(int unused); //後綴
Date& operator--(int unused); //後綴
};
void f()
{
Date d, d1;
d1 = ++d;//前綴:先增加d然後賦值給d1
d1 = d++; //後綴:先賦值然後增加d
}

使用函數調用語法


重載運算符的調用只是普通函數調用的一種變形。你可以使用顯式的函數調用來調用重載運算符,例如:



bool operator==(const Date& d1, const Date& d2);
void f()
{
Date d, d1;
bool equal;
d1.operator++(0); //等價於:d1++;
d1.operator++(); //等價於:++d1;
equal = operator==(d, d1);//等價於:d==d1;
Date&(Date::*pmf) (); //指向成員函數的指針
pmf = & Date::operator++;
}

和諧的運算符重載


無論什麼時候你重載了象+-的運算符,你也必須支持相應的+=-=運算符。編譯器不會自動的替你做。考慮下面的例子:



class Date
{
public:
Date& operator + (const Date& d); //注意:運算符+=未定義
};
Date d1, d2;
d1 = d1 + d2; //正確;使用重載的+和默認的賦值運算符
d1 += d2; //編譯期錯誤:“沒有用戶爲類Date定義運算符+=”

理論上講,編譯器能通過合併賦值運算符和重載運算符+來合成運算符+=,這樣表達式d1 += d2;被自動的擴展成d1 = d1+d2。但是,這是不受歡迎的,因爲自動擴展比一個用戶自定義版本低效。自動擴展版本建立了一個臨時對象,而用戶定義版本避免了這一點。而且,不能想象,一個有重載運算符+的類故意沒有運算符+=


通過值返回對象


爲了效率,大型對象常常通過引用或指針來傳遞給函數——或從函數返回。但是,仍然有少數情況最好的選擇是通過值返回對象。運算符+是這種情況的例子。它必須返回一個對象,但不能改變它的任何一個操作數。表面上自然的選擇是,分配一個對象返回它的地址。然而,這不是一個好主意。動態內存分配比本地存儲要慢的多。動態分配也可能失敗並拋出異常,異常再被處理程序截獲。更糟的是,這個解決方案是一種錯誤傾向,因爲誰刪除這個對象成了不清楚的問題——創建者還是用戶?


另一個解決方法是使用靜態對象並返回引用。例如



class Year
{
private:
int year;
public:
Year(int y = 0) : year(y) {}
Year& operator + (const Year& other) const; //返回本地靜態Year對象的
//引用
int getYear() const;
void setYear(int y);
};
Year& Year::operator + (const Year& other) const
{
static Year result;
result = Year(this->getYear() + other.getYear() );
return result;
}

靜態對象解決了所有權問題,但它仍是有問題的:在重載運算符的每一個調用中,靜態對象相同的實例被修改並返回給調用者。相同的對象能通過引用返回給許多用戶。這些用戶不知道他們保存的是一個共享的實例,這個實例在他們使用之後就被修改了。


最後,最安全和最有效的的解決方法仍然是通過值返回:



class Year
{
private:
int year;
public:
Year(int y = 0) : year(y) {}
Year operator + (const Year& other) const; //通過值返回Year對象
int getYear() const;
void setYear(int y);
};
Year Year::operator + (const Year& other) const
{
return Year(this->getYear() + other.getYear() );
}

多重重載


重載運算符服從函數重載的規則。因此,運算符 可以被重載多次。什麼時候有用呢?考慮下面的Month類和它關聯的運算符 ==



class Month
{
private:
int m;
public:
Month(int m = 0);
};
bool operator == (const Month& m1, const Month &m2);

由於隱含intMonth轉換運算符,可以用重載運算符==來比較簡單的 int值和Month對象。例如



void f()
{
int n = 7;
Month June(6);
bool same =
(June == n); //calls bool operator == (const Month& m1, const Month &m2);
}

這工作的很好,但這是低效的:參數n先被轉換成一個臨時的Month對象,然後於 June對象比較。你可以通過定義運算符==的另外的重載版本來避免臨時對象的構造:



bool operator == (int m, const Month& month);
bool operator == (const Month& month, int m);

因此,表達式June == n將調用下面的重載運算符:



bool operator == (const Month& month, int m);

這個重載版本不會創造臨時對象,所以它是更有效的。同樣是關於性能的考慮導致C++標準化委員會爲std::string和其他的標準庫中的類定義了三種不同版本的運算符==(參見第十章“STL和泛型程序設計”)。


用戶定義類型的重載運算符


你也可以爲enum類型定義重載運算符。例如,象++和--這樣的運算符是很有用的,因此他們可以通過給定enum類型的枚舉值來重複。你可以這樣做:



#include <iostream>
using namespace std;
enum Days
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
};
Days& operator++(Days& d, int) //後綴++
{
if (d == Sunday)
return d = Monday; //轉回去
int temp = d; //轉換成一個int
return d = static_cast<Days> (++temp);
}
int main()
{
Days day = Monday;
for (;;) //將days作爲整數顯示
{
cout<< day <<endl;
day++;
if (day == Sunday)
break;
}
return 0;
}

如果你想以符號而不是整數來顯示枚舉值,重載運算符<<也可以:



ostream& operator<<(ostream& os, Days d) //以符號形式顯示Days
{
switch
{
case Monday:
return os<<"Monday";
case Tuesday:
return os<<"Tuesday";
case Wednesday:
return os<<"Wednesday";
case Thursday:
return os<<"Thursady";
case Friday:
return os<<"Friday";
case Saturday:
return os<<"Satrurday";
case Sunday:
return os<<"Sunday";
default:
return os<<"Unknown";
}
}

重載下標運算符


對於包含數組元素的多樣的類,可以通過重載下標運算符來方便的訪問單一元素。記住,永遠定義兩個版本的下標運算符:const版本和非const版本。例如



class Ptr_collection
{
private :
void **ptr_array;
int elements;
public:
Ptr_collection() {}
//...
void * operator [] (int index) { return ptr_array[index];}
const void * operator [] (int index) const { return ptr_array[index];}
};
void f(const Ptr_collection & pointers)
{
const void *p = pointers[0]; //調用const版本的運算符[]
if ( p == 0)
return;
else
{
//...使用p
}
}

函數對象


函數對象是作爲一個包含函數調用運算符重載版本的類來實現的。這樣一個類的實例可以象一個函數一樣使用。普通函數可以有任意數目的參數;因此,運算符()在其他運算符中是一個例外。因爲它可以接受任意數目的參數。另外,它可以接受默認參數。在下面的例子裏,一個函數對象實現了一個泛型的增量函數:



#include <iostream>
using namespace std;
class increment
{
//一個泛型的增量函數
public : template < class T > T operator() (T t) const { return ++t;}
};
void f(int n, const increment& incr)
{
cout << incr(n); //輸出1
}
int main()
{
int i = 0;
increment incr;
f(i, incr);
return 0;
}

總結


運算符重載的概念並非新也不是C++獨有。它是實現數據抽象和混合類的最基本的工具。在許多方面,C++中重載運算符象普通函數:他們被繼承,他們可以被重載多次,他們即可申明爲非靜態成員也可申明爲非成員函數。但是,有一些限制適用與重載運算符。重載運算符有確定的參數數目,而且不能有默認參數。另外,運算符的一致性和優先級不能被改變。內建運算符有一個由運算符適用的操作數和運算符返回的結果組成的接口。當你重載運算符時,建議你保持和內建運算符相同的接口。


轉換運算符是一種特殊類型的重載運算符。他們與普通重載運算符相比在兩個地方需要關注:他們不返回值,也不需要任何參數。


發佈了23 篇原創文章 · 獲贊 4 · 訪問量 28萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章