1. 程式人生 > >C++語言學習(十七)——模板

C++語言學習(十七)——模板

china typename itl while ptr 特殊 實例化 需求 所有

C++語言學習(十七)——模板

一、模板簡介

泛型(Generic Programming)即是指具有在多種數據類型上皆可操作的含意。 泛型編程的代表作品STL是一種高效、泛型、可交互操作的軟件組件。
泛型編程最初誕生於C++中,目的是為了實現C++的STL(標準模板庫)。其語言支持機制就是模板(Templates)。模板的核心思想是參數化類型,即把一個原本特定於某個類型的算法或類當中的類型信息抽掉,抽出來做成模板參數T。

二、函數模板

1、宏實現交換函數

定義一個交換兩個數的宏

#define SWAP(t, a, b)   do                      {                           t c = a;                a = b;                  b = c;              }while(0);

宏代碼塊實現的優點是代碼復用,適合所有類型;缺點是缺少類型檢查。

2、函數重載實現

#include <iostream>
using namespace std;
void swap(int &a, int& b)
{
    int t = a;
    a = b;
    b = t;
}
void swap(double &a,double b)
{
    double t = a;
    a = b;
    b = t;
}
int main()
{
    int ia = 10; int ib = 20;
    swap(ia,ib);
    cout<<ia<<ib<<endl;
    double da = 10, db = 20;
    swap(da,db);
    cout<<da<<db<<endl;
    return 0;
}

函數重載實現的優點是真正進行函數調用,C++編譯器進行類型檢查;缺點是根據類型重復定義函數,無法代碼復用。

3、函數模板

函數模板是可用不同類型進行調用的特殊函數,關鍵在於類型參數化。
函數模板的語法格式如下:

template<typename/class 類型參數表>
返回類型 函數模板名(函數參數列表)
{
    函數模板定義體
}

template關鍵字用於聲明開始進行泛型編程。
typename關鍵字用於聲明泛指類型。
函數模板可以自動推導類型進行調用,也可以顯示指定具體類型進行調用。

4、函數模板實現

#include <iostream>
using namespace std;
template <typename T>
void Swap(T& a,T &b )
{
    T t = a;
    a = b;
    b = t;
}
int main()
{
    int ia = 10; int ib = 20;
    Swap(ia,ib); //Swap<int>(ia,ib);
    cout<<ia<<ib<<endl;
    double da = 10, db = 20;
    Swap(da,db); //Swap<double>(da,db);
    cout<<da<<db<<endl;
    string sa ="china"; string sb = "America";
    Swap(sa,sb);
    cout<<sa<<sb<<endl;
    return 0;
}

判斷一個變量是不是指針類型示例:

template 
<typename T>
bool isPtr(T *p) 
{
    return true;
}

template 
<typename T>
bool isPtr(T t)
{
    return false;
}

函數模板,只適用於函數的參數個數相同而類型不同,且函數體相同的情況。如果個數不同,則不能用函數模板。

5、函數模板分析

C++編譯器從函數模板通過具體類型產生不同的函數,C++編譯器會對函數模板進行兩次編譯,一次是函數模板代碼進行編譯,一次是參數替換後的函數代碼進行編譯。

#include <iostream>

using namespace std;

template <typename T>
void Swap(T& a, T& b)
{
    T c = a;
    a = b;
    b = c;
}

class Test
{

};

typedef void (*pFuncInt)(int&, int&);
typedef void (*pFuncDouble)(double&, double&);
typedef void (*pFuncTest)(Test&, Test&);

int main(int argc, char *argv[])
{
    pFuncInt pi = Swap;//Swap<int>
    printf("0x%x\n", pi);
    pFuncDouble pd = Swap;//Swap<double>
    printf("0x%x\n", pd);
    pFuncTest pt = Swap;//Swap<Test>
    printf("0x%x\n", pt);

    return 0;
}

