函數重載
如果同一作用域內的幾個函數名字相同但形參列表不同,我們稱之為重載函數。例如:
void print(const char *cp); void print(const int *beg,const int *end); void pring(const int ia[],size_t size);
這些函數接受的形參類型不一樣,但是執行的操作非常類似。當調用這些函數時,編譯器會根據傳遞的實參類型推斷想要的是哪個函數:
int j[2]={0,1};
print("Hello world"); //調用void print(const char *cp);
print(j,end(j)-begin(j)); //調用void pring(const int ia[],size_t size);
print(begin(j),end(j)); //調用void print(const int *beg,const int *end);
函數的名字僅僅是讓編譯器知道它調用的是哪個函數,而函數重載可以在一定程度上減輕程序員起名字、記名字的負擔。
main函數不能重載
定義重載函數
對於重載的函數來說,它們應該在形參數量或形參類型上有所不同。
不允許兩個函數除了返回類型外其他所有的要素都相同(因為只有返回類型不同,調用函數的時候不能區分調用的是哪一個函數)。假設有兩個函數,它們的形參列表一樣但是返回類型不同,則第二個函數的聲明是錯誤的:
Record lookup(const Account&);
bool lookup(const Account&); //錯誤:與上一個函數相比只有返回類型不同
判斷兩個形參的類型是否相異
有時候兩個形參列表看起來不一樣,但實際上是相同的:
//每對聲明的是同一個函數
Record lookup(const Account &acct);
Record lookup(const Account &); //省略了形參的名字
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); //Telno和Phone的類型相同
在第一對聲明中,第一個函數給它的形參起了名字,第二個函數沒有。形參的名字僅僅起到幫助記憶的作用,有沒有它並不影響形參列表的內部。
第二對聲明看起來類型不同,但事實上Telno不是一種新類型,他只是Phone的別名而已。類型別名為已存在的類型提供了另外一個名字,它並不是創建新類型。
重載和const形參
頂層const不影響傳入函數的對象,一個擁有頂層const的形參無法和另一個沒有頂層const的形參區分開來:
Record lookup(Phone);
Record lookup(const Phone); //重復聲明了Record lookup(Phone);
Record lookup(Phone*);
Record lookup(Phone * const ); //重復聲明了Record lookup (Phone *);
在這兩組函數聲明中,每一組的第二個聲明和第一個聲明時等價的。
另一方面,如果形參是某種類型的指針或引用,則通過區分其指向的是常量對象還是非常量對象可以實現函數重載,此時的const是底層的:
//對於接受引用或指針的函數來說,對象時常量還是非常量對象的形參不同
//定義了4個獨立的重載函數
Record lookup(Account &); //函數作用於Account的引用
Record lookup(const Account &); //新函數,作用於常量引用
Record lookup(Account *); //新函數,作用於指向Account的指針
Record lookup(const Account *); //新函數,作用於指向常量的指針
在上面的例子中,編譯器可以通過實參是否常量來推斷應該調用哪個函數。因為const不能轉換成其他類型,所以我們只能把const對象(或指向const的指針)傳遞給const形參。相反的,因為非常量可以轉換成const,所以上面的4個函數都能作用於非常量對象或者指向非常量對象的指針。不過,當我們傳遞一個非常量對象或者指向非常量對象的指針時,編譯器會優先選用非常量版本的函數。
const_cast和重載
const_cast在重載函數的情景中最有用。
//比較兩個string對象的長度,返回較短的那個引用
const string &shorterString(const string &s1,const string &s2)
{
return s1.size()<=s2.size()?s1:s2;
}
這個函數的參數和返回值都是const string的引用。我們可以對兩個非常量的string實參調用這個函數,但返回的結果仍然是const string的引用。因此我們需要一種新的shorterString函數,當它的實參不是常量時,得到的結果是一個普通的引用,使用const_cast可以做到這一點:
string &shorterString(string &s1,string &s2)
{
auto &r=shorterString(const_cast<const string&>(s1),const_cast<const string&>(s2));
return const_cast<stirng&>(r);
}
在這個版本的函數中,首先將它的實參強制轉換成對const的引用,然后調用了shorterString函數的const版本。const版本返回到const string的引用,這個引用事實上綁定在了某個初始的非常量實參上。因此,我們可以再將其轉換回一個普通的string&,這顯然是安全的。
調用重載的函數
定義了一組重載函數后,我們需要以合理的實參調用它們。函數匹配是指一個過程,在這個過程中我們把函數調用與一組重載函數中的某一個關聯起來,函數匹配也叫做函數確定。編譯器首先將調用的實參與重載集合中每一個函數的形參進行比較,然后根據比較的結果決定到底調用哪個函數。
現在我們需要掌握的是,當調用重載函數時有三種可能的結果:
- 編譯器找到一個與實參最佳匹配的函數,並生成調用該函數的代碼;
- 找不到任何一個函數與調用的實參匹配,此時編譯器發出無匹配的錯誤信息;
- 又多於一個函數可以匹配,但是每一個都不是明顯的最佳選擇。此時也將發生錯誤,稱為二義性調用。
重載與作用域
其實,重載對作用域的一般性質並沒有什么改變:如果我們在內層作用域中聲明名字,它將隱藏外層作用域中聲明的同名實體。在不同的作用域中無法重載函數名:
string read(); void print(const string&); void print(double); //重載print函數 void fooBar(int ival) { bool read=false; //新作用域:隱藏了外層的read string s=read(); //錯誤:read是一個布爾值,而非函數 //不好的習慣:通常來說,在局部作用域中聲明函數不是一個好的選擇 void print(int ); //新作用域:隱藏了之前的print print("value"); //錯誤:print(const string&)被隱藏了 print(ival); //正確:當前print(int)可見 print(3.14); //正確:調用print(int);print(double)被隱藏了 }
當我們調用print函數時,編譯器首先尋找對該函數名的聲明,找到的是接受int值得哪個局部什么。一旦在當前作用域中找到了所需的名字,編譯器就會忽略掉外層作用域中同名實體。剩下的工作就是檢查函數調用是否有效了。
在C++中,名字查找發生在類型檢查之前。
假設我們把print(int)和其他print函數聲明放在同一個作用域中,則它將成為另一種重載形式。此時,因為編譯器能看到所有三個函數,上述調用的處理結果將完全不同:
void print(const string&); void print(double); void print(int); void fooBar2(int ival) { print("value"); //調用print(const string&) print(ival); //調用print(int) print(3.14); //調用print(double) }