C++ 模板



模板是泛型編程的基礎,泛型編程及以一種獨立於任何特定類型的方式編寫代碼。

模板是創建泛型類或函數的藍圖或公式。庫容器:比如迭代器,都是泛型編程的例子,他們都使用類模板的概念, 每個容器都有一個單一的定義,比如向量,我們可以定義許多不同類型的向量,比如vector <int>或 vector <string>。

模板是一種對類型進行參數化的工具,通常有兩種形式:函數模板和類模板;

(1) 函數模板針對僅參數類型不同的函數;
(2) 類模板針對僅數據成員和成員函數類型不同的類。

使用模板讓程序員能夠編寫與類型無關的代碼。比如:編寫一個交換兩個int型的swap函數,這個函數就只能實現int型,對double,字符這些類型無法實現,要實現這些類型的交換就要重新寫另一個swap函數。使用模板的目的就是要讓這程序的實現與類型無關,比如一個swap模板函數,即可以實現int型,又可以實現double型的交換。

注意:模板的聲明或定義只能在全局,命名空間或類范圍內進行。即不能在局部范圍,函數內進行,比如不能在main函數中聲明或定義一個模板。

一、函數模板

在函數重載中,為了交換不同類型變量的值,通過函數重載定義了四個名字相同,參數列表不同的函數,如下所示:

//交換 int 變量的值
void Swap(int *a, int *b){
    int temp = *a;
    *a = *b;
    *b = temp;
}
//交換 float 變量的值
void Swap(float *a, float *b){
    float temp = *a;
    *a = *b;
    *b = temp;
}

這些函數雖然在調用時方便了一些,但從本質上說還是定義了功能相同、函數體相同的函數,只是數據的類型不同而已,這看起來有點浪費代碼,能不能把它們壓縮成一個函數呢?
能!可以借助本節講的函數模板。

數據的值可以通過函數參數傳遞,在函數定義時數據的值是未知的,只有等到函數調用時接收了實參才能確定其值。這就是值的參數化。
在C++中,數據的類型也可以通過參數傳遞,在函數定義時可以不指明具體的數據類型,當發生函數調用時,編譯器可以根據傳入的實參自動推斷數據類型。這就是類型的參數化。

值(Value)和類型(Type)是數據的兩個主要特征,它們在C++中都可以被參數化。

函數模板,實際上是建立一個通用函數,它所用到的數據的類型(包括返回值類型、形參類型、局部變量類型)可以不具體指定,而是用一個虛擬的類型來代替(實際上是用一個標識符來占位),等發生函數調用時再根據傳入的實參來逆推出真正的類型。
這個通用函數就稱為函數模板。在函數模板中,數據的值和類型都被參數化了,發生函數調用時編譯器會根據傳入的實參來推演形參的值和類型。換個角度說,函數模板除了支持值的參數化,還支持類型的參數化。

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

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

#include <iostream>
using namespace std;

template<typename T>
void Swap(T &a, T &b){
    T temp = a;
    a = b;
    b = temp;
}
int main(){
    //交換 int 變量的值
    int n1 = 100, n2 = 200;
    Swap(n1, n2);
    cout<<n1<<", "<<n2<<endl;
   
    //交換 float 變量的值
    float f1 = 12.5, f2 = 56.93;
    Swap(f1, f2);
    cout<<f1<<", "<<f2<<endl;

    return 0;
}

定義模板函數的語法:

template <typename 類型參數1 , typename 類型參數2 , ...>
返回值類型 函數名(形參列表)
{
    //在函數體中可以使用類型參數
}
// 類型參數可以有多個,它們之間以逗號,分隔。類型參數列表以< >包圍,形式參數列表以( )包圍。

注意:模板中typename和class關鍵字的區別?