函數模板本身不允許隱式類型轉換,因此,自動推導類型時需要嚴格匹配,但當顯示指定類型參數時可以進行隱式類型轉換。
函數模板中的返回值類型必須顯示指定。


add<int>(ia,ib);

6、多類型參數函數模板

函數模板可以定義多個不同的類型參數,但無法自動推導返回值類型,可以從左向右部分指定類型參數,實際工程中將返回值作為第一個類型參數,必須顯式指定。

#include <iostream>
#include <string>

using namespace std;

template 
< typename T1, typename T2, typename T3 >
T1 Add(T2 a, T3 b)
{
    return static_cast<T1>(a + b);
}

int main()
{
    // T1 = int, T2 = double, T3 = double
    int r1 = Add<int>(0.5, 0.8);

    // T1 = double, T2 = float, T3 = double
    double r2 = Add<double, float>(0.5, 0.8);

    // T1 = float, T2 = float, T3 = float
    float r3 = Add<float, float, float>(0.5, 0.8);

    cout << "r1 = " << r1 << endl;     // r1 = 1
    cout << "r2 = " << r2 << endl;     // r2 = 1.3
    cout << "r3 = " << r3 << endl;     // r3 = 1.3

    return 0;
}

7、普通函數與函數模板的關系

函數模板可以被重載,C++編譯器優先考慮普通函數,但如果函數模板可以產生更好的匹配,則使用函數模板,可以通過空模板實參列表限定只能使用函數模板。

#include <iostream>
#include <string>

using namespace std;

template < typename T >
T Max(T a, T b)
{
    cout << "T Max(T a, T b)" << endl;

    return a > b ? a : b;
}

int Max(int a, int b)
{
    cout << "int Max(int a, int b)" << endl;

    return a > b ? a : b;
}

template < typename T >
T Max(T a, T b, T c)
{
    cout << "T Max(T a, T b, T c)" << endl;

    return Max(Max(a, b), c);
}

int main()
{
    int a = 1;
    int b = 2;

    cout << Max(a, b) << endl;                   // 普通函數 Max(int, int)

    cout << Max<>(a, b) << endl;                 // 函數模板 Max<int>(int, int)

    cout << Max(3.0, 4.0) << endl;               // 函數模板 Max<double>(double, double)

    cout << Max(5.0, 6.0, 7.0) << endl;          // 函數模板 Max<double>(double, double, double)

    cout << Max(‘a‘, 100) << endl;               // 普通函數 Max(int, int)

    return 0;
}

三、類模板

1、類模板的定義

C++語言中將模板的思想應用於類,使得類的實現不關註數據元素的具體類型,只關註類需要實現的功能。
類模板的定義語法如下:

template <typename T>
class classname
{

};

在類聲明前使用template進行標識,&lt;typename T&gt;用於說明類中使用泛指類型T。
類內定義成員函數

template<typename T>
class classname
{
public:
    void push(int size)
    {
}
}

類外定義函數

template<typename T>
void classname<T>::push(T data)
{
}

類模板實例化為模板類:


classname<double> object;

類模板是類的抽象,類是類模板的實例。

2、類模板應用

類模板只能顯示指定類型參數,無法自動推導。聲明的泛型類型參數可以出現在類模板的任意地方。
類模板必須在頭文件中實現,不能分開實現在不同文件中。類模板的成員函數需要定義在外部定義時,每個成員函數需要加上類模板template&lt;typename T&gt;聲明。
類模板適合以相同的邏輯處理不同的數據類型的數據,因此非常適合編寫數據結構相關代碼。

