1. 程式人生 > >C++函式模板與類模板的區別

C++函式模板與類模板的區別

類模板:

C++ 除了支援函式模板,還支援類模板(Class Template)。函式模板中定義的型別引數可以用在函式宣告和函式定義中,類模板中定義的型別引數可以用在類宣告和類實現中。類模板的目的同樣是將資料的型別引數化。

宣告類模板的語法為:

template<typename 型別引數1 , typename 型別引數2 , …> class 類名{
    //TODO:
};

類模板和函式模板都是以 template 開頭(當然也可以使用 class,目前來講它們沒有任何區別),後跟型別引數;型別引數不能為空,多個型別引數用逗號隔開。

一但聲明瞭類模板,就可以將型別引數用於類的成員函式和成員變量了。換句話說,原來使用 int、float、char 等內建型別的地方,都可以用型別引數來代替。

假如我們現在要定義一個類來表示座標,要求座標的資料型別可以是整數、小數和字串,例如:

  • x = 10、y = 10
  • x = 12.88、y = 129.65
  • x = "東京180度"、y = "北緯210度"


這個時候就可以使用類模板,請看下面的程式碼:

  1. template<typename T1, typename T2> //這裡不能有分號
  2. class Point{
  3. public:
  4. Point(T1 x, T2 y): m_x(x), m_y(y){ }
  5. public:
  6. T1 getX() const; //獲取x座標
  7. void setX(T1 x); //設定x座標
  8. T2 getY() const; //獲取y座標
  9. void setY(T2 y); //設定y座標
  10. private:
  11. T1 m_x; //x座標
  12. T2 m_y; //y座標
  13. };

x 座標和 y 座標的資料型別不確定,藉助類模板可以將資料型別引數化,這樣就不必定義多個類了。

注意:模板頭和類頭是一個整體,可以換行,但是中間不能有分號。

上面的程式碼僅僅是類的宣告,我們還需要在類外定義成員函式。在類外定義成員函式時仍然需要帶上模板頭,格式為:

template<typename 型別引數1 , typename 型別引數2 , …>
返回值型別 類名<型別引數1 , 型別引數2, ...>::函式名(形參列表){
    //TODO:
}

第一行是模板頭,第二行是函式頭,它們可以合併到一行,不過為了讓程式碼格式更加清晰,一般是將它們分成兩行。

下面就對 Point 類的成員函式進行定義:

  1. template<typename T1, typename T2> //模板頭
  2. T1 Point<T1, T2>::getX() const /*函式頭*/ {
  3. return m_x;
  4. }
  5. template<typename T1, typename T2>
  6. void Point<T1, T2>::setX(T1 x){
  7. m_x = x;
  8. }
  9. template<typename T1, typename T2>
  10. T2 Point<T1, T2>::getY() const{
  11. return m_y;
  12. }
  13. template<typename T1, typename T2>
  14. void Point<T1, T2>::setY(T2 y){
  15. m_y = y;
  16. }

請讀者仔細觀察程式碼,除了 template 關鍵字後面要指明型別引數,類名 Point 後面也要帶上型別引數,只是不加 typename 關鍵字了。另外需要注意的是,在類外定義成員函式時,template 後面的型別引數要和類宣告時的一致。

使用類模板建立物件

上面的兩段程式碼完成了類的定義,接下來就可以使用該類建立物件了。使用類模板建立物件時,需要指明具體的資料型別。請看下面的程式碼:

  1. Point<int, int> p1(10, 20);
  2. Point<int, float> p2(10, 15.5);
  3. Point<float, char*> p3(12.4, "東京180度");

與函式模板不同的是,類模板在例項化時必須顯式地指明資料型別,編譯器不能根據給定的資料推演出資料型別。

除了物件變數,我們也可以使用物件指標的方式來例項化:

  1. Point<float, float> *p1 = new Point<float, float>(10.6, 109.3);
  2. Point<char*, char*> *p = new Point<char*, char*>("東京180度", "北緯210度");