template<typename T> class Widget; // uses "typename"
template<class T> class Widget; // uses "class"

   typename關鍵字也可以使用class關鍵字替代。C++早期對模板的支持並不嚴謹,沒有引入新的關鍵字,而是用class來指明類型參數,但是class關鍵字本來已經用在類的定義中了,這樣做顯得不太友好,所以后來C++又引入了一個新的關鍵字typename,專門用來定義類型參數。不過至今仍然有很多代碼在使用class關鍵字,包括C++標准庫、一些開源程序等。
   但是,《Effective C++》中建議我們盡量使用typename而不是class來聲明一個模板。因為class給人的感覺總像是T應該是一個類,或者說是用戶自定義的類型,而typename則更像是表明T可以是任意一種類型。雖然在C++編譯器看來,這兩種聲明方式沒有任何的區別,其意義是完全一樣的。

不過,typename關鍵字在模板中還有另一種用法: 使用嵌套依賴類型

class MyArray
{
public:
    typedef int LengthType;
    .....
}

template<class T>
void MyMethod(T myarr)
{ 
    typedef typename T::LengthType LengthType; 
    LengthType length = myarr.GetLength; 
}

這個時候typename的作用就是告訴c++編譯器,typename后面的字符串為一個類型名稱,而不是成員函數或者成員變量,這個時候如果前面沒有typename,編譯器沒有任何辦法知道T::LengthType是一個類型還是一個成員名稱(靜態數據成員或者靜態函數),所以編譯不能夠通過。

二、類模板

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

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

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

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

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

template<typename T1, typename T2>  //這里不能有分號
class Point
{
public:
    Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
    T1 getX() const;  //獲取x坐標
    void setX(T1 x);  //設置x坐標
    T2 getY() const;  //獲取y坐標
    void setY(T2 y);  //設置y坐標
private:
    T1 m_x;  //x坐標
    T2 m_y;  //y坐標
};
// x 坐標和 y 坐標的數據類型不確定,借助類模板可以將數據類型參數化,這樣就不必定義多個類了。

注意:模板頭和類頭是一個整體,可以換行,但是中間不能有分號。
上面的代碼僅僅是類的聲明,我們還需要在類外定義成員函數。在類外定義成員函數時仍然需要帶上模板頭,格式為:

template<typename 類型參數1 , typename 類型參數2 , …>
返回值類型 類名<類型參數1 , 類型參數2, ...>::函數名(形參列表)
{
    //TODO:
}
// 第一行是模板頭,第二行是函數頭,它們可以合並到一行,不過為了讓代碼格式更加清晰,一般是將它們分成兩行。

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

template<typename T1, typename T2>  //模板頭
T1 Point<T1, T2>::getX() const /*函數頭*/ {
    return m_x;
}
template<typename T1, typename T2>
void Point<T1, T2>::setX(T1 x){
    m_x = x;
}
template<typename T1, typename T2>
T2 Point<T1, T2>::getY() const{
    return m_y;
}
template<typename T1, typename T2>
void Point<T1, T2>::setY(T2 y){
    m_y = y;
}

使用類模板創建對象

上面的兩段代碼完成了類的定義,接下來就可以使用該類創建對象了。使用類模板創建對象時,需要指明具體的數據類型。請看下面的代碼:

Point<int, int> p1(10, 20);
Point<int, float> p2(10, 15.5);
Point<float, char*> p3(12.4, "東經180度");
// 與函數模板不同的是,類模板在實例化時必須顯式地指明數據類型,編譯器不能根據給定的數據推演出數據類型。

除了對象變量,我們也可以使用對象指針的方式來實例化:

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

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

//賦值號兩邊的數據類型不一致
Point<float, float> *p = new Point<float, int>(10.6, 109);
//賦值號右邊沒有指明數據類型
Point<float, float> *p = new Point(10.6, 109);

綜合示例
【實例1】將上面的類定義和類實例化的代碼整合起來,構成一個完整的示例,如下所示:

#include <iostream>
using namespace std;

