如何有效地進(jìn)行運(yùn)算符重載 重載運(yùn)算符將使代碼更清晰-只在合理使用它們. by Bill Wagner 譯者:黃森堂(vcmfc) C++初學(xué)者(特別是從其他語(yǔ)言“叛逃”而來(lái)的)往往視運(yùn)算符重載為一大絆腳石,害怕 改變內(nèi)建運(yùn)算符的原意(參閱“What is Operator Overloading?”)。殊不知,這卻能 使得代碼清晰,比如"C=A+B"和"C.assign(A.plus(B))",您更喜歡哪一個(gè)呢?若不重載 運(yùn)算符,您只能選擇累贅的后者了。 考慮重載運(yùn)算符時(shí),您應(yīng)站在客戶的角度,為代碼的使用者想想。不妨先自問一下,我 這樣做是提高還是降低代碼的可讀性,再由此做出恰當(dāng)?shù)臎Q定。而其中的關(guān)鍵并不在于 實(shí)現(xiàn)函數(shù)功能,而是由于每種運(yùn)算符都有其約定俗成的含義,重載它們應(yīng)是在保留原有 含義的基礎(chǔ)上對(duì)功能的擴(kuò)展,而非改變。所以應(yīng)時(shí)刻站在使用者的角度來(lái)審視,比如為 擴(kuò)展的算術(shù)類型而重載算術(shù)運(yùn)算符就是很自然的,以復(fù)數(shù)為例: Complex c1( 1.0, -4.0 ); Complex c2( 15.0, 1.0 ); Complex c3 = c1 * c2; c2 *= c1; 那么對(duì)于非算術(shù)類(例如一個(gè)Document類),自然就不應(yīng)當(dāng)重載算術(shù)運(yùn)算符,這會(huì)讓我 們面對(duì)一群陌生的運(yùn)算符而不知所措。從這里我們可以得到一點(diǎn)重載運(yùn)算符的心得:對(duì) 于特定的類,如果一眼能看出某個(gè)運(yùn)算符在此處的含義,那么函數(shù)實(shí)現(xiàn)往往也很容易; 若連您自己對(duì)其作用都有所遲疑,那么請(qǐng)罷手,連您自己都不能很快反應(yīng)過(guò)來(lái),何況別 人呢? I/O組: <<, >> 這是通常IO流運(yùn)算符重載的規(guī)則,你應(yīng)該這樣寫,在實(shí)際上,大部分的客戶端預(yù)期在你 的類已實(shí)現(xiàn)這些運(yùn)算符來(lái)支持io流,如何你的類支持序列化的話,你應(yīng)該實(shí)現(xiàn)這樣功能 ,除了在MFC中支持CArchive之外應(yīng)該實(shí)現(xiàn)的功能。你可以在你的類中始終實(shí)現(xiàn)這些功能 ,除非你明確不支持存儲(chǔ)你的對(duì)象到文件與從文件中讀取。例如:MFC的CString支持序 列化,但CDC不支持,CString聲明一個(gè)string,它將能進(jìn)行存儲(chǔ)到文件與從文件中恢復(fù), CDC是設(shè)備,它是操作系統(tǒng)資源但不能恢復(fù)。 以下運(yùn)算符實(shí)現(xiàn)了在任何支持序列化的類中實(shí)現(xiàn)提取與插入: ostream& operator << (ostream& o, const MyClass& c); istream& operator >> (istream& I, MyClass& c); 注釋:以上兩個(gè)運(yùn)算符是公共函數(shù),非成員函數(shù),第一個(gè)參數(shù)對(duì)每一個(gè)函數(shù)來(lái)說(shuō)是相同 的,都是IO流。如果兩個(gè)運(yùn)算符是成員函數(shù),它們需要內(nèi)部的iostream類,iostream類 是標(biāo)準(zhǔn)C++庫(kù)的一部分,追加新的定義,你不能在內(nèi)部實(shí)現(xiàn)流運(yùn)算符,他們必須是公共的 ,在你的類中友元你寫的函數(shù),那么就可以存取所有的內(nèi)部變量。 比較與賦值:=, !=, == 這些運(yùn)算符是實(shí)現(xiàn)在任何類中實(shí)現(xiàn)你想拷貝值的賦值運(yùn)算,如果在你的類中寫了拷貝構(gòu) 造,那么你得實(shí)現(xiàn)這些功能,再一次強(qiáng)調(diào),這些功能是新增到你的類的公共部分,在公 共部分包含了這些運(yùn)算符并顯露它們。 所有的比較運(yùn)算符都有關(guān)系到賦值運(yùn)算,你通常在你的類中提供了拷貝構(gòu)造與賦值操作 ,然后,如果你支持賦值,那么你也要支持同等的比較對(duì)象的能力,公共實(shí)現(xiàn)了!=與== 操作,在實(shí)際上,如果你明確隱含了賦值操作,比較操作仍然值得做 MFC的CRect類提供了這些所有運(yùn)算符,盡管你設(shè)想有兩個(gè)矩形相等的理由.MFC的CBrush 類不提供,將賦值操作創(chuàng)建兩個(gè)brush來(lái)指向系統(tǒng)資源或創(chuàng)建新的系統(tǒng)資源?任何一個(gè)都 可以,程序員在讀客端的CBrush賦值聲明將不明白如何管理下面的系統(tǒng)資源,客戶端程 序員可能需要知道 // as global operators: bool operator == (const MyClass& l, const MyClass& r) { bool isEqual; //... test each member, // set isEqual. Return isEqual; }; bool operator != (const MyClass& l, const MyClass& r) { return ! (l == r); } // As member functions: bool operator == (const MyClass& r) const { bool isEqual; //... test each member, // set isEqual. Return isEqual; }; bool operator != (const MyClass& r) const { return ! (*this == r); } 次序關(guān)系:<, >, <=, >= 標(biāo)準(zhǔn)模板庫(kù)在它所有的查找與排序算法中使用次序關(guān)系,如果你實(shí)現(xiàn)這些功能那么你隨 時(shí)都可以在你的類中使用自然的次序比較,沒有一個(gè)MFC對(duì)象包裝GDI對(duì)象來(lái)支持這些操 作,然而,CString類卻支持所有的。 STL使用小于運(yùn)算符來(lái)進(jìn)行排序與查找的比較,且STL算法只需要小于運(yùn)算符,不需要其 它。STL通過(guò)限制這種需求小于運(yùn)算符,不要在你的類中把這種限制強(qiáng)加于所有用戶上。 我更喜歡給予客戶端存取所有的次序比較,同樣,如果類已定義了次序關(guān)系,所有同等 的運(yùn)算符都得定義,你得避免在其它條件中用函數(shù)實(shí)現(xiàn)它們,你得到避免在小組的其它 地方使用同等的函數(shù)。注意:如果我在小組中實(shí)現(xiàn)了>=,我就會(huì)實(shí)現(xiàn)<、<=、>。一般認(rèn)為 只要實(shí)現(xiàn)>與<就可以,這是錯(cuò)誤的,因?yàn)楫?dāng)兩個(gè)對(duì)象相等的時(shí)候。 bool operator < (const MyClass& r) const { // ... test each member. }; bool operator >= (const MyClass& r) const { return !(*this < r) } bool operator > (const MyClass& r) const { // ... test each member. }; bool operator <= (const MyClass& r) const { return !(*this > r); } 遞增與遞減:++, -- 這些運(yùn)算符是遞增與遞減類對(duì)象,在以下三種情形你需要重載它們:類型指示器(譯者: 像STL的Iterator),順序存取與整型類型。類型的整數(shù)模型的支持要有明確的意義。 類型指示器是個(gè)指向vector類型,數(shù)據(jù)庫(kù)光標(biāo),似類的指示器,標(biāo)準(zhǔn)C++庫(kù)提供少量有關(guān) 這方面的例子,在STL中iterator支持遞增與與遞減,在一些場(chǎng)合,這些操作進(jìn)行向前與 向后移動(dòng)對(duì)象集,增加那些函數(shù)在類來(lái)包裝數(shù)據(jù)的存??;通過(guò)遞增與遞減來(lái)向前與向后 移動(dòng)數(shù)的行數(shù)。 我通過(guò)使用順序存取來(lái)循環(huán)取得color,通過(guò)遞增與遞減來(lái)移動(dòng)到向后與向前的color上( 譯者:我覺得作者的意思:如果你要提供像操縱數(shù)據(jù)庫(kù)中的數(shù)據(jù)表的上一筆與下一筆等 功能的話,你就需要重載它)。 只要你想重載遞增與遞減運(yùn)算符,你要記住每個(gè)操作符有前遞增與后遞增的版本,以下 是遞增的例子: int i=5; int j = i++; // j == 5, i == 6. j = ++i; // i == 7, j == 7. 注釋:返回值有對(duì)于前遞增與后遞增是不同的,C++語(yǔ)言定義了前遞增版本是無(wú)參數(shù),后 遞增是有一個(gè)整數(shù)參數(shù),你的類應(yīng)該象如下定義: //前遞增,例:++i MyClass& operator++ () { // ... whatever is necessary. return *this; } //后遞增,例:i++ MyClass operator ++ (int) { // Make a copy. MyClass rval (*this); // ... whatever is necessary. // to increment *this. // Return the old value. return rval; } 在通常駐,你應(yīng)該支持前遞增與后遞增的兩個(gè)版本,但有時(shí)候,只需要前遞增版本,后 遞增版本需要你拷類對(duì)象;有時(shí),拷貝對(duì)象是昂貴的代價(jià),所以只支持前遞增;當(dāng)對(duì)象使 用大多的內(nèi)存來(lái)進(jìn)行拷貝構(gòu)造的時(shí)候,我不贊成支持后遞增,類的使用者不知道沒有拷 貝構(gòu)造的支持是無(wú)法支持后遞補(bǔ)增的。 加法,減法:+, -, +=, -= 這些運(yùn)算符執(zhí)行通常駐的算術(shù)運(yùn)算,有兩種情形你需要重載它們:首先,你的類需要模 擬數(shù)學(xué)模型,第二是當(dāng)你的類是要模擬指針.通常的錯(cuò)誤是認(rèn)為只需要重載少量的運(yùn)算符 ,而我認(rèn)為應(yīng)該成組重載,因?yàn)橛脩粼谑褂媚愕念悤r(shí)認(rèn)為它們已經(jīng)全部重載了。 +=與-=比二元運(yùn)算符+能更好工作,二元運(yùn)算符版本在返回結(jié)果創(chuàng)建了你的類的拷貝,當(dāng) 函數(shù)執(zhí)行時(shí)候它們創(chuàng)建了臨時(shí)對(duì)象。 MyClass operator + (const MyClass& r) const { MyClass rVal; // perform operations, storing the // result in rVal. return rVal; } MyClass& operator += (const MyClass& r) { // perform operations, storing the // result in this. return *this; } 乘法: *, /, *=, /= 整除:%, %= 這些運(yùn)算符執(zhí)行通常的算術(shù)乘法與除法,像加法與減法運(yùn)算符,只有在你的類中需要模 擬處理這些類型時(shí)才需要重載它們,我分開整數(shù)乘法與除法是因?yàn)樗鼈冎挥性谡麛?shù)類型 才有意義。 相似的其它運(yùn)算符,如果你的類重載過(guò)一個(gè),那么你應(yīng)該重載相關(guān)的全部運(yùn)算符,這些 函數(shù)的原型與加法與減法是一樣的,例如:*、/相似于+,它們都有更多效的一元運(yùn)算符 (如:*=,/=),因此,當(dāng)你重載其中的一個(gè),那么人就得重載全部。 內(nèi)存管理組:new delete C++ new與delete運(yùn)算符是在用戶需要在公共內(nèi)存中產(chǎn)生新對(duì)象或刪除已存在對(duì)象而被調(diào) 用,你可以寫類的專有版本函數(shù)或公共版本;只有在你的類對(duì)象或派生類的創(chuàng)建中使用類 的專有版本。 只有一個(gè)理由來(lái)重載這些函數(shù):重載它們來(lái)改善你的應(yīng)用程序的性能,如果你重載這些 函數(shù),你必須提供相同的界面,相同的外部行為與相同的功能。前面所說(shuō)的,很少重載 new和delete運(yùn)算符的全局的form。這么做會(huì)產(chǎn)生很嚴(yán)重的后果,因?yàn)樗鼓愕膸?kù)與跟已 經(jīng)與new和delete運(yùn)算符的標(biāo)準(zhǔn)form相連接的庫(kù)產(chǎn)生沖突。而且,它使你的代碼與所有可 能產(chǎn)生同樣結(jié)論的庫(kù)相沖突。例如,MFC使用全局的new和delete運(yùn)算符來(lái)避免內(nèi)存的泄 漏。 我創(chuàng)建了類的專有版本的new和delete,在我運(yùn)行了我的一個(gè)應(yīng)用程序的配置文件并發(fā)現(xiàn) 內(nèi)存的分配和釋放是瓶頸的時(shí)候。一般來(lái)說(shuō),在程序運(yùn)行過(guò)程中有很多小的對(duì)象被創(chuàng)建 和刪除,例如一個(gè)簡(jiǎn)單的自由手繪工具。鼠標(biāo)的每次移動(dòng)都在圖形的一系列點(diǎn)中增加一 個(gè)新點(diǎn),而每個(gè)點(diǎn)都包含兩個(gè)整數(shù)。無(wú)論何時(shí)使用者創(chuàng)建了一個(gè)新的圖形,可能會(huì)增加 成百上千的點(diǎn)。如果使用者刪除了一個(gè)圖形,可能有成百上千的點(diǎn)被刪除。 你可以通過(guò)很多的途徑調(diào)用new和delete。無(wú)論何時(shí)你重載new和delete,你要保證在所 有的form中重載是有意義的。首先,在標(biāo)準(zhǔn)的form里重載new和delete,無(wú)論何時(shí)生成一 個(gè)對(duì)象都得調(diào)用類的專有版本。第二,重載new的數(shù)組form版:運(yùn)算符new[]。如果你重 載了這兩個(gè)form,只有當(dāng)用戶需要一個(gè)對(duì)象而不是對(duì)象的數(shù)組的時(shí)候,你才可以得到你 改良版的new和delete。你需要為你寫的每個(gè)new的form寫一個(gè)delete。 要認(rèn)識(shí)到new和delete是繼承的,即使它們是靜態(tài)的函數(shù)。這意味著你的專有版本的new 和delete是被你自己的類派生出來(lái)的類所調(diào)用。偶然的是,你的類的專有版本的new和d elete函數(shù)是基于對(duì)象的大小的,并且是你的專有的函數(shù) class MyClass { public: // Two forms of operator new: static void* operator new(size_t size); // Regular new static void* operator new [](size_t size); // array new // Similar forms of operator // delete: static void operator delete (void* rawMemory, size_t size); static void operator delete[](void* rawMemory, size_t size); } 在重載它們之前你需要充分理解所有標(biāo)準(zhǔn)版本newdelete的內(nèi)部行為,記得你重載了new 或delete,你必須重載兩者;同樣,new與delete在許多form是不同的,你要確信你能解 決所有的問題。 指針引用標(biāo)識(shí)符:->, * 使用這些運(yùn)算符來(lái)操縱指向內(nèi)存中自已的引用,如果p是指向整數(shù),那么*p的值就是整數(shù) ;如果pRect是指向CRect對(duì)象,pRect->left是矩形的左值。 只有當(dāng)你要模擬指針的時(shí)候才需要重載這些運(yùn)算符,標(biāo)準(zhǔn)庫(kù)auto_ptr定義了它們與STL中 所有的iterator,當(dāng)你需要自已的版本函數(shù),你得遵守相同的應(yīng)用規(guī)則,我建議寫兩個(gè)版 本:const對(duì)象版與隨機(jī)存取對(duì)象版,如果你不寫這兩個(gè)版本,在你的新類中適當(dāng)使用c onst方式,你需要非const版本。因此,當(dāng)客戶端程序員使用的智能指針去修改指向?qū)ο?BR>,你需要cons版本來(lái)處理你的類的const對(duì)象,如果你沒有定義這個(gè)運(yùn)算符,你就不能使 用const智能指針去存取指向的對(duì)象。 class MyClass { public: _Ty* operator -> (); const _Ty* operator->() const; _Ty& operator* (); const _Ty& operator*()const; }; 函數(shù)調(diào)用標(biāo)識(shí)符:() 使用函數(shù)運(yùn)算符來(lái)處理你的類的函數(shù)類型,當(dāng)類支持函數(shù)運(yùn)算符來(lái)調(diào)用你的函數(shù)對(duì)象,在 兩種情形下你需要重載函數(shù)運(yùn)算符:以前,程序員使用這種調(diào)用來(lái)提供兩維或更多給的 數(shù)組類: class TwoDArray { public: float operator () (int x, int y) const; float& operator () (int x, int y); }; 但現(xiàn)在,在更多的場(chǎng)合里在你使用這個(gè)函數(shù)來(lái)提供函數(shù)對(duì)象來(lái)使用STL算法(在這個(gè)文檔 里它不能完全代替函數(shù)對(duì)象,可參考其它信息),例如:排序函數(shù)使用函數(shù)對(duì)象來(lái)比較對(duì) 象類型: class CompareObjects { public: // Function call operator: bool operator () ( const MyClass& l, const MyClass& r) const { bool rVal = false; // various tests. return rVal; }; }; // usage: extern vector < MyClass > array; sort (array.begin (), array.end (), CompareObjects ()); 注解:在兩個(gè)示例里,你可以自由定義數(shù)字與其它參數(shù)類型的函數(shù),你可以在任何地方 調(diào)用運(yùn)算符。 數(shù)組存取標(biāo)識(shí)符:[] 使用[]來(lái)存取數(shù)組中的單個(gè)元素,你需要重載它們的唯一情形:模擬數(shù)組;當(dāng)你重載這個(gè) 運(yùn)算符你需要明白兩點(diǎn):首先,[]是二元運(yùn)算符,這意味著這個(gè)運(yùn)算符只有一個(gè)參數(shù), 你不能使用這個(gè)運(yùn)算符來(lái)使用多維數(shù)組;第二,你想在數(shù)組中使用任意類型的數(shù)據(jù),這 允許你結(jié)合數(shù)組來(lái)創(chuàng)建任何類型,例如: class MyDictionary { public: // Array operator: string operator [] (const string& word) const { string definition = "who knows"; // find definiton. return rVal; }; // To change a definition: string& operator [] (const string& word); }; // usage: extern MyDictionary Websters; string def = Websters["alligator"]; 位操作:^, &, |, ~, ^=, &=, |= 它們都是整數(shù)值的位運(yùn)算符,我從未到達(dá)使用重載來(lái)應(yīng)用位運(yùn)算,它們只定義了整數(shù)類 型,但我找不到更好的理由來(lái)模擬整數(shù)類型。 不能使用:!, <<=, >>=, &&, || !是邏輯非運(yùn)算符,它不能在表達(dá)式的右邊(譯者:可參閱Guru of the week#26 Boolean ),<<=與>>=是賦值移位運(yùn)算符,它們執(zhí)行了在對(duì)象中進(jìn)行左移或右移并返回結(jié)果,使用 公共運(yùn)算符來(lái)混合使用;&&是邏輯AND運(yùn)算符,與||是邏輯OR運(yùn)算符。 我看到!運(yùn)算符來(lái)進(jìn)行任何對(duì)象的有效性測(cè)試,這在你不記得它的時(shí)候,這是個(gè)好主意, 如果對(duì)象是true,那么對(duì)象是有效的,至少到目前為止我只看到過(guò)這種用法;>>=與<<=不 能使用是因?yàn)樗鼈冎荒苓M(jìn)行位運(yùn)算,除非你的類是整數(shù)值,否則重載移位是沒有意義的 。 &&與||不能重載是因?yàn)樗鼈兣c正常的表達(dá)式計(jì)算規(guī)則沖突,逗號(hào)是不能重載的是因?yàn)槟?BR>是沒有意義的 運(yùn)算符重載是有用的技術(shù),但你需要恰當(dāng)?shù)厥褂脩?yīng)用它們,你也需要確定在客戶端什么 時(shí)候使用你重載的運(yùn)算符,使得代碼更簡(jiǎn)練與明了,始終要求你自已實(shí)現(xiàn)重載運(yùn)算符比 用正常的函數(shù)更加清楚描繪你的意圖。 |
|