需要注意的是,賦值號兩邊都要指明具體的資料型別,且要保持一致。下面的寫法是錯誤的:

  1. //賦值號兩邊的資料型別不一致
  2. Point<float, float> *p = new Point<float, int>(10.6, 109);
  3. //賦值號右邊沒有指明資料型別
  4. Point<float, float> *p = new Point(10.6, 109);

綜合示例

將上面的類定義和類例項化的程式碼整合起來,構成一個完整的示例,如下所示:

  1. #include <iostream>
  2. using namespace std;
  3. template<class T1, class T2> //這裡不能有分號
  4. class Point{
  5. public:
  6. Point(T1 x, T2 y): m_x(x), m_y(y){ }
  7. public:
  8. T1 getX() const; //獲取x座標
  9. void setX(T1 x); //設定x座標
  10. T2 getY() const; //獲取y座標
  11. void setY(T2 y); //設定y座標
  12. private:
  13. T1 m_x; //x座標
  14. T2 m_y; //y座標
  15. };
  16. template<class T1, class T2> //模板頭
  17. T1 Point<T1, T2>::getX() const /*函式頭*/ {
  18. return m_x;
  19. }
  20. template<class T1, class T2>
  21. void Point<T1, T2>::setX(T1 x){
  22. m_x = x;
  23. }
  24. template<class T1, class T2>
  25. T2 Point<T1, T2>::getY() const{
  26. return m_y;
  27. }
  28. template<class T1, class T2>
  29. void Point<T1, T2>::setY(T2 y){
  30. m_y = y;
  31. }
  32. int main(){
  33. Point<int, int> p1(10, 20);
  34. cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
  35. Point<int, char*> p2(10, "東京180度");
  36. cout<<"x="<<p2.getX()<<", y="<<p2.getY()<<endl;
  37. Point<char*, char*> *p3 = new Point<char*, char*>("東京180度", "北緯210度");
  38. cout<<"x="<<p3->getX()<<", y="<<p3->getY()<<endl;
  39. return 0;
  40. }

執行結果:
x=10, y=20
x=10, y=東京180度
x=東京180度, y=北緯210度

在定義型別引數時我們使用了 class,而不是 typename,這樣做的目的是讓讀者對兩種寫法都熟悉。

函式模板:

在《C++函式過載》一節中,為了交換不同型別的變數的值,我們通過函式過載定義了四個名字相同、引數列表不同的函式,如下所示:

  1. //交換 int 變數的值
  2. void Swap(int *a, int *b){
  3. int temp = *a;
  4. *a = *b;
  5. *b = temp;
  6. }
  7. //交換 float 變數的值
  8. void Swap(float *a, float *b){
  9. float temp = *a;
  10. *a = *b;
  11. *b = temp;
  12. }
  13. //交換 char 變數的值
  14. void Swap(char *a, char *b){
  15. char temp = *a;
  16. *a = *b;
  17. *b = temp;
  18. }
  19. //交換 bool 變數的值
  20. void Swap(bool *a, bool *b){
  21. char temp = *a;
  22. *a = *b;
  23. *b = temp;
  24. }

這些函式雖然在呼叫時方便了一些,但從本質上說還是定義了三個功能相同、函式體相同的函式,只是資料的型別不同而已,這看起來有點浪費程式碼,能不能把它們壓縮成一個函式呢?

能!可以藉助本節講的函式模板。

我們知道,資料的值可以通過函式引數傳遞,在函式定義時資料的值是未知的,只有等到函式呼叫時接收了實參才能確定其值。這就是值的引數化。

在C++中,資料的型別也可以通過引數來傳遞,在函式定義時可以不指明具體的資料型別,當發生函式呼叫時,編譯器可以根據傳入的實參自動推斷資料型別。這就是型別的引數化。

值(Value)和型別(Type)是資料的兩個主要特徵,它們在C++中都可以被引數化。

所謂函式模板,實際上是建立一個通用函式,它所用到的資料的型別(包括返回值型別、形參型別、區域性變數型別)可以不具體指定,而是用一個虛擬的型別來代替(實際上是用一個識別符號來佔位),等發生函式呼叫時再根據傳入的實參來逆推出真正的型別。這個通用函式就稱為函式模板(Function Template)。