#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
using namespace std;
template<typename T>
class Stack
{
public:
    Stack(int size)
    {
        space = new T[size];
        top = 0;
    }
    ~Stack();
    bool isEmpty();
    bool isFull();
    void push(T data);
    T pop();
private:
    T* space;
    int top;
};
template<typename T>
Stack<T>::~Stack()
{
    delete []space;
}
template<typename T>
bool Stack<T>::isEmpty()
{
    return top == 0;
}
template<typename T>
bool Stack<T>::isFull()
{
    return top == 1024;
}
template<typename T>
void Stack<T>::push(T data)
{
    space[top++] = data;
}
template<typename T>
T Stack<T>::pop()
{
    return space[--top];
}
int main()
{
    Stack<double> s(100); //Stack<string> s(100);
    if(!s.isFull())
        s.push(10.3);
    if(!s.isFull())
        s.push(20);
    if(!s.isFull())
        s.push(30);
    if(!s.isFull())
        s.push(40);
    if(!s.isFull())
        s.push(50);
    while(!s.isEmpty())
        cout<<s.pop()<<endl;
    return 0;
}

3、類模板分析

類模板通過具體類型產生不同的類,C++編譯器在類模板聲明的地方對類模板代碼本身進行編譯,在使用的地方對類模板參數替換後產生的代碼進行編譯。
類模板可以定義多個不同類型參數。

#include <iostream>
#include <string>

using namespace std;

template <typename T>
class Operator
{
public:
    Operator()
    {
        cout << "Operator()" << endl;
    }
    T add(T a, T b)
    {
        cout << "T add(T a, T b)" << endl;
        return a + b;
    }
    T minus(T a, T b)
    {
        return a - b;
    }
    T multiply(T a, T b)
    {
        return a * b;
    }
    T divide(T a, T b)
    {
        return a / b;
    }
};

int main(int argc, char *argv[])
{
    Operator<int> op1;
    cout << op1.add(1, 2) << endl;
    cout << op1.add(1, 2) << endl;

    Operator<string> op2;

    cout << op2.add("D.T.", "Software") << endl;

    return 0;
}
// output:
// Operator()
// T add(T a, T b)
// 3
// Operator()
// T add(T a, T b)
// 3
// Operator()
// T add(T a, T b)
// Hello World

上述代碼中,類模板中的函數代碼在使用的時候才會被分別編譯。

四、模板的特化

1、類模板的特化

類模板可以被特化,以下情況需要特化類模板:
A、指定特定類型的實現
B、部分參數類型必須顯示指定
C、根據類型參數分開實現類模板
類模板的特化分為部分特化和完全特化。部分特化是指用特定規則約束類型參數,完全特化是指完全顯示指定類型參數。
類模板的特化是模板的分開實現,本質上是同一個類模板,特化類模板必須顯示指定每一個類型參數。編譯器會自動優先選擇特化類模板。

#include <iostream>

using namespace std;

template
<typename T1, typename T2>
class Test
{
public:
    void add(T1 a, T2 b)
    {
        cout << "void add(T1 a, T2 b)" << endl;
        cout << a + b << endl;
    }
};

//部分特化
template
<typename T>
class Test<T,T>
{
public:
    void add(T a, T b)
    {
        cout << "void add(T a, T b)" << endl;
        cout << a + b << endl;
    }
    void print()
    {
        cout << "class Test <T,T>" << endl;
    }
};

//完全特化
template
<>
class Test<int,int>
{
public:
    void add(int a, int b)
    {
        cout << "void add(int a, int b)" << endl;
        cout << a + b << endl;
    }
    void print()
    {
        cout << "class Test<int,int>" << endl;
    }
};

int main(int argc, char *argv[])
{
    Test<int, int> t1;//完全特化
    t1.add(1,2);
    t1.print();

    Test<double, double> t2;//部分特化
    t2.add(3.14,2.0);
    t2.print();

    Test<float, double> t3;//類模板
    t3.add(3.14,2.0);

    return 0;
}

// output:
// void add(int a, int b)
// 3
// class Test<int,int>
// void add(T a, T b)
// 5.14
// class Test <T,T>
// void add(T1 a, T2 b)
// 5.14

2、函數模板的特化

函數模板只支持模板的完全特化。

#include <iostream>

using namespace std;

//函數模板
template
<typename T>
bool Equal(T a, T b)
{
    cout << "bool Equal(T a, T b)" << endl;
    return a == b;
}

