C++函數模板(模板函數)詳解

2020-07-16 10:04:21
物件導向的繼承和多型機制有效提高了程式的可重用性和可擴充性。在程式的可重用性方面,程式設計師還希望得到更多支援。舉一個最簡單的例子,為了交換兩個整型變數的值,需要寫下面的 Swap 函數:
void Swap(int & x, int & y)
{
    int tmp = x;
    x = y;
    y = tmp;
}

為了交換兩個 double 型變數的值,還需要編寫下面的 Swap 函數:
void Swap (double & xr double & y)
{
    double tmp = x;
    x = y;
    y = tmp;
}

如果還要交換兩個 char 型變數的值,交換兩個 CStudent 類物件的值……都需要再編寫 Swap 函數。而這些 Swap 函數除了處理的資料型別不同外,形式上都是一樣的。能否只寫一遍 Swap 函數,就能用來交換各種型別的變數的值呢?繼承和多型顯然無法解決這個問題。因此,“模板”的概念就應運而生了。

眾所周知,有了“模子”後,用“模子”來批次製造陶瓷、塑料、金屬製品等就變得容易了。程式設計語言中的模板就是用來批次生成功能和形式都幾乎相同的程式碼的。有了模板,編譯器就能在需要的時候,根據模板自動生成程式的程式碼。從同一個模板自動生成的程式碼,形式幾乎是一樣的。

函數模板的原理

C++ 語言支援模板。有了模板,可以只寫一個 Swap 模板,編譯器會根據 Swap 模板自動生成多個 Sawp 函數,用以交換不同型別變數的值。

在 C++ 中,模板分為函數模板和類別範本兩種。函數模板是用於生成函數的,類別範本則是用於生成類的。

函數模板的寫法如下:

template <class 型別引數1, class型別引數2, ...>
返回值型別  模板名(形參表)
{
    函數體
}

其中的 class 關鍵字也可以用 typename 關鍵字替換,例如:

template <typename 型別引數1, typename 型別引數2, ...>

函數模板看上去就像一個函數。前面提到的 Swap 模板的寫法如下:
template <class T>
void Swap(T & x, T & y)
{
    T tmp = x;
    x = y;
    y = tmp;
}
T 是型別引數,代表型別。編譯器由模板自動生成函數時,會用具體的型別名對模板中所有的型別引數進行替換,其他部分則原封不動地保留。同一個型別引數只能替換為同一種型別。編譯器在編譯到呼叫函數模板的語句時,會根據實參的型別判斷該如何替換模板中的型別引數。

例如下面的程式:
#include <iostream>
using namespace std;
template<class T>
void Swap(T & x, T & y)
{
    T tmp = x;
    x = y;
    y = tmp;
}
int main()
{
    int n = 1, m = 2;
    Swap(n, m);  //編譯器自動生成 void Swap (int &, int &)函數
    double f = 1.2, g = 2.3;
    Swap(f, g);  //編譯器自動生成 void Swap (double &, double &)函數
    return 0;
}
編譯器在編譯到Swap(n, m);時找不到函數 Swap 的定義,但是發現實參 n、m 都是 int 型別的,用 int 型別替換 Swap 模板中的 T 能得到下面的函數:
void Swap (int & x, int & y)
{
    int tmp = x;
    x = y;
    y = tmp;
}
該函數可以匹配Swap(n, m);這條語句。於是編譯器就自動用 int 替換 Swap 模板中的 T,生成上面的 Swap 函數,將該 Swap 函數的原始碼加入程式中一起編譯,並且將Swap(n, m);編譯成對自動生成的 Swap 函數的呼叫。

同理,編譯器在編譯到Swap(f, g);時會用 double 替換 Swap 模板中的 T,自動生成以下 Swap 函數:
void Swap(double & x, double & y)
{
    double tmp = x;
    x = y;
    y = tmp;
}
然後再將Swap(f, g);編譯成對該 Swap 函數的呼叫。