在函式模板中,資料的值和型別都被引數化了,發生函式呼叫時編譯器會根據傳入的實參來推演形參的值和型別。換個角度說,函式模板除了支援值的引數化,還支援型別的引數化。

一但定義了函式模板,就可以將型別引數用於函式定義和函式聲明瞭。說得直白一點,原來使用 int、float、char 等內建型別的地方,都可以用型別引數來代替。

下面我們就來實踐一下,將上面的四個Swap() 函式壓縮為一個函式模板:

  1. #include <iostream>
  2. using namespace std;
  3. template<typename T> void Swap(T *a, T *b){
  4. T temp = *a;
  5. *a = *b;
  6. *b = temp;
  7. }
  8. int main(){
  9. //交換 int 變數的值
  10. int n1 = 100, n2 = 200;
  11. Swap(&n1, &n2);
  12. cout<<n1<<", "<<n2<<endl;
  13. //交換 float 變數的值
  14. float f1 = 12.5, f2 = 56.93;
  15. Swap(&f1, &f2);
  16. cout<<f1<<", "<<f2<<endl;
  17. //交換 char 變數的值
  18. char c1 = 'A', c2 = 'B';
  19. Swap(&c1, &c2);
  20. cout<<c1<<", "<<c2<<endl;
  21. //交換 bool 變數的值
  22. bool b1 = false, b2 = true;
  23. Swap(&b1, &b2);
  24. cout<<b1<<", "<<b2<<endl;
  25. return 0;
  26. }

執行結果:
200, 100
56.93, 12.5
B, A
1, 0

請讀者重點關注第 4 行程式碼。template是定義函式模板的關鍵字,它後面緊跟尖括號<>,尖括號包圍的是型別引數(也可以說是虛擬的型別,或者說是型別佔位符)。typename是另外一個關鍵字,用來宣告具體的型別引數,這裡的型別引數就是T。從整體上看,template<typename T>被稱為模板頭。

模板頭中包含的型別引數可以用在函式定義的各個位置,包括返回值、形參列表和函式體;本例我們在形參列表和函式體中使用了型別引數T

型別引數的命名規則跟其他識別符號的命名規則一樣,不過使用 T、T1、T2、Type 等已經成為了一種慣例。

定義了函式模板後,就可以像呼叫普通函式一樣來呼叫它們了。

在講解C++函式過載時我們還沒有學到引用(Reference),為了達到交換兩個變數的值的目的只能使用指標,而現在我們已經對引用進行了深入講解,不妨趁此機會來實踐一把,使用引用重新實現 Swap() 這個函式模板:

  1. #include <iostream>
  2. using namespace std;
  3. template<typename T> void Swap(T &a, T &b){
  4. T temp = a;
  5. a = b;
  6. b = temp;
  7. }
  8. int main(){
  9. //交換 int 變數的值
  10. int n1 = 100, n2 = 200;
  11. Swap(n1, n2);
  12. cout<<n1<<", "<<n2<<endl;
  13. //交換 float 變數的值
  14. float f1 = 12.5, f2 = 56.93;
  15. Swap(f1, f2);
  16. cout<<f1<<", "<<f2<<endl;
  17. //交換 char 變數的值
  18. char c1 = 'A', c2 = 'B';
  19. Swap(c1, c2);
  20. cout<<c1<<", "<<c2<<endl;
  21. //交換 bool 變數的值
  22. bool b1 = false, b2 = true;
  23. Swap(b1, b2);
  24. cout<<b1<<", "<<b2<<endl;
  25. return 0;
  26. }

引用不但使得函式定義簡潔明瞭,也使得呼叫函式方便了很多。整體來看,引用讓編碼更加漂亮。

下面我們來總結一下定義模板函式的語法:

template <typename 型別引數1 , typename 型別引數2 , ...> 返回值型別  函式名(形參列表){
    //在函式體中可以使用型別引數
}

型別引數可以有多個,它們之間以逗號,分隔。型別引數列表以< >包圍,形式引數列表以( )包圍。