//函數特化模板
template
< >
bool Equal<double>(double a, double b)
{
    const double delta = 0.00000000000001;
    double r = a - b;
    cout << "bool Equal<double>(double a, double b)" << endl;
    return (-delta < r) && (r < delta);
}

//函數重載
bool Equal(double a, double b)
{
    const double delta = 0.00000000000001;
    double r = a - b;
    cout << "bool Equal(double a, double b)" << endl;
    return (-delta < r) && (r < delta);
}

int main(int argc, char *argv[])
{
    Equal<double>(0.1,0.1);//函數特化模板
    Equal<int>(10,10);//函數模板
    Equal(0.1,0.1);//函數重載

    return 0;
}

// output:
// bool Equal<double>(double a, double b)
// bool Equal(T a, T b)
// bool Equal(double a, double b)

工程實踐中當需要重載函數模板時,優先使用函數模板特化,當函數模板特化無法滿足需求時,使用函數重載。

五、數組類模板

1、數值型模板

模板參數可以是數值型參數,數值型模板參數存在限制:
A、變量不能作為模板參數
B、浮點數不能作為模板參數
C、類對象不能作為模板參數
模板參數是在編譯階段處理的,因此在編譯階段需要唯一確定。
使用最高效方式求1+2+3+4......+100

#include <iostream>
using namespace std;
template
<int N>
class Sum
{
public:
    static const int value = Sum<N-1>::value + N;
};

template
<>
class Sum<1>
{
public:
    static const int value = 1;
};

int main(int argc, char *argv[])
{
    cout<<Sum<100>::value<<endl;
    return 0;
}

2、數組模板

#ifndef _ARRAY_H_
#define _ARRAY_H_

template
< typename T, int N >
class Array
{
    T m_array[N];
public:
    int length();
    bool set(int index, T value);
    bool get(int index, T& value);
    T& operator[] (int index);
    T operator[] (int index) const;
    virtual ~Array();
};

template
< typename T, int N >
int Array<T, N>::length()
{
    return N;
}

template
< typename T, int N >
bool Array<T, N>::set(int index, T value)
{
    bool ret = (0 <= index) && (index < N);
    if( ret )
    {
        m_array[index] = value;
    }
    return ret;
}

template
< typename T, int N >
bool Array<T, N>::get(int index, T& value)
{
    bool ret = (0 <= index) && (index < N);
    if( ret )
    {
        value = m_array[index];
    }
    return ret;
}

template
< typename T, int N >
T& Array<T, N>::operator[] (int index)
{
    return m_array[index];
}

template
< typename T, int N >
T Array<T, N>::operator[] (int index) const
{
    return m_array[index];
}

template
< typename T, int N >
Array<T, N>::~Array()
{

}

#endif

六、智能指針類模板

智能指針是C++開發庫的重要類模板之一,是自動內存管理的主要手段,可以避開內存的相關問題。

1、STL的智能指針

STL中的智能指針分為auto_ptr、shared_ptr、weak_ptr、unique_ptr四類。
auto_ptr智能指針的特性:
A、生命周期結束時,銷毀指向的內存空間
B、不能指向堆數組,只能指向堆對象
C、一塊堆空間只能屬於一個智能指針
D、多個智能指針對象不能指向同一塊空間
shared_ptr智能指針的特性:
帶有引用計數機制,支持多個指針指向同一對象內存空間。
weak_ptr智能指針的特性:
weak_ptr是一種弱引用,指向shared_ptr所管理的對象。
unique_ptr智能指針的特性:
一個指針對象指向一片內存空間,不能拷貝構造和賦值

STL智能指針使用實例:

#include <iostream>
#include <memory>

using namespace std;

class Test
{
    string m_name;
public:
    Test(const char* name)
    {
        cout << "Hello, " << name << "." << endl;
        m_name = name;
    }

    void print()
    {
        cout << "I‘m " << m_name << "." << endl;
    }

