C++ 允許同一范圍內具有相同名稱的多個函數的規范。這些函數稱為重載函數,“重載”中對其進行了詳細介紹。利用重載函數,程序員可以根據參數的類型和數量為函數提供不同的語義。
例如,采用字符串(或 char *)參數的 print 函數執行的任務與采用“雙精度”類型的參數的函數執行的任務截然不同。重載允許通用命名并使程序員無需創建名稱,例如 print_sz 或 print_d。下表顯示了 C++ 使用函數聲明的哪些部分來區分同一范圍內具有相同名稱的函數組。
重載注意事項
函數聲明元素 | 是否用于重載? |
---|---|
函數返回類型 | No |
參數的數量 | 是 |
參數的類型 | 是 |
省略號存在或缺失 | 是 |
typedef 名稱的使用 | 否 |
未指定的數組邊界 | 否 |
const 或 volatile(見下文) | 是 |
雖然可以根據返回類型區分函數,但是無法在此基礎上對它們進行重載。僅當 Const 或 volatile 在類中用于應用于類的 this 指針(而不是函數的返回類型)時,它們才用作重載的基礎。換言之,僅當 const 或 volatile 關鍵字遵循聲明中函數的參數列表時,重載才適用。
以下示例闡述如何使用重載。
// function_overloading.cpp// compile with: /EHsc#include <iostream>#include <math.h>// Prototype three print functions.int print( char *s ); // Print a string.int print( double dvalue ); // Print a double.int print( double dvalue, int prec ); // Print a double with a// given precision.using namespace std;int main( int argc, char *argv[] ){const double d = 893094.2987;if( argc < 2 ) {// These calls to print invoke print( char *s ).print( "This program requires one argument." );print( "The argument specifies the number of" );print( "digits precision for the second number" );print( "printed." );exit(0); }// Invoke print( double dvalue ).print( d );// Invoke print( double dvalue, int prec ).print( d, atoi( argv[1] ) );}// Print a string.int print( char *s ){cout << s << endl;return cout.good();}// Print a double in default precision.int print( double dvalue ){cout << dvalue << endl;return cout.good();}// Print a double in specified precision.// Positive numbers for precision indicate how many digits// precision after the decimal point to show. Negative// numbers for precision indicate where to round the number// to the left of the decimal point.int print( double dvalue, int prec ){// Use table-lookup for rounding/truncation.static const double rgPow10[] = { 10E-7, 10E-6, 10E-5, 10E-4, 10E-3, 10E-2, 10E-1, 10E0,10E1, 10E2, 10E3, 10E4, 10E5, 10E6 };const int iPowZero = 6;// If precision out of range, just print the number.if( prec < -6 || prec > 7 )return print( dvalue );// Scale, truncate, then rescale.dvalue = floor( dvalue / rgPow10[iPowZero - prec] ) *rgPow10[iPowZero - prec];cout << dvalue << endl;return cout.good();}
前面的代碼演示了文件范圍內的 print 函數重載。
默認參數不被視為函數類型的一部分。因此,它不用于選擇重載函數。僅在默認參數上存在差異的兩個函數被視為多個定義而不是重載函數。
不能為重載運算符提供默認參數。
參數匹配
選擇重載函數以實現當前范圍內的函數聲明與函數調用中提供的參數的最佳匹配。如果找到合適的函數,則調用該函數。此上下文中的“Suitable”具有下列含義之一:
- 找到完全匹配項。
- 已執行不重要的轉換。
- 已執行整型提升。
- 已存在到所需參數類型的標準轉換。
- 已存在到所需參數類型的用戶定義的轉換(轉換運算符或構造函數)。
- 已找到省略號所表示的參數。
編譯器為每個參數創建一組候選函數。候選函數是這樣一種函數,其中的實參可以轉換為形參的類型。
為每個參數生成一組“最佳匹配函數”,并且所選函數是所有集的交集。如果交集包含多個函數,則重載是不明確的并會生成錯誤。對于至少一個參數而言,最終選擇的函數始終是比組中的所有其他函數更好的匹配項。如果不是這樣(如果沒有清晰的勝者),則函數調用會生成錯誤。
考慮下面的聲明(針對下面的討論中的標識,將函數標記為 Variant 1、Variant 2 和 Variant 3):
Fraction &Add( Fraction &f, long l ); // Variant 1Fraction &Add( long l, Fraction &f ); // Variant 2Fraction &Add( Fraction &f, Fraction &f ); // Variant 3Fraction F1, F2;
請考慮下列語句:
F1 = Add( F2, 23 );
前面的語句生成兩個集:
集 1:其第一個參數的類型為 Fraction 的候選函數 | 集 2:其第二個參數可轉換為類型 int 的候選函數 |
---|---|
Variant 1 | Variant 1(可使用標準轉換將 int 轉換為 long) |
Variant 3 |
集 2 中的函數是具有從實參類型到形參類型的隱式轉換的函數,在這些函數中,有一種函數的從實參類型到其形參類型的轉換的“成本”是最低的。
這兩個集的交集為 Variant 1。不明確的函數調用的示例為:
F1 = Add( 3, 6 );
前面的函數調用生成以下集:
集 1:其第一個參數的類型為 int 的候選函數 | 集 2:其第二個參數的類型為 int 的候選函數 |
---|---|
Variant 2(可使用標準轉換將 int 轉換為 long) | Variant 1(可使用標準轉換將 int 轉換為 long) |
請注意,這兩個集之間的交集為空。因此,編譯器會生成錯誤消息。
對于參數匹配,具有 n 個默認參數的函數將視為 n+1 個單獨函數,并且每個函數均具有不同數量的參數。
省略號 (...) 用作通配符;它與任何實參匹配。如果您未極其謹慎地設計重載函數集,這可能導致產生許多不明確的集。
注意
重載函數的多義性無法確定,直到遇到函數調用。此時,將為函數調用中的每個參數生成集,并且可以確定是否存在明確的重載。這意味著,多義性可保持在您的代碼中,直到它們由特定函數調用引發。
參數類型差異
重載函數區分使用不同的初始值設定項的參數類型。因此,對于重載而言,給定類型的參數和對該類型的引用將視為相同。由于它們采用相同的初始值設定項,因此它們被視為是相同的。例如,max( double, double ) 被視為與 max( double &, double & ) 相同。聲明兩個此類函數會導致錯誤。
出于同一原因,對由 const 或 volatile 修改的類型的函數參數(出于重載的目的)的處理方式與基類沒有什么不同。
但是,函數重載機制可以區分由 const 和 volatile 限定的引用和對基類型的引用。此方法可以編寫諸如以下內容的代碼:
// argument_type_differences.cpp// compile with: /EHsc /W3// C4521 expected#include <iostream>using namespace std;class Over {public: Over() { cout << "Over default constructor/n"; } Over( Over &o ) { cout << "Over&/n"; } Over( const Over &co ) { cout << "const Over&/n"; } Over( volatile Over &vo ) { cout << "volatile Over&/n"; }};int main() { Over o1; // Calls default constructor. Over o2( o1 ); // Calls Over( Over& ). const Over o3; // Calls default constructor. Over o4( o3 ); // Calls Over( const Over& ). volatile Over o5; // Calls default constructor. Over o6( o5 ); // Calls Over( volatile Over& ).}
輸出
Over default constructorOver&Over default constructorconst Over&Over default constructorvolatile Over&
指向 const 和 volatile 對象的指針也被認為和指向基類型的指針(以重載為目的)不同。
參數匹配和轉換
當編譯器嘗試根據函數聲明中的參數匹配實際參數時,如果未找到任何確切匹配項,它可以提供標準轉換或用戶定義的轉換來獲取正確類型。轉換的應用程序受這些規則的限制:
不考慮包含多個用戶定義的轉換的轉換序列。
不考慮可通過刪除中間轉換來縮短的轉換序列。
最終的轉換序列(如果有)稱為最佳匹配序列??赏ㄟ^多種方式使用標準轉換將類型 int 的對象轉換為類型unsigned long 的對象(如標準轉換中所述):
- 從 int 轉換為 long,然后從 long 轉換為 unsigned long。
- 從 int 轉換為 unsigned long。
第一個序列(盡管它實現了所需目標)不是最佳匹配序列 - 存在一個較短的序列。
下表顯示了一組稱為常用轉換的轉換,這些轉換對確定哪個序列是最佳匹配項有一定的限制。該表后面的列表中討論了常用轉換影響序列選擇的實例。
常用轉換
從類型轉換 | 轉換為類型 |
---|---|
type-name | type-name & |
type-name & | type-name |
type-name [ ] | type-name* |
type-name ( argument-list ) | ( *type-name ) ( argument-list ) |
type-name | const type-name |
type-name | volatile type-name |
type-name* | const type-name* |
type-name* | volatile type-name* |
在其中嘗試轉換的序列如下:
完全匹配。用于調用函數的類型與函數原型中聲明的類型之間的完全匹配始終是最佳匹配。常用轉換的序列將歸類為完全匹配。但是,不進行任何轉換的序列被視為比進行轉換的序列更佳:
- 從指針,到指向 const(type* 指向 consttype*)的指針。
- 從指針,到指向 volatile(type* 指向 volatiletype*)的指針。
- 從引用,到對 const(type & 到 const type &)的引用。
- 從引用,到對 volatile(type & 到 volatile type &)的引用。
使用提升的匹配。未歸類為僅包含整型提升、從 float 到 double 的轉換以及常用轉換的完全匹配的任何序列將被歸類為使用提升的匹配。盡管比不上完全匹配,但使用提升的匹配仍優于使用標準轉換的匹配。
使用標準轉換的匹配。未歸類為完全匹配或僅包含標準轉換和常用轉換的使用提升的匹配的序列將歸類為使用標準轉換的匹配。在此類別中,以下規則將適用:
從指向派生類的指針到指向直接或間接基類的指針的轉換優于到 void * 或 const void * 的轉換。
從指向派生類的指針到指向基類的指針的轉換會產生一個到直接基類的更好匹配。假定類層次結構如下圖所示。
演示首選轉換的關系圖
從 D* 類型到 C* 類型的轉換優于從 D* 類型到 B* 類型的轉換。同樣,從 D* 類型到 B* 類型的轉換優于從 D* 類型到 A* 類型的轉換。
此同一規則適用于引用轉換。從 D& 類型到 C& 類型的轉換優于從 D& 類型到 B& 類型的轉換等。
此同一規則適用于指向成員的指針轉換。從 T D::* 類型到 T C::* 類型的轉換優于從 T D::* 類型到 T B::* 類型的轉換等(其中,T 是該成員的類型)。
前面的規則僅沿派生的給定路徑應用。考慮下圖中顯示的關系圖。
演示首選轉換的多繼承關系圖
從 C* 類型到 B* 類型的轉換優于從 C* 類型到 A* 類型的轉換。原因是它們位于同一個路徑上,且 B* 更為接近。但是,從 C* 類型到 D* 類型的轉換不優于到 A* 類型的轉換;沒有首選項,因為這些轉換遵循不同的路徑。
使用用戶定義的轉換的匹配。此序列不能歸類為完全匹配、使用提升的匹配或使用標準轉換的匹配。序列必須僅包含用戶定義的轉換、標準轉換或要歸類為使用用戶定義的轉換的匹配的常用轉換。使用用戶定義的轉換的匹配被認為優于使用省略號的匹配,但比不上使用標準轉換的匹配。
使用省略號的匹配。與聲明中的省略號匹配的任何序列將歸類為使用省略號的匹配。這被視為最弱匹配。
如果內置提升或轉換不存在,則用戶定義的轉換將適用?;趯⑵ヅ涞膮档念愋瓦x擇這些轉換??紤]下列代碼:
// argument_matching1.cppclass UDC{public: operator int() { return 0; } operator long();};void Print( int i ){};UDC udc;int main(){ Print( udc );}
類 UDC 的可用的用戶定義的轉換來自 int 類型和 long 類型。因此,編譯器會考慮針對將匹配的對象類型的轉換:UDC。到 int 的轉換已存在且已被選中。
在匹配參數的過程中,標準轉換可應用于參數和用戶定義的轉換的結果。因此,下面的代碼將適用:
void LogToFile( long l );...UDC udc;LogToFile( udc );
在前面的示例中,將調用用戶定義的轉換 operator long 以將 udc 轉換為類型 long。如果未定義到 long 類型的用戶定義的轉換,則按如下所示繼續轉換:使用用戶定義的轉換將 UDC 類型轉換為 int 類型。將應用從 int 類型到 long 類型的標準轉換以匹配聲明中的參數。
如果需要任何用戶定義的轉換來匹配參數,則在計算最佳匹配時不會使用標準轉換。即使多個候選函數需要用戶定義的轉換也是如此;在這種情況下,這些函數被認為是相等的。例如:
// argument_matching2.cpp// C2668 expectedclass UDC1{public: UDC1( int ); // User-defined conversion from int.};class UDC2{public: UDC2( long ); // User-defined conversion from long.};void Func( UDC1 );void Func( UDC2 );int main(){ Func( 1 );}
Func 的兩個版本都需要用戶定義的轉換以將類型 int 轉換為類類型參數。可能的轉換包括:
- 從 int 類型轉換到 UDC1 類型(用戶定義的轉換)。
- 從 int 類型轉換到 long 類型;然后轉換為 UDC2 類型(一個兩步轉換)。
即使其中的第二個轉換需要標準轉換以及用戶定義的轉換,這兩個轉換仍被視為相等。
注意
用戶定義的轉換被認為是通過構造函數的轉換或通過初始化的轉換(轉換函數)。在考慮最佳匹配時,兩個方法被認為是相等的。
參數匹配和 this 指針
處理類成員函數的方式各不相同,具體取決于它們是否已被聲明為 static。由于非靜態函數具有提供 this 指針的隱式參數,因此將非靜態函數視為比靜態函數多一個參數;否則,將以相同的方式聲明這些函數。
這些非靜態成員函數要求隱含的 this 指針與通過其調用函數的對象類型匹配,或者對于重載運算符,它們要求第一個參數與該運算符應用于的對象匹配。
與重載函數中的其他參數不同,當嘗試匹配 this 指針參數時,不會引入臨時對象,且不會嘗試轉換。
當 – > 成員選擇運算符用于訪問成員函數時,this 指針參數具有 class-name* const 的類型。如果將成員聲明為 const 或 volatile,則類型分別為 const class-name* const 和 volatile class-name * const。
. 成員選擇運算符以相同的方式工作,只不過隱式 & (address-of) 運算符將成為對象名稱的前綴。下面的示例演示了此工作原理:
// Expression encountered in codeobj.name// How the compiler treats it(&obj)->name
處理 –>* 和 .*(指向成員的指針)運算符的左操作數的方式與處理與參數匹配相關的 . 和 –>(成員選擇)運算符的方式相同。
限制
多個限制管理可接受的重載函數集:
- 重載函數集內的任意兩個函數必須具有不同的參數列表。
- 僅基于返回類型重載具有相同類型的參數列表的函數是錯誤的。
不能只根據一個靜態類型和一個非靜態類型來重載成員函數。
typedef 聲明不定義新類型;它們引入現有類型的同義詞。它們不影響重載機制??紤]下列代碼:
typedef char * PSTR;void Print( char *szToPrint );void Print( PSTR szToPrint );
前面的兩個函數具有相同的參數列表。 PSTR 是類型 char * 的同義詞。在成員范圍內,此代碼生成錯誤。
枚舉類型是不同的類型,并且可用于區分重載函數。
就區分重載函數而言,類型“array of”和“pointer to”是等效的。此情況僅適用于單維度數組。因此,以下重載函數會發生沖突并生成錯誤消息:
void Print( char *szToPrint );void Print( char szToPrint[] );
對于多維數組,第二個和后續維度被視為類型的一部分。因此,它們可用來區分重載函數:
void Print( char szToPrint[] );void Print( char szToPrint[][7] );void Print( char szToPrint[][9][42] );
聲明匹配
同一范圍內具有同一名稱的任何兩個函數聲明都可以引用同一函數或重載的兩個不同的函數。如果聲明的參數列表包含等效類型的參數(如上一節所述),函數聲明將引用同一函數。否則,它們將引用使用重載選擇的兩個不同的函數。
需要嚴格遵守類范圍;因此,在基類中聲明的函數與在派生類中聲明的函數不在同一范圍內。如果使用與基類中的函數相同的名稱聲明派生類中的函數,則該派生類函數會隱藏基類函數,而不是導致重載。
需要嚴格遵守塊范圍;因此,在文件范圍中聲明的函數與在本地聲明的函數不在同一范圍內。如果在本地聲明的函數與在文件范圍中聲明的函數具有相同名稱,則在本地聲明的函數將隱藏文件范圍內的函數而不是導致重載。例如:
// declaration_matching1.cpp// compile with: /EHsc#include <iostream>using namespace std;void func( int i ){ cout << "Called file-scoped func : " << i << endl;}void func( char *sz ){ cout << "Called locally declared func : " << sz << endl;}int main(){ // Declare func local to main. extern void func( char *sz ); func( 3 ); // C2664 Error. func( int ) is hidden. func( "s" );}
前面的代碼顯示函數 func 中的兩個定義。由于 char * 語句,采用 main 類型的參數的定義是 extern 的本地定義。因此,采用 int 類型的參數的定義被隱藏,而對 func 的第一次調用出錯。
對于重載的成員函數,不同版本的函數可能獲得不同的訪問權限。它們仍被視為在封閉類的范圍內,因此是重載函數。請考慮下面的代碼,其中的成員函數 Deposit 將重載;一個版本是公共的,另一個版本是私有的。
此示例的目的是提供一個 Account 類,其中需要正確的密碼來執行存款。使用重載可完成此操作。
請注意,對 Deposit 中的 Account::Deposit 的調用將調用私有成員函數。此調用是正確的,因為 Account::Deposit 是成員函數,因而可以訪問類的私有成員。
// declaration_matching2.cppclass Account{public: Account() { } double Deposit( double dAmount, char *szPassword );private: double Deposit( double dAmount ) { return 0.0; } int Validate( char *szPassword ) { return 0; }};int main(){ // Allocate a new object of type Account. Account *pAcct = new Account; // Deposit $57.22. Error: calls a private function. // pAcct->Deposit( 57.22 ); // Deposit $57.22 and supply a password. OK: calls a // public function. pAcct->Deposit( 52.77, "pswd" );}double Account::Deposit( double dAmount, char *szPassword ){ if ( Validate( szPassword ) ) return Deposit( dAmount ); else return 0.0;}