typename關鍵字也可以使用class關鍵字替代,它們沒有任何區別。C++ 早期對模板的支援並不嚴謹,沒有引入新的關鍵字,而是用 class 來指明型別引數,但是 class 關鍵字本來已經用在類的定義中了,這樣做顯得不太友好,所以後來 C++ 又引入了一個新的關鍵字 typename,專門用來定義型別引數。不過至今仍然有很多程式碼在使用 class 關鍵字,包括 C++ 標準庫、一些開源程式等。

本教程會交替使用 typename 和 class,旨在讓讀者在別的地方遇到它們時不會感覺陌生。更改上面的 Swap() 函式,使用 class 來指明型別引數:

  1. template<class T> void Swap(T &a, T &b){
  2. T temp = a;
  3. a = b;
  4. b = temp;
  5. }

除了將 typename 替換為 class,其他都是一樣的。

為了加深對函式模板的理解,我們再來看一個求三個數的最大值的例子:

  1. #include <iostream>
  2. using namespace std;
  3. //宣告函式模板
  4. template<typename T> T max(T a, T b, T c);
  5. int main( ){
  6. //求三個整數的最大值
  7. int i1, i2, i3, i_max;
  8. cin >> i1 >> i2 >> i3;
  9. i_max = max(i1,i2,i3);
  10. cout << "i_max=" << i_max << endl;
  11. //求三個浮點數的最大值
  12. double d1, d2, d3, d_max;
  13. cin >> d1 >> d2 >> d3;
  14. d_max = max(d1,d2,d3);
  15. cout << "d_max=" << d_max << endl;
  16. //求三個長整型數的最大值
  17. long g1, g2, g3, g_max;
  18. cin >> g1 >> g2 >> g3;
  19. g_max = max(g1,g2,g3);
  20. cout << "g_max=" << g_max << endl;
  21. return 0;
  22. }
  23. //定義函式模板
  24. template<typename T> //模板頭,這裡不能有分號
  25. T max(T a, T b, T c){ //函式頭
  26. T max_num = a;
  27. if(b > max_num) max_num = b;
  28. if(c > max_num) max_num = c;
  29. return max_num;
  30. }

執行結果:
12  34  100↙
i_max=100
73.234  90.2  878.23↙
d_max=878.23
344  900  1000↙
g_max=1000

函式模板也可以提前宣告,不過宣告時需要帶上模板頭,並且模板頭和函式定義(宣告)是一個不可分割的整體,它們可以換行,但中間不能有分號。

兩者的區別:

函式模板與類模板有什麼區別?答:函式模板的例項化是由編譯程式在處理函式呼叫時自動完成的,而類模板的例項化 必須由程式設計師在程式中顯式地指定。

即函式模板允許隱式呼叫和顯式呼叫而類模板只能顯示呼叫這期間有涉及到函式模板與模板函式,類模板與模板類的概念(類似於類與類物件的區別)請看下面例子

注意:模板類的函式宣告和實現必須都在標頭檔案中完成,不能像普通類那樣宣告在.h檔案中實現在.cpp檔案中,原因可以看連結http://hi.baidu.com/cn_rigel/blog/item/6cf6fc083723e2286a60fb53.html

#include "stdafx.h"
#include <iostream>
using namespace std;

//使用模板建立一個返回最大值的函式
//這是一個函式模板
template <class Type>
Type MaxValue(Type a,Type b)
{
    if ( a > b)
    {
        return a;
    }
    else
        return b;
}

//建立一個堆疊模板類
//這是一個類模板
template <class T>
class Stack
{
public:
    Stack(){ m_nPos = 0;}
    ~Stack(){}

    void Push(T value);
    T Pop();

    bool IsEmpty()
    {
        return m_nPos == 0;
    }
    bool HasElement()
    {
        return !IsEmpty();
    }
    bool IsFull()
    {
        return m_nPos == STATCK_SIZE;
    }

private:
    int m_nPos;
    //使用常量表示堆疊的大小

    const static int STATCK_SIZE = 100;
    T m_Data[STATCK_SIZE];
};
//模板類的成員函式實現

template <class T>
void Stack<T> ::Push(T value)
{
    //使用後置遞增操作符

    m_Data[m_nPos++] = value;
}
template <class T>
T Stack<T>::Pop()
{
    //使用前置遞減操作符

    return m_Data[--m_nPos];
}