編譯器由模板自動生成函數的過程叫模板的範例化。由模板範例化而得到的函數稱為模板函數。在某些編譯器中,模板只有在被範例化時,編譯器才會檢查其語法正確性。如果程式中寫了一個模板卻沒有用到,那麼編譯器不會報告這個模板中的語法錯誤。

編譯器對模板進行範例化時,並非只能通過模板呼叫語句的實參來範例化模板中的型別引數,模板呼叫語句可以明確指明要把型別引數範例化為哪種型別。可以用:

模板名<實際型別引數1, 實際型別引數2, ...>

的方式告訴編譯器應該如何範例化模板函數。例如下面的程式:
#include <iostream>
using namespace std;
template <class T>
T Inc(int n)
{
    return 1 + n;
}
int main()
{
    cout << Inc<double>(4) / 2;
    return 0;
}
Inc<double>(4)指明了此處範例化的模板函數原型應為:

double Inc(double);

編譯器不會因為實參 4 是 int 型別,就生成原型為 int Inc(int) 的函數。因此,上面程式輸出的結果是 2.5 而非 2。

函數模板中可以有不止一個型別引數。例如,下面這個函數模板的寫法是合法的:
template <class Tl, class T2>
T2 print(T1 argl, T2 arg2)
{
    cout << arg1 << " " << arg2 << endl;
    return arg2;
}

【範例】一個求陣列中最大元素的函數模板

例題:設計一個分數類 CFraction,再設計一個名為 MaxElement 的函數模板,能夠求陣列中最大的元素,並用該模板求一個 CFmction 陣列中的最大元素。

範例程式如下:
#include <iostream>
using namespace std;
template <class T>
T MaxElement(T a[], int size) //size是陣列元素個數
{
    T tmpMax = a[0];
    for (int i = 1; i < size; ++i)
        if (tmpMax < a[i])
            tmpMax = a[i];
    return tmpMax;
}
class CFraction //分數類
{
    int numerator;   //分子
    int denominator; //分母
public:
    CFraction(int n, int d) :numerator(n), denominator(d) { };
    bool operator <(const CFraction & f) const
    {//為避免除法產生的浮點誤差,用乘法判斷兩個分數的大小關係
        if (denominator * f.denominator > 0)
            return numerator * f.denominator < denominator * f.numerator;
        else
            return numerator * f.denominator > denominator * f.numerator;
    }
    bool operator == (const CFraction & f) const
    {//為避免除法產生的浮點誤差,用乘法判斷兩個分數是否相等
        return numerator * f.denominator == denominator * f.numerator;
    }
    friend ostream & operator <<(ostream & o, const CFraction & f);
};
ostream & operator <<(ostream & o, const CFraction & f)
{//過載 << 使得分數物件可以通過cout輸出
    o << f.numerator << "/" << f.denominator; //輸出"分子/分母" 形式
    return o;
}
int main()
{
    int a[5] = { 1,5,2,3,4 };
    CFraction f[4] = { CFraction(8,6),CFraction(-8,4),
        CFraction(3,2), CFraction(5,6) };
    cout << MaxElement(a, 5) << endl;
    cout << MaxElement(f, 4) << endl;
    return 0;
}
編譯到第 41 行時,根據實參 a 的型別,編譯器通過 MaxElement 模板自動生成了一個 MaxElement 函數,原型為:

int MaxElement(int a[], int size);

編譯到第 42 行時,根據 f 的型別,編譯器又生成一個 MaxElement 函數,原型為:

CFraction MaxElement(CFraction a[], int size);

在該函數中,用到了<比較兩個 CFraction 物件的大小。如果沒有對<進行適當的過載,編譯時就會出錯。

從 MaxElement 模板的寫法可以看出,在函數模板中,型別引數不但可以用來定義引數的型別,還能用於定義區域性變數和函數模板的返回值。