void f(); void f(int ); void f(int,int); void f(double,double=3.14); f(5.6);//調用void f(double,double)
確定候選函數和可行函數
函數匹配的第一步是選定本次調用對應的重載函數集,集合中的函數稱為候選函數。候選函數具備兩個特征:一是與被調用的函數同名;二是其聲明在調用點可見。在這個例子中,有4個名為f的候選函數。
第二步考察本次調用提供的實參,然后從候選函數中選出能被這組實參調用的函數,這些新選出的函數稱為可行函數。可行函數也有兩個特征:一是其形參數量與本次調用提供的實參數量相等,二是美國實參的類型與對應的形參類型相同,或者能轉換成形參的類型。
注意:如果函數有默認實參,則我們在調用該函數時傳入的實參數量可能少於它實際使用的實參數量。
如果沒有找到可行函數,編譯器將報告無匹配函數的錯誤。
尋找最佳匹配(如果有的話)
函數匹配的第三步是從可行函數中選擇與本次調用最匹配的函數。在這一過程中,逐一檢查函數調用提供的實參,尋找形參類型與實參類型最匹配的那個可行函數。
在我們的例子中,調用只提供了一個(顯式的)實參,它的類型是double。如果調用f(int),實參將不得不從double轉換成int。另一個可行函數f(double,double)則與實參精確匹配。精確匹配比需要類型轉換的匹配更好,因此,編譯器把f(5.6)解析成對含有兩個double形參的函數的調用,並使用默認值填補我們未提供的第二個實參。
含有多個形參的函數匹配
當實參的數量有兩個或更多時,函數匹配就比較復雜了。對於前面那些名為f的函數,我們分析如下的調用會發生什么情況:
(42,2.56);
選擇可行函數的方法和只有一個實參時一樣,編譯器選擇那些形參數量滿足要求且實參類型和形參類型能夠匹配的函數。此例中,可行函數包括f(int,int)和f(double,double)。接下來,編譯器依次檢查每個實參以確定哪個函數是最佳匹配。如果有且只有一個函數滿足下列條件,則匹配成功:
- 該函數每個實參的匹配都不劣於其他可行函數需要的匹配。
- 至少有一個實參的匹配優於其他可行函數提供的匹配。
如果檢查了所有實參之后沒有任何一個函數脫穎而出,則該調用時錯誤的。編譯器將報告二義性調用的信息。
在上面的調用這,只考慮第一個實參時我們發現函數f(int,int)能精確匹配:要想匹配第二個函數,int類型的實參必須轉換成double類型。顯然需要內置類型轉換的匹配劣於精確匹配,因此僅就第一個實參來說,f(int,int)比f(double,double)更好。
接着考慮第二個實參,此時f(double,double)是精確匹配:要調用f(int,int)必須將2.56從double類型轉換成int類型。因此僅第二個實參來說,f(double,double)更好。
編譯器最終將因為這個調用具有二義性而拒絕其請求:因為每個可行函數各自在一個實參上實現了更好的匹配,從整體上無法判斷孰優孰劣。看起來我們似乎可以通過強制類型轉換其中一個實參來實現函數的匹配,但是在設計良好的系統中,不應該對實參進行強制類型轉換。
實參類型轉換
為了確定最佳匹配,編譯器將實參類型到形參類型的轉換划分為幾個等級,具體排序如下所示:
1 精確匹配,包括以下幾種:
- 實參類型和形參類型相同
- 實參從數組類型或函數類型轉換成對應的指針類型
- 向實參添加頂層const或者從實參中刪除頂層const
2 通過const轉換實現的匹配
3 通過類型提升實現的匹配
4 通過算術轉換或指針轉換實現的匹配
5 通過類類型轉換實現的匹配
需要類型提升和算術類型轉換的匹配
分析函數調用前,我們應該知道小整型一般都會提升到int類型或者更大的整數類型。假設有兩個函數,一個接受int,另一個接受short,則只有當調用提供的是short類型的值時才會選擇short版本的函數。有時候,即使實參時一個很小的整數值,也會直接將它提升成int類型:此時使用short版本反而會導致類型轉換:
void ff(int);
void ff(short);
ff('a'); //char提升成int,調用ff(int)
所有算術類型轉換的級別都一樣。例如,從int到unsigned int的轉換並不比從int到double的轉換級別高。舉個具體的例子:
void manip(long);
void manip(float);
manip(3.14); //錯誤:二義性調用
字面值3.14的類型是double,它既能轉換成龍也能轉換成float。因為存在兩種可能的算數類型轉換,所有該調用具有二義性。
函數匹配和const實參(都是底層const)
如果重載函數的區別在於它們的引用類型的形參是否引用了const,或者指針類型的形參是否執行const,則當調用發生時編譯器通過實參是否是常量來決定選擇哪個函數。
Record lookup(Account &); //函數的參數是Account的引用
Record lookup(const Account &); //函數的參數是一個常量引用
const Account a;
Account b;
lookup(a); //調用lookup(const Account&)
lookup(b); //調用lookup(Account&)
在第一個調用中,我們傳入的是const對象a。因為不能把普通引用綁定到const對象上,所以此例中唯一可行的函數是以常量引用作為形參的那個函數,並且調用該函數與實參a精確匹配。
在第二個調用這,我們傳入的是非常量對象b。對於這個調用來說,兩個函數都是可行的,因為我們既可以使用b初始化常量引用也可以用它初始化非常量引用。然而,用非常量對象初始化常量引用需要類型轉換,接受非常量 形參的版本則與b精確匹配。因此,應該選用非常量版本的函數。
指針類型的形參也類似。如果兩個函數的唯一區別是它的指針形參指向常量或非常量,則編譯器能通過是否是常量決定選用哪個函數:如果實參時指向常量的指針,調用形參是const *的函數;如果實參是指向非常量的指針,調用形參是普通指針的函數。