    ~Test()
    {
        cout << "Goodbye, " << m_name << "." << endl;
    }
};

int main(int argc, char *argv[])
{
    auto_ptr<Test> pt(new Test("D.T.Software"));
    cout << "pt = " << pt.get() << endl;
    pt->print();
    cout << endl;

    auto_ptr<Test> pt1(pt);
    cout << "pt = " << pt.get() << endl;//NULL
    cout << "pt1 = " << pt1.get() << endl;//

    return 0;
}

2、QT中的智能指針

QT中的主要智能指針有:QPointer、QSharedPointer、QWeakPointer、QScopedPointer、QSharedDataPoiner、QExplicitlySharedDataPointer。
QPointer智能指針特性:
A、當其所指向的對象被銷毀時會被自動置空
B、析構時不會自動銷毀所指向的對象
多個QPointer指針對象可以指向同一內存空間,當所指向對象被銷毀時指針會被自動置空,但是指針對象析構時不會自動銷毀所指向的對象。
QSharedPointer智能指針特性:
A、引用計數型智能指針
B、可以被自由的拷貝和賦值
C、當引用計數為0時才刪除指向的對象
QT中智能指針使用實例:

#include <QPointer>
#include <QSharedPointer>
#include <QDebug>

class Test : public QObject
{
    QString m_name;
public:
    Test(const char* name)
    {
        qDebug() << "Hello, " << name << ".";

        m_name = name;
    }

    void print()
    {
        qDebug() << "I‘m " << m_name << ".";
    }

    ~Test()
    {
        qDebug() << "Goodbye, " << m_name << ".";
    }
};

int main()
{
    QPointer<Test> pt(new Test("D.T.Software"));
    QPointer<Test> pt1(pt);
    QPointer<Test> pt2(pt);//多個QPointer指針對象可以指向同一內存空間

    pt->print();
    pt1->print();
    pt2->print();

    delete pt;//QPointer智能指針指向的對象被銷毀時,指針對象被置空

    qDebug() << "pt = " << pt;//NULL
    qDebug() << "pt1 = " << pt1;//NULL
    qDebug() << "pt2 = " << pt2;//NULL

    qDebug() << endl;

    QSharedPointer<Test> spt(new Test("Delphi Tang"));
    QSharedPointer<Test> spt1(spt);
    QSharedPointer<Test> spt2(spt);

    spt->print();
    spt1->print();
    spt2->print();

    return 0;//指針對象都被銷毀時引用計數為0,自動析構指針指向的對象
}

3、智能指針類模板

#ifndef _SMARTPOINTER_H_
#define _SMARTPOINTER_H_

template
< typename T >
class SmartPointer
{
    T* mp;
public:
    SmartPointer(T* p = NULL)
    {
        mp = p;
    }

    SmartPointer(const SmartPointer<T>& obj)
    {
        mp = obj.mp;
        const_cast<SmartPointer<T>&>(obj).mp = NULL;
    }

    SmartPointer<T>& operator = (const SmartPointer<T>& obj)
    {
        if( this != &obj )
        {
            delete mp;
            mp = obj.mp;
            const_cast<SmartPointer<T>&>(obj).mp = NULL;
        }

        return *this;
    }

    T* operator -> ()
    {
        return mp;
    }

    T& operator * ()
    {
        return *mp;
    }

    bool isNull()
    {
        return (mp == NULL);
    }

    T* get()
    {
        return mp;
    }

    ~SmartPointer()
    {
        delete mp;
    }
};

#endif

七、單例類模板

某些類在整個系統的生命周期中只能有一個對象存在,即單例模式。
要控制類的對象數目必須隱藏類的構造函數,即構造函數聲明為private。
定義一個instance標識符,初始化為NULL,當需要使用對象時查看instance的值,如果instance為NULL則創建對象並用instance標識,如果instance非空則返回instance標識的值。

#ifndef _SINGLETON_H_
#define _SINGLETON_H_