template<class T1, class T2>  //這里不能有分號
class Point{
public:
  Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
  T1 getX() const;  //獲取x坐標
  void setX(T1 x);  //設置x坐標
  T2 getY() const;  //獲取y坐標
  void setY(T2 y);  //設置y坐標
private:
  T1 m_x;  //x坐標
  T2 m_y;  //y坐標
};

template<class T1, class T2>  //模板頭
T1 Point<T1, T2>::getX() const /*函數頭*/ {
  return m_x;
}

template<class T1, class T2>
void Point<T1, T2>::setX(T1 x){
  m_x = x;
}

template<class T1, class T2>
T2 Point<T1, T2>::getY() const{
  return m_y;
}

template<class T1, class T2>
void Point<T1, T2>::setY(T2 y){
  m_y = y;
}

int main(){
  Point<int, int> p1(10, 20);
  cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;

  Point<int, char*> p2(10, "東經180度");
  cout<<"x="<<p2.getX()<<", y="<<p2.getY()<<endl;

  Point<char*, char*> *p3 = new Point<char*, char*>("東經180度", "北緯210度");
  cout<<"x="<<p3->getX()<<", y="<<p3->getY()<<endl;

  return 0;
}
// x=10, y=20
// x=10, y=東經180度
// x=東經180度, y=北緯210度

【實例2】用類模板實現可變長數組。

#include <iostream>
#include <cstring>

using namespace std;
template <class T>
class CArray
{
    int size; //數組元素的個數
    T *ptr; //指向動態分配的數組
public:
    CArray(int s = 0);  //s代表數組元素的個數
    CArray(CArray & a);
    ~CArray();
    void push_back(const T & v); //用於在數組尾部添加一個元素v
    CArray & operator=(const CArray & a); //用於數組對象間的賦值
    T length() { return size; }
    T & operator[](int i)
    {//用以支持根據下標訪問數組元素,如a[i] = 4;和n = a[i]這樣的語句
        return ptr[i];
    }
};
template<class T>
CArray<T>::CArray(int s):size(s)
{
     if(s == 0)
         ptr = NULL;
    else
        ptr = new T[s];
}
template<class T>
CArray<T>::CArray(CArray & a)
{
    if(!a.ptr) {
        ptr = NULL;
        size = 0;
        return;
    }
    ptr = new T[a.size];
    memcpy(ptr, a.ptr, sizeof(T ) * a.size);
    size = a.size;
}
template <class T>
CArray<T>::~CArray()
{
     if(ptr) delete [] ptr;
}
template <class T>
CArray<T> & CArray<T>::operator=(const CArray & a)
{ //賦值號的作用是使"="左邊對象里存放的數組,大小和內容都和右邊的對象一樣
    if(this == & a) //防止a=a這樣的賦值導致出錯
    return * this;
    if(a.ptr == NULL) {  //如果a里面的數組是空的
        if( ptr )
            delete [] ptr;
        ptr = NULL;
        size = 0;
        return * this;
    }
     if(size < a.size) { //如果原有空間夠大,就不用分配新的空間
         if(ptr)
            delete [] ptr;
        ptr = new T[a.size];
    }
    memcpy(ptr,a.ptr,sizeof(T)*a.size);   
    size = a.size;
     return *this;
}
template <class T>
void CArray<T>::push_back(const T & v)
{  //在數組尾部添加一個元素
    if(ptr) {
        T *tmpPtr = new T[size+1]; //重新分配空間
    memcpy(tmpPtr,ptr,sizeof(T)*size); //拷貝原數組內容
    delete []ptr;
    ptr = tmpPtr;
}
    else  //數組本來是空的
        ptr = new T[1];
    ptr[size++] = v; //加入新的數組元素
}
int main()
{
    CArray<int> a;
    for(int i = 0;i < 5;++i)
        a.push_back(i);
    for(int i = 0; i < a.length(); ++i)
        cout << a[i] << " ";
    cout << endl;
    return 0;
}
// 0 1 2 3 4 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM