您好,登錄后才能下訂單哦!
在我們編寫代碼時,我們會遇見這種情況:
比如交換函數,當我們要交換的類型是int(傳的參數為int型)時,我們要編寫的swap函數的形參就應該是int,但當我們要交換的是double型時,我們還要再寫一個swap函數來滿足要求。每換一種類型就要再重載一個swap函數來滿足條件。
雖然通過這方法重載實現所有類型的交換函數,但是這種方法有幾個不好的地方,一是重載函數僅僅類型不同,導致代碼的復用率很低,只要有新類型出現,就要增加對應的函數;再者代碼的可維護性比較低,一個出錯可能所有的重載都出錯,要一個一個改。
通過上面的例子,我們想能不能告訴編譯器一個模子,編譯器可以通過不同的類型利用這樣的模子自動生成適合各種類型的函數。答案是當然可以
即泛型編程:編寫與類型無關的通用代碼,而模板是泛型編程的基礎。
下面我們來鄭重的引入模板
如何使用?
template <typename T1,typename T2...>
返回值 函數名(參數列表){ }
typename是用來定義模板參數關鍵字的,也可以用class
例如:
template<typename T>
void Swap(T &x, T &y)
//之前不同的類型,對應不同的形參列表,所以要寫多個重載函數,但現在只寫一個模板函數,編譯器就可以根據這個模板結合傳入的參數就可完成所有類型的生成對應類型的函數以供調用
T tmp = x;
x = y;
y = tmp;
}
//在這個函數中,T只能被替換為一樣的類型,若傳入參數不同,則編譯器其則生成不了匹配的函數,而編譯器又不會進行類型轉換,因而會報錯
//若想要不同類型,可定義兩個T
template<class T1, class T2> //typename可用class代替
void Swap(T1 &x, T2 &y)
{
T1 tmp = x;
x = y;
y = tmp;
}
int main()
{
int x1=0, y1=1;
double x2 = 1.0, y2 = 2.0;
Swap(x1, y1);
Swap(x2, y2);
Swap(x1, y2);
}
函數模板實例化
編譯器根據不同的參數用模板推演不同的函數稱為函數模板的實例化,模板參數實例化分為:隱式實例化和顯示實例化。
通過下面例子介紹隱式實例化和顯示實例化
template<typename T>
T Add(const T &x, const T &y)
{
return x + y;
}
int main()
{
int x1=0, y1=1;
double x2 = 1.0, y2 = 2.0;
//隱式實例化,讓編譯器根據實參推演模板參數的實際類型
Add(x1, y1); //隱式實例化
//當無法確定T是什么,由該推演成什么的時候,會報錯,因而要不然進行強轉使傳入參數類型相同,要不然進行顯式實例化
Add(x1, (int)y2); //強轉
//顯式實例化,直接告訴編譯器,由模板該推演成什么,在函數名后的<>中指定模板參數的實際類型
Add<int>(x1, (int)y2);
}
1.一個非模板函數可以和一個同名的函數模板同時存在,而且該函數模板還可以被實例化為這個非模板函數。
2.模板函數不允許自動類型轉換,但普通函數可以進行自動類型轉換。
3.對于非模板函數和同名函數模板,如果其他條件都相同,在調動時會優先調用非模板函數而不會從該模板產生出一個實例。如果模板可以產生一個具有更好匹配的函數,那么將選擇模板。
int Add(int x, int y)
{
return x + y;
}
template<typename T>
T Add(const T &x, const T &y)
{
return x + y;
}
template<typename T1, typename T2>
T1 Add(const T1 &x, const T2 &y)
{
return x + y;
}
int main()
{
//1
Add(1, 2); //調用非模板函數,無需模板實例化
Add<int>(1, 2);//調用編譯器特化的模板函數版本 --如果指定類型就必須用模板來生成相應類型
//2.
Add(1, 2.0); //此處會調用非模板函數,發生隱式類型轉換 (說明:此處還未加Add(const T1 &x, const T2 &y)函數模板)
//3.
Add(1, 2.0); //選擇函數模板若選擇非模板函數,則會發生隱式類型轉化,
//不如調用Add(const T1 &x, const T2 &y)這個實例化的函數形參列表更匹配
}
使用格式:
template <class T1,class T2....>
class A( /A為類模板名,A不是具體的類,是編譯器根據被實例化的類型生成具體類的模具)
{
...
};
類模板實例化需要在類模板名字后跟<>,然后將實例化的類型放在<>中即可,類模板名字不是真正的類,而實例化的結果才是真正的類。
例如容器vector的實現:
template <class T>
class vector{
typedef T* iterator;
public:
//構造析構
vector()
: _start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{
}
vector(int n, const T &value = 0)
:_start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{
_start = new T[n+1];
int i = n;
while (i--)
{
_start[i] = value;
}
_start[n] = '\0';
_finish = _start + n;
_endOfStorage = _finish;
}
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{
int n=0;
auto tmp = first;
while (tmp != last)
{
n++;
tmp++;
}
_start = new T[n + 1];
_finish = _start + n;
_endOfStorage = _finish;
last--;
while (n--)
{
_start[n] = *last;
last--;
}
}
vector(const vector<T>& v)
:_start(nullptr)
, _finish (nullptr)
,_endOfStorage(nullptr)
{
reserve(v.capacity());
//memcpy(_start, v._start,v.size());
for (int i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_endOfStorage = _start + v.capacity();
}
~vector()
{
delete[] _start;
_start = nullptr;
_finish = nullptr;
_endOfStorage = nullptr;
}
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
int size()const
{
return _finish - _start;
}
int capacity()const
{
return _endOfStorage - _start;
}
friend iterator find(iterator begin, iterator end, const T &value);
private:
T *_start;
T *_finish;
T *_endOfStorage;
};
// 注意:類模板中函數放在類外進行定義時,需要加模板參數列表
template <class T>
T* find(T* begin, T* end, const T &value)
{
auto it = begin;
while (it != end)
{
if (*it == value)
{
return it;
}
it++;
}
return nullptr;
}
int main(){
vector<int> v1; //實例化
}
模板的其他知識說明:
模板參數分為類型形參與非類型形參
類型形參:出現在模板參數列表中,跟在class或者typename之類的參數類型名稱。
非類型形參,就是用一個常量作為類(函數)模板的一個參數,在類(函數)模板中可將該參數當成常量來使用。
例如:
namespace Eg
{
template <class T,size_t N=10> //N在下面類中,作為常數使用
class array{
public:
size_t size()const
{
//N = 20; //會報錯:錯誤 1 error C2106: “=”: 左操作數必須為左值,可證明N是常數
return _size;
}
private:
T _array[N];
size_t _size;
};
}
void Test1(){
Eg::array<int> x;
x.size();
}
對于一些特殊的類型(比如指針類型),使用已寫的模板可能達不到我們想要的結果,得出錯誤的結果。
例如:
template <class T>
T& MAX_T(T& left, T& right)
{
return left>right?left:right;
}
void Test(){
int x = 2,y=3;
cout << MAX_T(x, y) << endl;
char *p1 = "wello";
char *p2 = "hello";
cout << MAX_T(p1, p2) << endl; //應該輸出wello 但因為比較的是地址卻輸出hello不符合邏輯,因此我們要為char*來特化一個模板提供給這種類型
}
通過上述列子我們可以通過對模板進行特化,在原來模板函數(或類)的基礎上,針對特殊類型進行特殊化的實現。比如上述例子中為char*特化一個函數,按照我們的想法針對這種類型進行特殊化處理,得到正確的結果。
模板特化又分為函數模板特化與類模板特化
函數模板的特化方式:
1.要先有一個基礎函數模板
2.關鍵字template后面接一堆空的尖括號<>
3.函數名后跟一對尖括號里面放需要特化的類型
4.函數形參表必須要和模板函數的基礎參數類型完全相同,不同的話編譯器可能會報一些錯誤
//比如針對上述char*類型特化:
template <class T>
T& MAX_T(T& left, T& right)
{
return left>right?left:right;
}
template <>
char*& MAX_T<char*>(char*& left, char*& right)
{
if (strcmp(left, right) == 1)
{
return left;
}
else{
return right;
}
}
void Test(){
char *p1 = "wello";
char *p2 = "hello";
cout << MAX_T(p1, p2) << endl;
//會調用char*& MAX_T<char*>(char*& left, char*& right)特化模板函數
}
但是!!
對于特例化的函數模板,一般都是將該函數直接給出,一是實現簡單,二是因為函數模板可能會遇到不能處理或者處理有誤的類型
類模板特化又分為全特化和偏特化
template<class T1, class T2>
class Data1
{
public:
Data1() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
template<>
class Data1<int, int>
{
public:
Data1()
{
cout << "Data1<int, int>" << endl;
}
private:
int _d1;
int _d2;
};
void Test(){
Data1<int, int> d1; //用特化模板參數類
Data1<int, double> d2;
}
2.偏特化:有兩種表現:部分特化和參數更進一步的限制
template<class T1, class T2>
class Data2
{
public:
Data2() { cout << "Data2<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
template<class T1>
class Data2<T1, int> //特化一半
{
public:
Data2()
{
cout << "Data2<T1, int>" << endl;
}
private:
T1 _d1;
int _d2;
};
void Test(){
Data2<int, int> d1; //用部分特化模板參數類
Data2<double, int> d2; //用部分特化模板參數類 因為后面的都是int,都符合這個部分特化模板
Data2<int, double> d3;
Data2<double, double> d4;
}
template<class T1, class T2>
class Data2<T1*, T2*>
{
public:
Data2()
{
cout << "Data2<T1*, T2*>" << endl;
}
private:
T1* _d1;
T2* _d2;
};
void Test(){
Data2<int*, int> d5; //使用class Data2<T1, int>
Data2<int, int*> d6; //使用template<class T1, class T2> class Data2{}
Data2<int*, int*> d7; //使用class Data2<T1*, T2*>
Data2<int*, double*> d8; //使用class Data2<T1*, T2*>
}
通過以下題目來感受類型萃取
// 寫一個通用的拷貝函數,要求:效率盡可能高
/*Way1:
//String:用該函數會報錯拷貝的是地址,析構會對同一塊空間釋放兩次,會報錯
template<class T>
void Copy(T* dst, T* src, size_t size)
{
memcpy(dst, src, sizeof(T)*size);
}
*/
//因而要區分自定義和內置類型,來調用使用不同的方法進行拷貝
/*
//Way2:增加函數判定 區分自定義和內置類型
bool IsPodType(const char* strType){
const char* arrType[] = { "char", "short", "int", "long", "long long", "float","double", "long double" };
for (size_t i = 0; i < sizeof(arrType) / sizeof(arrType[0]); ++i) //每次都要遍歷,效率太低!
{
if (0 == strcmp(strType, arrType[i]))
return true;
}
return false;
}
template<class T>
void Copy(T* dst, T* src, size_t size)
{
if (IsPodType(typeid(T).name()))
{
memcpy(dst, src, sizeof(T)*size);
}
else{
for (int i = 0; i < size; i++)
{
dst[i] = src[i];
}
}
}
*/
//Way3:萃取類型
//代表內置類型
struct TrueType{
static bool Get(){ //只有靜態才能用 :: 訪問
return true;
}
};
//代表自定義類型
struct FlaseType{
static bool Get(){
return false;
}
};
template<class T>
struct TypeTraits{
typedef FlaseType IsPodeType;
};
//對上述模板進行實例化,將內置類型都特化
template<>
struct TypeTraits<int>{
typedef TrueType IsPodeType;
};
template<>
struct TypeTraits<double>{
typedef TrueType IsPodeType;
};
/*
T為int:TypeTraits<int>已經特化過,程序運行時就會使用已經特化過的TypeTraits<int>, 該類中的IsPODType剛好為類TrueType,而TrueType中Get函數返回true,內置類型使用memcpy方式拷貝
T為string:TypeTraits<string>沒有特化過,程序運行時使用TypeTraits類模板, 該類模板中的IsPODType剛好為類FalseType,而FalseType中Get函數返回true,自定義類型使用賦值方式拷貝
*/
template<class T>
void Copy(T* dst, T* src, size_t size)
{
if (TypeTraits<T>::IsPodeType::Get())
{
memcpy(dst, src, sizeof(T)*size);
}
else{
for (int i = 0; i < size; i++)
{
dst[i] = src[i];
}
}
}
void TestCopy()
{
int array1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int array2[10];
Copy(array2, array1, 10);
String s1[3] = { "1111", "2222", "3333" };
String s2[3];
Copy(s2, s1, 3);
}
要說模板分離編譯我們就要先來談談什么是分離編譯
一個工程中有很多文件,但他們分為兩大類:頭文件和源文件
程序的運行有以下五個步驟:
預處理-->編譯-->匯編-->鏈接
頭文件會在預處理階段展開,在預處理期間程序會將頭文件中的內容復制一份到源文件。參與編譯的只有源文件,而且每個源文件都單獨進行編譯生成目標文件,最后將所有目標文件鏈接形成單一的可執行文件。如下圖:
目標文件鏈接的時候,是通過找函數地址(入口)進行調用的。
注意:強調:每個源文件都單獨進行編譯
對于模板:
實例化之前編譯器只會做一些簡單的語法測驗,不會生成處理具體類型的代碼
實例化期間編譯器用過推演形參類型來確保模板參數,通過列表中T的實際類型在生成處理具體類型的代碼
//頭文件 CompilingTest.h
template <class T>
T Add(T left,T right);
//源文件 CompilingTest.c
#include "CompilingTest.h"
template <class T>
T Add(T left, T right)
{
return left + right;
}
//源文件 main.c
#include "CompilingTest.h"
int main()
{
Add(1.0, 1.0); //會發生報錯,因為沒有實例化Add(int,int),找不到匹配的函數入口地址,因而在鏈接時會報錯
}
//頭文件 CompilingTest.h
template <class T>
T Add(T left,T right);
//源文件 CompilingTest.c
#include "CompilingTest.h"
template <class T>
T Add(T left, T right)
{
return left + right;
}
void tmp(){
Add(1, 2); //在此編譯器推演出了T->int的函數,在鏈接時找到了適合Add(1, 1);該函數的入口地址,因而才不會報錯
}
//源文件 main.c
#include "CompilingTest.h"
int main()
{
Add(1, 1); //這樣就不會發生錯誤了
//Add(1.0, 1.0); //無匹配的Add(double,,double)
}
通過上述例子可得模板不支持分離編譯
解決方法:
分離編譯詳細講解: http://blog.csdn.net/pongba/article/details/19130
優點:
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。