void TestMaxValue()
{
    //隱式呼叫
 
//函式模板的例項化在程式呼叫時自動完成
cout << MaxValue(100, 204)<< endl;//MaxValue(100, 204)這是一個模板函式
    cout << MaxValue(2.5002,30.003) << endl;//MaxValue(2.5002,30.003)這也是一個模板函式
//當然由程式設計師自己指定也可以
    //顯示呼叫

    cout << MaxValue<int>(10,20) << endl;
    cout << MaxValue<double>(2.5002,30.003) << endl;
}

void TestStack()
{
    //測試模板類(整數)

    Stack <int> intStack;//類模板的例項化由程式設計師顯示的指定
    intStack.Push(10);
    intStack.Push(20);
    intStack.Push(30);

    while (intStack.HasElement())
    {
        cout << intStack.Pop() << endl;
    }

    //測試模板類(浮點)

    Stack <float> floatStack;//類模板的例項化由程式設計師顯示的指定
    floatStack.Push(1.001);
    floatStack.Push(2.002);
    floatStack.Push(3.003);

    while (floatStack.HasElement())
    {
        cout << floatStack.Pop() << endl;
    }

    //測試動態建立物件

    //Stack建立的指標必須指明型別

    Stack<int>* pInt = new Stack<int>();類模板的例項化由程式設計師顯示的指定
    pInt->Push(10);


    pInt->Push(20);
    pInt->Push(30);

    while (pInt->HasElement())
    {
        cout << pInt->Pop() << endl;
    }
    if ( pInt != NULL)
    {
        delete pInt;
        pInt = NULL;
    }
}

區別2:

在C++中有好幾個這樣的術語,但是我們很多時候用的並不正確,幾乎是互相替換混淆使用。下面我想徹底辨清幾個術語,這樣就可以避免很多概念上的混淆和使用上的錯誤。

這幾個詞是:

函式指標——指標函式

陣列指標——指標陣列

類模板——模板類

函式模板——模板函式

最終在使用中,我們就可以讓它們實至名歸,名正言順。

1.函式指標——指標函式

函式指標的重點是指標。表示的是一個指標,它指向的是一個函式,例子:

int   (*pf)();

指標函式的重點是函式。表示的是一個函式,它的返回值是指標。例子:

int*   fun();

 
2.陣列指標——指標陣列

陣列指標的重點是指標。表示的是一個指標,它指向的是一個數組,例子:

int   (*pa)[8];

指標陣列的重點是陣列。表示的是一個數組,它包含的元素是指標。例子;

int*   ap[8];

 

3.類模板——模板類(class   template——template   class)

類模板的重點是模板。表示的是一個模板,專門用於產生類的模子。例子:

template   <typename   T>

class   Vector

{

            …

};

使用這個Vector模板就可以產生很多的class(類),Vector <int> 、Vector <char> 、Vector <   Vector <int>   > 、Vector <Shape*> ……。

模板類的重點是類。表示的是由一個模板生成而來的類。例子:

上面的Vector <int> 、Vector <char> 、……全是模板類。

這兩個詞很容易混淆,我看到很多文章都將其用錯,甚至一些英文文章也是這樣。將他們區分開是很重要的,你也就可以理解為什麼在定義模板的標頭檔案.h時,模板的成員函式實現也必須寫在標頭檔案.h中,而不能像普通的類(class)那樣,class的宣告(declaration)寫在.h檔案中,class的定義(definition)寫在.cpp檔案中。請參照Marshall   Cline的《C++   FAQ   Lite》中的[34]   Container   classes   and   templates中的[34.12]   Why   can 't   I   separate   the   definition   of   my   templates   class   from   it 's   declaration   and   put   it   inside   a   .cpp   file?   URL地址是http://www.parashift.com/c++-faq-lite/containers-and-templates.html#faq-34.12

我將幾句關鍵的段落摘錄如下,英文很好理解:

In   order   for   the   compiler   to   generate   the   code,   it   must   see   both   the   template   definition   (not   just   declaration)   and   the   specific   types/whatever   used   to   "fill   in "   the   template.   For   example,   if   you 're   trying   to   use   a   Foo <int> ,   the   compiler   must   see   both   the   Foo   template   and   the   fact   that   you 're   trying   to   make   a   specific   Foo <int> .  

