1. 程式人生 > >函式過載和虛擬函式

函式過載和虛擬函式

虛擬函式和函式過載
本文來自:http://www.cnblogs.com/j2eee/archive/2006/09/22/511954.html

函式過載
如何講函式過載:
What——函式過載是什麼?
why——為什麼要用函式過載,沒有函式過載會怎樣?
how——舉例說明怎麼使用函式過載
*******************************************************************************
        能使名字方便使用,是任何程式設計語言的一個重要特徵。
        當我們建立一個物件(即變數)時,要為這個儲存區取一個名字。一個函式就是一個操作的名字。正是靠系統描述各種各樣的名字,我們才能寫出易於人們理解和修改的程式。象寫文章一樣,目的在於如何同讀者交流。這裡就產生了這樣一個問題:如何把人類自然語言的有細微差別的概念對映到程式語言中。
        通常,自然語言中同一個詞可以代表許多種不同的含義,這要依賴上下文來確定。這就是所謂的一詞多義,從程式設計的角度來說就是該詞被過載了
。所謂過載,從詞義上說就是重新載入,用句俗話說就是“換湯不換藥”。要學會試著理解這一點,這點非常有用,特別是對於細微的差別。函式過載是用來描述同名函式具有相同或者相似功能,但資料型別或者是引數不同的函式管理操作的稱呼。所謂函式過載是指同一個函式名可以對應著多個函式的實現。每種實現對應著一個函式體,這些函式的名字相同,但是函式的引數的型別或返回值不同。具個例子:
        我們可以說“喝可樂,喝酒”。如果非得說成“喝(喝可樂的喝)可樂,喝酒(喝酒的喝)酒”,那將是很愚蠢的,就好像聽話的人對指定的動作毫無辨別能力一樣。管他和的麼事,總不是讓液體進入你的體內,至於麼樣喝,喝了以後起麼樣的反應,那就是後話了。
        然而大多數程式語言要求我們為每個函式設定一個唯一的識別符號。如果我們想列印三種不同型別的資料:整型、字元型和實型,我們通常不得不用三個不同的函式名,如print_int( )、print_char( )和print_float( ) ,這些既增加了我們的程式設計工作量,也給讀者理解程式增加了困難。舉個吃飯的例子,你桌子上面有三盤菜,你吃第一盤菜要用金筷子,第二盤,要用銀筷子,第三盤要用象牙筷子一樣,幾麻煩,用個方便筷子不是一樣的吃了。 說白了,那雙方便筷子就是一個函式過載的例項。
         在C + +中,還有另外一個原因需要對函式名過載:建構函式
。因為建構函式的名字預先由類的名字確定,所以只能有一個建構函式名。但如果我們想用幾種方法來建立一個物件時該怎麼辦呢?例如建立一個類,它可以用標準的方法初始化,也可以從檔案中讀取資訊來初始化,我們就需要兩個建構函式,一個不帶引數(預設建構函式),另一個帶一個字串作為引數,以表示用於初始化物件的檔案的名字。(例子:兩雙方便筷子,一雙是新的剛那出來的,另一個是高溫消毒好了的!但都是方便筷子。)所以函式過載的本質就是允許函式同名。在這種情況下,建構函式是以不同的引數型別被呼叫的
         過載不僅對建構函式來說是必須的,對其他函式也提供了很大的方便,包括非成員函式。
        另外,函式過載意味著,我們有兩個庫,它們都有一個同名的函式,只要它們的引數不同就不會發生衝突。
        我這裡講的主題不是函式過載,而是讓大家如何方便是使用“筷子”(函式名)去取用餐桌上的“美味食物”(呼叫函式)!
        函式過載允許多個函式同名(象多種形式的筷子一樣),但還有另一種方法使函式呼叫更方便。
如果我們想以不同的方法(象很多不同的手法那筷子一樣,左手拿,右手拿)呼叫同一函式,該怎麼辦呢?當函式有一個長長的引數列表,而大多數引數每次呼叫都一樣時,書寫這樣的函式呼叫會使人厭煩,程式可讀性也差。C + +中有一個很通用的作法叫預設引數。預設引數就是在使用者呼叫一個函式時沒有指定引數值而由編譯器插入引數值的引數。(象吃了飯,筷子碗一丟,不想洗碗筷,讓你父母來洗一樣。) 這樣f (“hello”) , f (“hi”,1)和f (“howdy”,2 ,‘c’)可以用來呼叫同一函式。它們也可能是呼叫三個已過載的函式,但當引數列表相同時,我們通常希望呼叫同一函式來完成相同的操作。

關於函式過載更深層的說明(編譯器)
         void print(char);
         void print(float);
        無論這兩個函式是某個類的成員函式還是全域性函式都無關緊要。如果編譯器只使用函式名字的範圍,編譯器並不能產生單一的內部識別符號,這兩種情況下都得用_ print結尾。(就象一雙方便筷子,光看握筷子的手,我們誰都不曉得他夾了什麼菜。)
        過載函式雖然可以讓我們有同名的函式,但這些函式的引數列表應該不一樣。所以,為了讓過載函式正確工作,編譯器要用函式名來區分引數型別名。上面的兩個在全域性範圍定義的函式,可能會產生類似於_ print _ char和_ print _ float的內部名。(看到他夾了麼菜了)
        因為,為這樣的名字分解規定一個統一的標準毫無意義,所以不同的編譯器可能會產生不同的內部名。

        過載的函式與具有多型性的函式(即虛擬函式)不同處在於
呼叫正確的被過載函式實體是在編譯期間就被決定了的;而對於具有多型性的函式來說,是通過執行期間的動態繫結來呼叫我們想呼叫的那個函式實體。多型性是通過重定義(或重寫)這種方式達成的。請不要被過載(overloading)和重寫(overriding)所迷惑過載是發生在兩個或者是更多的函式具有相同的名字的情況下。區分它們的辦法是通過檢測它們的引數個數或者型別來實現的。
*********************************************************
虛擬函式
        什麼是虛擬函式???
        虛擬函式是指一個類中你希望過載的成員函式,當你用一個基類指標或引用指向一個繼承類物件的時候,你呼叫一個虛擬函式,實際呼叫的是繼承類的版本。   ——摘自MSDN
        什麼是多型???
        多型是面向物件程式設計和麵向過程程式設計的主要區別之一,何謂多型?一名俗話說:“龍生九子,子子不同”多型就是同一個處理手段可以用來處理多種不同的情況。
        這裡我們主要討論虛擬函式的格式、條件(什麼樣的函式才叫虛擬函式)、呼叫虛擬函式時的注意事項。

        虛擬函式是成員函式,而且是非static的成員函式。說明虛擬函式的方法如下:
        virtual <型別說明符><函式名>(<引數表>)
        其中,被關鍵字virtual說明的函式稱為虛擬函式。
        如果某類中的一個成員函式被說明為虛擬函式,這就意味著該成員函式在派生類中可能有不同的實現。(例:一句俗話就是“聾子的耳朵——擺設”,就象一個殘疾人士,可能他聽不見,但不代表他兒子聽不見,我們一般把基類叫做“父類”,派生類叫做“子類”。但我們不能說“龍生龍,鳳生風,老鼠生兒會打洞”啊!他老爸的耳朵是擺設,但在兒子這裡就是接受資訊的器官了。)
        (1) 與基類的虛擬函式有相同的引數個數
        (2) 其引數的型別與基類的虛擬函式的對應引數型別相同
        (3) 其返回值或者與基類虛擬函式的相同,或者都返回指標或引用,並且派生類虛擬函式所返回的指標或引用的基型別是基類中被替換的虛擬函式所返回的指標或引用的基型別的子型別。(就像他老爹不識字,但是他可以讓他讀大學的兒子來帶他看信,讀報一樣)
        一般要求基類中說明了虛擬函式後,派生類說明的虛擬函式應該與基類中虛擬函式的引數個數相等,對應引數的型別相同,如果不相同,則將派生類虛擬函式的引數的型別強制轉換為基類中虛擬函式的引數型別。

抽象類

        帶有純虛擬函式的類稱為抽象類。抽象類是一種特殊的類,它是為了抽象和設計的目的而建立的,它處於繼承層次結構的較上層。抽象類是不能定義物件的,在實際中為了強調一個類是抽象類,可將該類的建構函式說明為保護的訪問控制權限。
        抽象類的主要作用是將有關的組織在一個繼承層次結構中,由它來為它們提供一個公共的根,相關的子類是從這個根派生出來的。(例:就是一個手機模具)
        抽象類刻畫了一組子類的操作介面的通用語義,這些語義也傳給子類。一般而言,抽象類只描述這組子類共同的操作介面,而完整的實現留給子類。
        抽象類只能作為基類來使用,其純虛擬函式的實現由派生類給出如果派生類沒有重新定義純虛擬函式,而派生類只是繼承基類的純虛擬函式,則這個派生類仍然還是一個抽象類。如果派生類中給出了基類純虛擬函式的實現,則該派生類就不再是抽象類了,它是一個可以建立物件的具體類了。

一個例項:
class 人()
{public :
//......
void 吃()
{人吃飯;
}
//......
char *Name;
int age;
};

class 狗()
{public :
//......
void 吃()
{狗吃屎;
}
//......
char *Name;
int age;
};
        人類、狗類有一些相同的東東,如名字,年紀,吃的動作等,有人想到了為了程式碼的重用,讓人類繼承狗類,可是資料的冗餘讓這個想法完蛋了,所以有人又想出可不可以定義一個這樣的類:
        這個類界於人類狗類之間,有人類和狗類共有的一些東東,比如年紀,名字,體重什麼的,但它是不存在例項的,它的存在意義也是隻是為了派 生其它類,如人類、狗類,這樣可以使系統清淅、。。。。。、、反正好處多多。

        在這個世界上根本不存在界於人狗之間的東東,所以這個“人狗之間類”中的“吃”也是沒什麼意義,你也很難為它的內容下個定義,況且也沒必要,所以定義它為純虛擬函式,形式為:virtual void 吃()=0; 用來告訴系統:
       1、這個函式可以沒有函式定義;
       2、擁有本函式的類是抽象類;
       你可能會說,即然純虛擬函式沒什麼意義,為什麼還要它,它的作用是什麼呢? 為實現多型作準備!!!

虛擬函式的作用:

之一: 虛實同型
之二: 相伴永遠
之三: 有則用之,無則虛之

關於二義性的單獨說明:
        什麼是二義性?從字面上來說就是產生的歧義,為什麼會產生歧義?這裡我舉個例子:象你到商店去買菸,都是黃鶴樓,有150,24,18三個價位的,都是煙,也就是說煙是他們的基類,而你要買哪包抽呢?跟跟老闆將:“拿包黃鶴樓!”他哪知道你買多少錢的煙?這裡就產生了二義性!同時也引出了二義性的解決方法!老闆就會問你:“買多少錢一包的!”這裡你就可以做選擇了!但是編譯器不會開口問你呼叫哪個父類的成員函式!所以你要事先說明!(y::gety())
例:隱式型別轉換導致過載函式產生二義性
# include <iostream.h>
void output( int x); // 函式宣告
void output( float x); // 函式宣告
void output( int x)
{
cout << " output int " << x << endl ;
}
void output( float x)
{
cout << " output float " << x << endl ;
}
void main(void)
{
int x = 1;
float y = 1.0;
output(x); // output int 1
output(y); // output float 1
output(1); // output int 1
// output(0.5); // error! ambiguous call, 因為自動型別轉換
output(int(0.5)); // output int 0
output(float(0.5)); // output float 0.5
}
第一個output 函式的引數是int 型別,第二個output 函式的引數是float 型別。由於數字本身沒有型別,將數字當作引數時將自動進行型別轉換(稱為隱式型別轉換)。語句output(0.5)將產生編譯錯誤,因為編譯器不知道該將0.5 轉換成int 還是float 型別的引數。隱式型別轉換在很多地方可以簡化程式的書寫,但是也可能留下隱患。

總結:

         用個例子字來總結,函式過載,是靜態的多型(靜態聯編),你可以想象成為,你去餐館吃飯,吃著不同的菜(不同數量,不同型別的引數),用的是同一雙筷子(函式名),自己動手吃,不能指使別人幫你實現功能(自己動手,風記足食)。而虛擬函式,是動態的多型(動態聯編),你則可以想象為,你要麼是兒孫滿堂的老人或者是有錢的款爺,到飯店吃大餐,自己不動手,而且吃得很講究,不同的食物要用不同的餐具(不同的物件),而且是使喚別人(子類成員函式實現功能)餵你吃。你只要指使一下(基類指標或引用指向)就OK了。在別人看來,你也是在吃飯(具有相應的功能),但是你根本沒有動手,都是要手下的小弟搞定(子類成員函式實現功能)。