810 likes | 1.01k Views
第十四章. 運算子覆載. 第十四章 運算子覆載. 在 C++ 語言中,程式設計師除了可以對函式(包含成員函式)進行覆載之外,也可以對運算符號進行覆載,以擴充運算符號的功能,此稱為「運算子覆載」 (Operator Overloading) 。當然由於運算子分為單元運算子、二元運算子、前置運算子、後置運算子,因此運算子的覆載也比函式覆載稍微複雜一些,因此我們將運算子覆載獨立出來在本章作詳細的介紹。. 大綱. 14.1 運算子覆載的需求 14.2 運算子覆載 14.3 單元運算子的覆載 14.4 二元運算子的覆載 14.5 單元運算與二元運算同時存在的覆載
E N D
第十四章 運算子覆載
第十四章 運算子覆載 • 在C++語言中,程式設計師除了可以對函式(包含成員函式)進行覆載之外,也可以對運算符號進行覆載,以擴充運算符號的功能,此稱為「運算子覆載」(Operator Overloading)。當然由於運算子分為單元運算子、二元運算子、前置運算子、後置運算子,因此運算子的覆載也比函式覆載稍微複雜一些,因此我們將運算子覆載獨立出來在本章作詳細的介紹。
大綱 • 14.1 運算子覆載的需求 • 14.2 運算子覆載 • 14.3 單元運算子的覆載 • 14.4 二元運算子的覆載 • 14.5 單元運算與二元運算同時存在的覆載 • 14.6 轉型運算子的覆載 • 14.6.1 物件轉基本資料型態 • 14.6.2 不同類別型態間的轉換 • 14.7 「=」與「= =」運算子覆載 • 14.7.1 「=」運算子覆載 • 14.7.2 「==」運算子覆載 • 14.8 「<<」與「>>」運算子覆載 • 14.8.1 「<<」運算子覆載 • 14.8.2 「>>」運算子覆載 • 14.9 本章回顧
14.1 運算子覆載的需求 • 在前面的章節中,我們使用運算子進行運算時,使用的都是運算子內定的功能,例如基本的四則運算子:加「+」、減「-」、乘「*」、除「/」,比較運算子的比較運算:大於「>」、等於「= =」、小於「<」… 等等。 • 舉例來說,如果要進行兩個整數a,b的相加,則只要透過加號運算子「+」即可完成,也就是a+b,之所以能夠如此做,是因為C/C++編譯器已經內建了「+」運算子的數學加法功能。 • 事實上,「+」運算子原本就已經具有內定的覆載功能,因為它不但能進行整數的相加,還能進行浮點數的相加,甚至它還能夠自行依照資料型態作自動轉型的動作。雖然「+」運算子的功能已經非常強大,但它仍無法對我們所定義的結構體及類別(物件)進行運算,這是因為編譯器並不了解應該如何對這些由程式設計師所定義的資料型態作運算才是適當的。例如變數a與變數b為複雜的矩陣Matrix類別所產生的物件,編譯器對於a+b就不知道該如何解決。
14.1 運算子覆載的需求 • 為了使程式設計更方便,C++允許某些運算子被另行定義一些新功能,以便處理由程式設計師所定義的資料型態,這就是運算子覆載的主要目的。 • 【註】: • 在上述範例中,當然我們也可以於Matrix類別中定義一個Matrix Sum(Matrix Var)成員函式,並透過c=a.Sum(b);完成矩陣的相加,但這卻不如c=a+b;來得方便與直觀,因此運算子覆載仍有其必要性及方便性。 圖14-1 運算子覆載的必要性
14.2 運算子覆載 • 我們已經了解運算子覆載的用途後,本節將介紹如何實作運算子覆載。事實上,運算子覆載的實作方法與定義類別中的成員函式覆載相似,也就是在類別內宣告覆載運算子,以及在類別內或類別外具體定義覆載運算子的新運算行為。 • 覆載運算子需要先在類別內宣告,其宣告語法如下: • 語法說明: • (1)語法中的『#』代表的是運算子符號,例如「+」、「-」、「>>」、「--」等等。 • (2)運算子的運算可以區分為不需要回傳值及需要回傳值。若不需要回傳值,則回傳資料型態應宣告為void。 回傳資料型態 operator#(傳入引數);
14.2 運算子覆載 • (3)傳入引數與函式的傳入引數類似,但有些不同。運算子的運算對象可以區分為單元運算(例如傳統的a++;)及二元運算(例如傳統的a+b;)。如果是二元運算則後面的運算元(第二個運算元)需設定為引數(前面的運算元不必設定為引數,因為這是在類別內定義,因此該類別的物件將自動隱含成為第一個運算元)。如果是單元運算,則又可以分為兩種狀況,如下所述。 • (4)單元運算分為兩種狀況,一種是前置運算子(例如傳統的++a;),一種是後置運算子(例如傳統的a++;)。宣告前置運算子覆載時不需要指定引數,宣告後置運算子覆載時則需要設定引數為int。 • (5)運算子的覆載還有一些限制如下:
14.2 運算子覆載 • 限制一:覆載運算子需符合C++語法。 • 限制二:程式設計師無法新創運算子符號,只能針對C++原有的運算符號進行覆載,同時有些運算子是無法被覆載的,整理如下: 表14-1 可進行覆載的運算子
14.2 運算子覆載 • 限制三:在可被覆載的運算子中,有些運算子只能被定義為單元運算子,如下表: • 限制四:在可被覆載的運算子中,有些運算子可以被定義為單元運算子或二元運算子,如下表: 表14-2 不可進行覆載的運算子 表14-3 只能被定義為單元運算子 表14-4 可被定義為單元運算子或二元運算子
14.2 運算子覆載 • 限制五:無法重新定義運算元的個數(如限制四與限制三的規定)。 • 限制六:無法重新定義運算子優先權。 • 限制七:無法覆蓋運算子的原有功能。也就是內建的資料型態無法使用運算子覆載重新定義,例如下列語法欲重新定義兩浮點數相加是錯誤的語法。
14.2 運算子覆載 • 當宣告運算子覆載後,即可在類別外定義運算子覆載的內容,語法如下: • 語法說明: • 我們也可以將宣告與定義合併於類別內,此時就不需指定類別名稱。而事實上,覆載運算子函式也屬於該類別的一個成員函式,我們稱之為覆載函式或覆載成員函式。 回傳資料型態 類別名稱::operator#(傳入引數) { ……… 定義運算子覆載的運算行為………… };
14.3 單元運算子的覆載 • 在本節中,我們將介紹單元運算子的覆載,單元運算子分為前置運算子與後置運算子兩種,首先我們先複習第三章所介紹的前置運算子與後置運算子定義。 • 前置運算子(例如:++i): • 運算元進行加一或減一的動作後,再進行其他運用。 • 後置運算子(例如:i++): • 運算元先進行其他運用,再進行加一或減一的動作。 • 對於前置運算子而言,其覆載宣告語法如下: • 對於後置運算子而言,其覆載宣告語法如下: 回傳資料型態 operator#(); 回傳資料型態 operator#(int);
14.3 單元運算子的覆載 • 語法說明: • (1)前置運算子覆載宣告與後置運算子覆載宣告的分別在於是否有傳入引數int(int是固定語法,無法修改為其他如float等資料型態)。 • (2)前置運算子與後置運算子的運算內容可以不同。 • (3)若無回傳資料,則回傳資料型態為void,此時若前置運算子與後置運算子的行為相同,則無論是前置運算子或後置運算子都不會影響整個運算式的結果。 • (4)若有回傳資料,則應該宣告回傳資料的資料型態,此時前置運算子應先進行運算行為,再回傳資料;後置運算子則應回傳原有資料再進行運算行為(這需要一些技巧)。
14.3 單元運算子的覆載 • 範例: • 【觀念範例14-1】:宣告及定義單元運算子「++」的覆載,使得對自訂類別Matrix2D具有運算功能,扮演前置運算子,將進行累加2,扮演後置運算子,將進行累加3。不論是前置或後置運算子都不會回值資料。 • 範例14-1:ch14_01.cpp(檔案位於隨書光碟 ch14\ch14_01.cpp)。
14.3 單元運算子的覆載 • 執行結果: • 範例說明: • (1)第15~19行是「++」前置運算子的覆載定義。 • (2)第20~25行是「++」後置運算子的覆載定義。 • (3)第42行的++ObjA會呼叫第15~19行的覆載定義。 • (4)第48行的ObjB++會呼叫第20~25行的覆載定義。 • (5)當我們覆載了「++」運算子之後,編譯器會根據作用運算元的資料型態來決定運算方法,舉例如下: ObjA= | 2 , 2 | | 2 , 2 | ------------- ObjB= | 3 , 3 | | 3 , 3 |
14.3 單元運算子的覆載 • 在範例14-1中,若後置運算子也設定為矩陣內容+2,則由執行結果中,將看不出來後置運算子與前置運算子的差別,這是因為我們並要求覆載的前置或後置運算子回傳資料。事實上,如果我們讓覆載的前置或後置運算子回傳資料,也能夠讓前置運算子與後置運算子的效果有所不同。 • 舉例來說,在C++中,若「++」作為前置運算子,則會先進行加一的動作後,再將結果回傳。若「++」作為後置運算子,則會先傳遞變數值,然後才進行加一的動作。如下圖:
14.3 單元運算子的覆載 圖14-2 前置運算子 與後置運算子的差別
14.3 單元運算子的覆載 • 【觀念及實用範例14-2】:參考「++」運算子的原意,覆載「++」的前置與後置運算子功能,使其可適用於Matrix2D類別的矩陣遞增運算。 • 範例14-2:ch14_02.cpp(檔案位於隨書光碟 ch14\ch14_02.cpp)。
14.3 單元運算子的覆載 原本ObjA= | 0 , 0 | | 0 , 0 | ObjB= | 0 , 0 | | 0 , 0 | 經ObjB=++ObjA;運算後 ObjA= | 1 , 1 | | 1 , 1 | ObjB= | 1 , 1 | | 1 , 1 | ============= 原本ObjC= | 0 , 0 | | 0 , 0 | ObjD= | 0 , 0 | | 0 , 0 | 經ObjD=ObjC++;運算後 ObjC= | 1 , 1 | | 1 , 1 | ------------- ObjD= | 0 , 0 | | 0 , 0 | • 執行結果: • 範例說明: • (1)第15~19行是「++」前置運算子的覆載定義。它會回傳一個Matrix2D類別的物件,由於前置運算子的原意是先進行運算再回傳運算結果,因此,回傳時只要將原本物件內容回傳即可,因此,我們透過this指標達成這項工作。 • (2)第20~26行是「++」後置運算子的覆載定義。雖然後置運算子的原意和前置運算子的原意恰好相反,但我們卻無法單純地將第17、18行的順序對調作為覆載運算定義,因為這會先遇到return而造成遞增運算未被執行,故我們先使用TempObj存放原始矩陣內容,最後也是回傳TempObj,以符合後置運算子的原意。
14.4 二元運算子的覆載 • 除了少數的「單元」運算子,絕大多數的運算子都屬於「二元」運算子,也就是運算元有兩個,例如A%B的『%』取餘數就是一個二元運算子,其運算元有A與B兩個。在覆載二元運算子時,由於我們一定是定義在某類別內,因此,必定有一個物件是內定的運算元,在傳統加法中,被加數就是那一個運算元,例如ObjA+ObjB,我們應該把『+』運算子覆載在ObjA的所屬類別內。 • 除了內定的運算元外,二元運算子的另一個運算元的資料型態則必須明確定義於覆載運算子的宣告中,因此,覆載二元運算子的宣告語法如下: • 語法說明: • (1)二元運算一般都具有回傳值,如果沒有回傳值則可將回傳資料型態設為void。 • (2)宣告時,變數名稱可以省略,但定義時變數名稱不可省略,而且通常該變數會在函式內被使用。 回傳資料型態 operator#(另一個運算元的資料型態 變數名稱);
14.4 二元運算子的覆載 • 舉例來說,假設X、Y、Z皆是Matrix2D類別的物件,其運算子呼叫運算子覆載函式的示意圖如下: 圖14-3 二元運算子覆載示意圖
14.4 二元運算子的覆載 • 【觀念及實用範例14-3】:將Matrix2D矩陣類別(矩陣元素資料型態修正為double),加入二元運算子「*」乘法功能,乘法對象可以是矩陣,也可以是實數。其中矩陣相乘公式如下: • 範例14-3:ch14_03.cpp(檔案位於隨書光碟 ch14\ch14_03.cpp)。
14.4 二元運算子的覆載 ObjA= | 2 , 4 | | 6 , 8 | ObjB= | 1 , 9 | | 4 , 16 | ------------- ObjC=ObjA*3= | 6 , 12 | | 18 , 24 | ObjD=ObjA*ObjB= | 18 , 82 | | 38 , 182 | • 執行結果: • 範例說明: • (1)第20行,宣告覆載『*』運算子,使其能夠應用於Matrix2D類別,進行矩陣*數學常數的運算,其定義(運算行為)紀錄於第27~36行。 • (2)第21行,宣告覆載『*』運算子,使其能夠應用於Matrix2D類別,進行矩陣*矩陣的運算,其定義(運算行為)紀錄於第37~46行。 • (3)第62行會呼叫第27~36行(3將會被變數R接收,而this將指向物件ObjA)進行矩陣*數學常數的運算,並將回傳值存放於ObjC中。 • (4)第65行會呼叫第37~46行(ObjB將會被物件變數R接收,而this將指向物件ObjA)進行矩陣*矩陣的運算,並將回傳值存放於ObjD中。
14.5 單元運算與二元運算同時存在的覆載 • 不知讀者是否發現一個問題,單元運算的後置運算必須將傳遞引數設為int,而二元運算則必須將第二個運算元設為引數。這在允許進行單元運算及多元運算的運算子時(如-,*),是否可能會造成混淆?請回顧範例14-3的第20行,如果我們將其運算對象設為整數(int),並將運算子改為『-』號,請問我們想要進行的是『負號』單元運算子的覆載,還是『減號』二元運算子的覆載呢? • 答案其實很簡單,因為同時允許進行單元運算及二元運算覆載的運算子是無法進行後置運算子覆載的,例如我們通常會以「-a」代表「負a」,而不會以「a-」代表「負a」,因此,下列語法代表宣告的是覆載『-』二元運算子,其引數代表第二個運算元為整數(絕對不是代表覆載『-』後置單元運算子)。
14.5 單元運算與二元運算同時存在的覆載 • 在解答上述疑惑後,同一符號的單元運算覆載與多元運算覆載就變得單純多了,我們只要遵守14.2節的限制四即可。以下,我們透過範例實作『-』的負號覆載與減法覆載。 • 【觀念及實作範例14-4】:『-』的負號覆載(單元前置運算子)與減法覆載(二元運算子)。 • 範例14-4:ch14_04.cpp(檔案位於隨書光碟 ch14\ch14_04.cpp)。
14.5 單元運算與二元運算同時存在的覆載 • 執行結果: • 範例說明: • (1)第20行,宣告覆載『-』運算子的前置運算覆載,其定義(運算行為)記錄於第27~36行。 • (2)第21行,宣告覆載『-』運算子的二元運算覆載,其定義(運算行為)記錄於第37~46行。 • (3)第62行會呼叫第27~36行進行矩陣*(-1)的運算,並將回傳值存放於ObjC中。 • (4)第65行會呼叫第37~46行進行矩陣-矩陣的運算,並將回傳值存放於ObjD中。 ObjA= | 2 , 4 | | 6 , 8 | ObjB= | 1 , 9 | | 4 , 16 | ------------- ObjC=-ObjA= | -2 , -4 | | -6 , -8 | ObjD=ObjA-ObjB= | 1 , -5 | | 2 , -8 |
14.6 轉型運算子的覆載 • 「()」是一種非常特殊的運算子,它通常用在引數的宣告或運算式優先權的改變,在這些狀況下,它是無法被覆載的。除此之外,「()」若搭配某種資料型態,還可以使用在強制型別轉換,在C語言及C++語言中,強制型別轉換具有不同的語法格式,假設A為整數變數,B為浮點數,欲將B的數值設定給A,C與C++的語法如下: • C語法 C++語法A = (int) B; A = int(B); • 「()」的強制型別轉換不但可以轉換為基本資料型態,也可以用在轉換結構體與類別的自訂型態轉換。當「()」使用在強制型別轉換時,它是可以被覆載的運算子,但僅限於C++語法的型別轉換,因為運算子覆載是C++提供的功能,C語言並不存在這種能力。
14.6 轉型運算子的覆載 • 當然,對於「()」的強制型別轉換需求可能有下列四種狀況: • 狀況一:從基本資料型別轉換為基本資料型別。這是C++原有功能,無法也不需要被覆載。 • 狀況二:從基本資料型別轉換為自訂資料型別。這是C++原有功能,無法也不需要被覆載。 • 狀況三:從自訂資料型別轉換為基本資料型別。可以在類別內宣告為覆載。於14.6.1節中說明。 • 狀況四:從自訂資料型別轉換為自訂資料型別。可以在類別內宣告為覆載。於14.6.2節中說明。 • 如果現在A為浮點變數,但是B為程式設計師自行定義的向量類別物件,主要用來儲存某個向量的x與y軸的分量,我們是否能利用類似上面的轉型動作,來求出此向量的長度呢? • 在C++語言中,答案是可以的,我們可以利用「( )」運算子的覆載,讓物件也具有這種轉型功能,我們將在本節中加以說明。
14.6.1 物件轉基本資料型態 • 當我們欲將物件強制轉型為基本資料型態時,其回傳值必定是某一種指定的基本資料型態(例如float型態)。事實上,由於物件包含的成員變數可能不只一個,通常我們會想要將多筆資料換算成單一筆資料,都是發生在欲求出某種具有意義的數值時才會使用,例如求出最大值max(),但基本資料型別的名稱都已經固定,因此,將物件強制轉型為基本資料型態的案例並不多見。(例如我們想要求出物件成員變數的最大值,會以成員函式max()來加以解決。) • 暫且不論物件轉基本資料型態的實際用途為何!C++仍提供了此一功能,其語法如下: • 覆載轉型運算子「()」的宣告語法如下(轉為基本資料型態): • 覆載轉型運算子「()」的定義語法如下(轉為基本資料型態): operator 欲轉換的基本資料型態( ); 類別名稱::operator 欲轉換的基本資料型態( ) { ……………..轉換方式…………… return 欲轉換型態的變數或常數; }
14.6.1 物件轉基本資料型態 • 語法說明: • (1)宣告必須在類別內。宣告與定義可以合併在類別內,此時就不需要指定類別名稱。 • (2)轉換資料型態一定會有回傳值,但不需要指定回傳值的資料型態,因為回傳值的資料型態就是欲轉換的基本資料型態。 • (3)轉換方式可以是任何合法的程式內容,您可以直接將某個成員變數經由轉型後回傳,或經過複雜的運算後,再決定要回傳什麼資料。 • (4)假設物件A為float浮點數,B為Matrix2D類別的物件,下圖可說明覆載『()』運算子執行的過程。 圖14-4 覆載強制轉型運算子「()」示意圖
14.6.1 物件轉基本資料型態 • 【觀念範例14-5】:覆載double ( )運算子,使物件也具有轉換資料的能力,其轉換過程為,取出成員變數中的最大值,並且將之乘以2倍後回傳(如果該值非double型態,則必須先將之轉型為double型態)。 • 範例14-5:ch14_05.cpp(檔案位於隨書光碟 ch14\ch14_05.cpp)。
14.6.1 物件轉基本資料型態 • 執行結果: • 範例說明: • (1)第20行,宣告覆載轉型運算子『()』,轉型後的資料型態為double,其定義(運算行為)記錄於第27~36行。 • (2)第51行會呼叫第27~36行進行轉型,並將回傳值存放於X中。依據題意及程式內容,X將會是ObjA成員變數中最大值的兩倍。 ObjA= | 1.3 , 7.5 | | 9.7 , 6.2 | X=19.4
14.6.2 不同類別型態間的轉換 • 類別強制轉型運算子的覆載,在物件轉物件時(兩物件隸屬不同類別)通常比較有幫助,例如,我們可能希望將2x2矩陣物件轉型為3x3矩陣物件(將原有元素保留,其餘填0)。在C++中,類別轉類別型態的語法如下: • 覆載轉型運算子「()」的宣告語法如下(轉為自訂類別資料型態): • 覆載轉型運算子「()」的定義語法如下(轉為自訂類別資料型態): • 語法說明: • 語法與轉基本資料型態類似,只不過將之改為已宣告過的合法類別名稱。 operator 欲轉換的類別資料型態( ); 類別名稱::operator 欲轉換的類別資料型態( ) { ……………..轉換方式…………… return 欲轉換類別的物件; }
14.6.2 不同類別型態間的轉換 • 【觀念範例14-6】:覆載( )運算子,使Matrix22物件可轉換為Matrix33物件,轉換後原有元素將以兩倍出現,而新元素將被設定為100。 • 範例14-6:ch14_06.cpp(檔案位於隨書光碟 ch14\ch14_06.cpp)。