Suppose   you   have   a   template   Foo   defined   like   this:  

  template <class   T>
  class   Foo   {
  public:
      Foo();
      void   someMethod(T   x);
  private:
      T   x;
  };  

Along   with   similar   definitions   for   the   member   functions:  

  template <class   T>
  Foo <T> ::Foo()
  {
      ...
  }
 
  template <class   T>
  void   Foo <T> ::someMethod(T   x)
  {
      ...
  }  

Now   suppose   you   have   some   code   in   file   Bar.cpp   that   uses   Foo <int> :  

  //   Bar.cpp
 
  void   blah_blah_blah()
  {
      ...
      Foo <int>   f;
      f.someMethod(5);
      ...
  }  

Clearly   somebody   somewhere   is   going   to   have   to   use   the   "pattern "   for   the   constructor   definition   and   for   the   someMethod()   definition   and   instantiate   those   when   T   is   actually   int.   But   if   you   had   put   the   definition   of   the   constructor   and   someMethod()   into   file   Foo.cpp,   the   compiler   would   see   the   template   code   when   it   compiled   Foo.cpp   and   it   would   see   Foo <int>   when   it   compiled   Bar.cpp,   but   there   would   never   be   a   time   when   it   saw   both   the   template   code   and   Foo <int> .   So   by   rule   above,   it   could   never   generate   the   code   for   Foo <int> ::someMethod().  

關於一個預設模板引數的例子:

template   <typename   T   =   int>

class   Array

{

            …

};

第一次我定義這個模板並使用它的時候,是這樣用的:

Array   books;//我認為有預設模板引數,這就相當於Array <int>   books

上面的用法是錯誤的,編譯不會通過,原因是Array不是一個類。正確的用法是Array <>   books;

這裡Array <> 就是一個用於預設模板引數的類模板所生成的一個具體類。

 

4.函式模板——模板函式(function   template——template   function)

函式模板的重點是模板。表示的是一個模板,專門用來生產函式。例子:

template   <typename   T>

void   fun(T   a)

{

            …

}

在運用的時候,可以顯式(explicitly)生產模板函式,fun <int> 、fun <double> 、fun <Shape*> ……。

也可以在使用的過程中由編譯器進行模板引數推導,幫你隱式(implicitly)生成。

fun(6);//隱式生成fun <int>

fun(8.9);//隱式生成fun <double>

fun(‘a’);//   隱式生成fun <char>

Shape*   ps   =   new   Cirlcle;

fun(ps);//隱式生成fun <Shape*>

 

模板函式的重點是函式。表示的是由一個模板生成而來的函式。例子:

上面顯式(explicitly)或者隱式(implicitly)生成的fun <int> 、fun <Shape*> ……都是模板函式。

關於模板本身,是一個非常龐大的主題,要把它講清楚,需要的不是一篇文章,而是一本書,幸運的是,這本書已經有了:David   Vandevoorde,   Nicolai   M.   Josuttis寫的《C++   Templates:   The   Complete   Guide》。可惜在大陸買不到紙版,不過有一個電子版在網上流傳。

 

模板本身的使用是很受限制的,一般來說,它們就只是一個產生類和函式的模子。除此之外,運用的領域非常少了,所以不可能有什麼模板指標存在的,即指向模板的指標,這是因為在C++中,模板就是一個程式碼的程式碼生產工具,在最終的程式碼中,根本就沒有模板本身存在,只有模板具現出來的具體類和具體函式的程式碼存在。

但是類模板(class   template)還可以作為模板的模板引數(template   template   parameter)使用,在Andrei   Alexandrescu的《Modern   C++   Design》中的基於策略的設計(Policy   based   Design)中大量的用到。

template <   typename   T,   template <typename   U>   class   Y>

class   Foo

{

            …

};

 

從文章的討論中,可以看到,名字是非常重要的,如果對名字的使用不恰當的話,會引起很多的麻煩和誤解。我們在實際的程式中各種識別符號的命名也是一門學問,為了清晰易懂,有時候還是需要付出一定的代價。