template
< typename T >
class Singleton
{
    static T* c_instance;
public:
    static T* GetInstance();
};

template
< typename T >
T* Singleton<T>::c_instance = NULL;

template
< typename T >
T* Singleton<T>::GetInstance()
{
    if( c_instance == NULL )
    {
        c_instance = new T();
    }

    return c_instance;
}

#endif

使用代碼:

#include <iostream>
#include <string>
#include "Singleton.h"

using namespace std;

class SObject
{
    friend class Singleton<SObject>;    // 當前類需要使用單例模式

    SObject(const SObject&);
    SObject& operator= (const SObject&);

    SObject()
    {
    }
public:

    void print()
    {
        cout << "this = " << this << endl;
    }
};

int main()
{
    SObject* s = Singleton<SObject>::GetInstance();
    SObject* s1 = Singleton<SObject>::GetInstance();
    SObject* s2 = Singleton<SObject>::GetInstance();

    s->print();
    s1->print();
    s2->print();

    return 0;
}

八、模板應用示例

判斷一個變量是不是指針
C++編譯器匹配的調用優先級:
A、重載函數
B、函數模板
C、變參函數
可以根據C++編譯器匹配的調用優先級,將函數模板匹配指針變量,返回true,變參函數匹配非指針變量,返回false。

template
<typename T>
bool IsPtr(T *pt)
{
    return true;
}

bool IsPtr(...)
{
    return false;
}

但是,由於變參函數是C語言的內容,無法解析C++自定義類型對象,可能造成程序崩潰。

template
<typename T>
char IsPtr(T* v) // match pointer
{
    return ‘d‘;
}

int IsPtr(...)  // match non-pointer
{
    return 0;
}

#define ISPTR(p) (sizeof(IsPtr(p)) == sizeof(char))

上述代碼中,C++編譯器在編譯時會進行函數匹配,不會進行調用,避免了參數為自定義對象時調用變參函數導致的程序崩潰。

九、class與typename關鍵字

C++語言在引入了面向對象編程思想時,使用class關鍵字定義類類型。C++語言發展過程中引入了泛型編程,直接復用class關鍵字來定義模板。但泛型編程針對的不只是類類型,直接復用class關鍵字會使代碼出現二義性。因此,C++直接引入了typename關鍵字,用於在模板定義中聲明泛指類型,明確告訴C++編譯器聲明的標識符為類型。
C++語言中允許類定義中嵌套類型,因此當自定義類類型中嵌套類型的標識符與其它類類型中定義的成員變量標識符重名時將會造成二義性。不同類中的同名標識符代表可能導致二義性,因此C++編譯器無法識別標識符的確切意義。

#include <iostream>

using namespace std;

class Test1
{
public:
    static const int NS = 1;
};

class Test2
{
public:
    struct NS
    {
        int value;
    };
};

int a = 0;

template <class T>
void func()
{
    T::NS* a;
}

int main(int argc, char *argv[])
{
    func<Test1>();
    //func<Test2>();//error
    //error: dependent-name ‘T:: NS‘ is parsed as a non-type,
    //but instantiation yields a type
    //say ‘typename T:: NS‘ if a type is meant

    return 0;
}

上述代碼中,C++編譯器不會將func函數模板中NS解析為類型,因此使用Test2作為參數時,C++編譯器會報錯。因此,為了將NS明確聲明為類型,需要使用typename關鍵字對NS標識符進行聲明。代碼如下:

#include <iostream>

using namespace std;

class Test1
{
public:
    static const int NS = 1;
};

class Test2
{
public:
    struct NS
    {
        int value;
    };
};

int a = 0;

template <class T>
void func()
{
    typename T::NS* a;
}

int main(int argc, char *argv[])
{
    //func<Test1>();//error
    func<Test2>();
    return 0;
}

上述代碼中,NS被明確聲明為類型,因此如果使用Test1作為參數,func函數模板將會報錯。

C++語言學習(十七)——模板