函式多載

C++ 可讓您在相同範圍中指定多個相同名稱的函式。 這些函式稱為 多載函 式或 多載 。 多載函式可讓您根據函式的類型和引數數目,提供不同的語意。

例如,請考慮 print 採用 引數的 std::string 函式。 此函式可能會執行與採用 類型 double 引數的函式非常不同的工作。 多載可讓您不必使用 或 等 print_stringprint_double 名稱。 在編譯階段,編譯器會根據呼叫端傳入的類型和引數數目,選擇要使用的多載。 如果您呼叫 print(42.0) ,則會叫用 函 void print(double d) 式。 如果您呼叫 print("hello world") ,則會 void print(std::string) 叫用多載。

您可以多載成員函式和免費函式。 下表顯示 C++ 函式宣告的哪些部分用來區分相同範圍中具有相同名稱的函式群組。

多載考量

函式宣告專案 用於多載?
函式傳回類型 No
引數數目 Yes
引數類型 Yes
省略符號是否存在 Yes
typedef使用名稱 No
未指定的陣列範圍 No
constvolatile 是,套用至整個函式時
參考限定詞 ( &&& Yes

範例

下列範例說明如何使用函式多載:

// function_overloading.cpp
// compile with: /EHsc
#include <iostream>
#include <math.h>
#include <string>

// Prototype three print functions.
int print(std::string 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(string 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 載。

預設引數不會被視為函式類型的一部分。 因此,它不會用於選取多載函式。 兩個函式的不同之處只有在其預設引數是視為多重定義而不是多載函式。

無法為多載運算子提供預設引數。

引數比對

編譯器會根據目前範圍中函式宣告與函式呼叫中提供的引數之間,選取要叫用的多載函式。 如果找到適合的函式,就會呼叫該函式。 此內容中的「適合」表示:

  • 找到完全相符項目。

  • 已執行一般轉換。

  • 已執行整數提升。

  • 有轉換成所需引數類型的標準轉換存在。

  • 使用者定義的轉換(轉換運算子或建構函式)存在所需的引數類型。

  • 找到省略符號所表示的引數。

編譯器會為每一個引數建立一組候選函式。 候選函式是指函式中該位置的實際引數可以轉換成正式引數的類型。

每個引數都會有一組建置的「最相符函式」,而且選取的函式是所有集合的交集。 如果交集包含多個函式,則多載會變得模稜兩可並產生錯誤。 最終選取的函式一律比群組中每個其他函式至少一個引數更相符。 如果沒有明確的贏家,函式呼叫會產生編譯器錯誤。

以下列宣告為例 (在下面的討論中,函式會標記為 Variant 1Variant 2Variant 3 以便識別):

Fraction &Add( Fraction &f, long l );       // Variant 1
Fraction &Add( long l, Fraction &f );       // Variant 2
Fraction &Add( Fraction &f, Fraction &f );  // Variant 3

Fraction F1, F2;

考量下列陳述式:

F1 = Add( F2, 23 );

上面的陳述式會建置兩個集合:

設定 1:具有類型第一個引數的候選函式 Fraction 設定 2:可將其第二個引數轉換成類型的候選函式 int
Variant 1 Variant 1 ( int 可以使用標準轉換轉換成 long
Variant 3

Set 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 型別的函式引數不會因多載目的,以不同于基底型別。

不過,函式多載機制可以區別基 constvolatile 底型別和參考所限定的參考。 它可讓程式碼如下列專案:

// 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 constructor
Over&
Over default constructor
const Over&
Over default constructor
volatile Over&

constvolatile 物件的指標也會被視為不同于基底型別的指標,以便進行多載。

引數比對和轉換

當編譯器嘗試比對實質引數與函式宣告中的引數時,如果找不到完全相符的項目,它可以提供取得正確類型的標準或使用者定義轉換。 轉換的應用會受限於下列規則:

  • 不會考慮包含多個使用者定義轉換的轉換序列。

  • 不會考慮透過移除中繼轉換來縮短的轉換序列。

轉換的結果序列,如果有的話,稱為最佳比對 序列 。 使用標準轉換將型別的物件轉換成類型 intunsigned long 的方法有數種(如標準轉換 中所述 ):

  • intlong 轉換為 ,然後從 long 轉換為 unsigned long

  • intunsigned 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*

轉換嘗試執行的序列如下:

  1. 完全相符。 呼叫函式所使用的類型與函式原型中所宣告的類型之間完全相符的項目,必定是最相符項目。 一般轉換的序列會分類為完全相符項目。 不過,不進行這些轉換的序列會比轉換的序列更好:

    • 從指標,指向 ( type-name* 至 ) 的 const type-name* 指標 const

    • 從指標,指向 ( type-name* 至 ) 的 volatile type-name* 指標 volatile

    • 從參考到 參考 const 到 ( type-name&const type-name& )。

    • 從參考到 參考 volatile 到 ( type-name&volatile type& )。

  2. 使用提升的相符項目。 任何未分類為只包含整數升階、從 float 轉換為 double 的完全相符序列,以及使用促銷分類為相符專案。 雖然不如任何完全相符項目一般合適,但是使用提升的相符項目與使用標準轉換的相符項目相比之下仍較為理想。

  3. 使用標準轉換的相符項目。 未分類為完全相符或使用提升的相符項目,而且只包含標準轉換和一般轉換的任何序列,都會分類為使用標準轉換的相符項目。 這個分類適用下列規則:

    • 從指標轉換成衍生類別,轉換為直接或間接基類的指標,最好轉換成 void *const void *

    • 從指標轉換成衍生類別、轉換成基底類別的指標會產生較佳的相符項目,基底類別也會越接近直接基底類別。 假設類別階層如下圖所示:

Example class hierarchy showing that class A inherits from B which inherits from C which inherits from D.
顯示慣用轉換的圖表。

D* 類型轉換成 C* 類型,比從 D* 類型轉換成 B* 類型更理想。 同樣地,從 D* 類型轉換成 B* 類型,比從 D* 類型轉換成 A* 類型更理想。

相同的規則適用於參考轉換。 從 D& 類型轉換成 C& 類型,比從 D& 類型轉換成 B& 類型更理想,以此類推。

相同的規則適用於指標至成員轉換。 從 T D::* 類型轉換成 T C::* 類型,比從 T D::* 類型轉換成 T B::* 類型更理想,以此類推 (其中 T 是成員的類型)。

前述規則僅適用於所指的衍生路徑。 以下圖顯示的圖形為例。

Diagram of multiple inheritance that shows preferred conversions. Class C is the base class of class B and D. Class A inherits from class B
顯示慣用轉換的多重繼承圖表。

C* 類型轉換成 B* 類型,比從 C* 類型轉換成 A* 類型更理想。 這是因為它們位於相同路徑上,且 B* 較接近。 不過,從類型 C* 轉換成類型 D* 並不偏好轉換成類型 A* ;因為轉換會遵循不同的路徑,因此沒有喜好設定。

  1. 使用使用者定義轉換的相符項目 此序列無法分類為完全相符、使用促銷的相符專案,或使用標準轉換的相符專案。 若要分類為與使用者定義轉換的相符專案,序列必須只包含使用者定義的轉換、標準轉換或簡單轉換。 與使用者定義轉換的比對會被視為比以省略號 ( ... ) 比對比對 ,但與標準轉換的比對不一樣好。

  2. 使用省略符號的相符項目。 符合宣告中省略符號的任何序列都會分類為使用省略符號的相符項目。 這被認為是最弱的比賽。

如果沒有內建的提升或轉換存在,則會套用使用者定義的轉換。 這些轉換會根據所比對的引數類型來選取。 請考慮下列程式碼:

// argument_matching1.cpp
class 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 ,編譯器會先使用使用者定義的 operator int 轉換,將類型 UDC 轉換成類型 int 。 然後,它會將標準轉換從類型套用至類型 intlong ,以符合宣告中的引數。

如果需要任何使用者定義的轉換來比對引數,則評估最佳比對時不會使用標準轉換。 即使有多個候選函式需要使用者定義的轉換,函式仍會視為相等。 例如:

// argument_matching2.cpp
// C2668 expected
class 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 轉換成類別類型引數。 可能的轉換包括:

  • 從類型轉換成類型 intUDC1 (使用者定義的轉換)。

  • 從類型 int 轉換成類型;然後轉換成類型 longUDC2 (雙步驟轉換)。

即使第二個轉換需要標準轉換和使用者定義的轉換,但兩個轉換仍視為相等。

注意

使用者定義的轉換會依建構或初始化的轉換來視為轉換。 編譯器會在判斷最佳比對時,將這兩種方法視為相等。

引數比對 this 和指標

根據類別成員函式是否宣告為 static ,會以不同的方式處理。 static 函式沒有提供指標的 this 隱含引數,因此會將引數視為比一般成員函式少一個引數。 否則,它們會以相同方式宣告。

不需要 static 隱含 this 指標來比對呼叫函式的物件類型的成員函式。 或者,對於多載運算子,它們需要第一個引數來比對套用運算子的物件。 如需多載運算子的詳細資訊,請參閱 多載運算子

不同于多載函式中的其他引數,編譯器不會引進任何暫存物件,而且嘗試比 this 對指標引數時不會嘗試轉換。

->當成員選取運算子用來存取 類別 class_name 的成員函式時, this 指標引數的類型為 class_name * const 。 如果成員宣告為 constvolatile ,則類型分別為 const class_name * constvolatile class_name * const

. 成員選取運算子的工作方式完全相同,但有一點除外,就是其物件名稱前方會放置隱含的 & (傳址) 運算子。 下列範例顯示如何執行這項工作:

// Expression encountered in code
obj.name

// How the compiler treats it
(&obj)->name

->*.* (成員的指標) 運算子的左運算元處理方式,與具有相符引數的 .-> (成員選取) 運算子相同。

成員函式上的參考限定詞

參考限定詞可讓您根據所指向 this 的物件是右值或左值,多載成員函式。 在選擇不提供資料的指標存取的情況下,請使用此功能來避免不必要的複製作業。 例如,假設 類別 C 會初始化其建構函式中的某些資料,並在成員函式 get_data() 中傳回該資料的複本。 如果 類型的 C 物件是即將終結的右值,則編譯器會選擇 get_data() && 多載,它會移動而不是複製資料。

#include <iostream>
#include <vector>

using namespace std;

class C
{
public:
    C() {/*expensive initialization*/}
    vector<unsigned> get_data() &
    {
        cout << "lvalue\n";
        return _data;
    }
    vector<unsigned> get_data() &&
    {
        cout << "rvalue\n";
        return std::move(_data);
    }

private:
    vector<unsigned> _data;
};

int main()
{
    C c;
    auto v = c.get_data(); // get a copy. prints "lvalue".
    auto v2 = C().get_data(); // get the original. prints "rvalue"
    return 0;
}

多載的限制

有幾項限制負責管理一組可接受的多載函式:

  • 一組多載函式中的任兩個函式必須具有不同的引數清單。

  • 具有相同型別引數清單的多載函式,僅根據傳回型別,就會發生錯誤。

    Microsoft 特定的

    您可以根據傳回類型多載 operator new ,特別是根據指定的記憶體模型修飾詞。

    END Microsoft 特定的

  • 成員函式只能多載,因為其中一個是 static ,另一個不是 static

  • 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] );
    

多載、覆寫和隱藏

相同範圍中相同名稱的任何兩個函式宣告都可以參考相同的函式,或兩個離散多載函式。 如果宣告的引數清單包含相同類型的引數 (如先前章節所述),函式宣告會參考相同的函式。 否則,它們會參考使用多載選取的兩個不同函式。

嚴格觀察類別範圍。 在基類中宣告的函式與衍生類別中宣告的函式不在相同的範圍內。 如果衍生類別中的函式以與 virtual 基類中的函式同名宣告,則衍生類別函 式會 覆寫基類函式。 如需詳細資訊,請參閱 虛擬函式

如果基類函式未宣告為 virtual ,則表示衍生類別函式會 隱藏 它。 覆寫和隱藏都與多載不同。

嚴格觀察區塊範圍。 在檔案範圍中宣告的函式不在與本機宣告的函式相同的範圍內。 如果本機宣告的函式與在檔案範圍中宣告的函式相同名稱,本機宣告的函式會隱藏檔案範圍涵式,而不會引發多載。 例如:

// 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 類別,在此類別中需要有正確的密碼才能執行儲放。 它是使用多載來完成的。

中的 Account::Deposit 呼叫會呼叫 Deposit 私用成員函式。 此呼叫正確,因為 Account::Deposit 是成員函式,而且可以存取 類別的私人成員。

// declaration_matching2.cpp
class 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;
}

另請參閱

函式 (C++)