目錄
一、什么是泛型編程?
二、函數模板
2.1函數模板的概念
2.2函數模板格式
2.3函數模板的原理
?2.5函數模板的實例化
2.6模板參數的匹配原則
三、類模板
3.1類模板的定義格式
3.2 類模板的實例化
四、非類型模板參數
五、模板的特化
5.1模板特化的概念:
5.2函數模板特化
5.3類模板的特化
5.4模板特化的應用場景
六、模板分離編譯?
6.1什么是模板的分離編譯
6.2模板的分離編譯
6.3解決方案
七、模板總結
一、什么是泛型編程?
在C語言中,當我們需要使用函數交換兩個int類型的變量的值時,這時肯定可以想到直接定義一個int類型的函數就好了,但是此時又想交換double類型的變量,又會想到定義一個交換double類型的函數,在C++中有人會想到函數重載,如:
#include<iostream>
using namespace std;
void Swapi(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swapd(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swapc(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1, b = 2;
double c = 3.000, d = 4.000;
char e = 'A', f = 'B';
Swapi(a, b);
Swapd(c, d);
Swapc(e, f);
cout << "a= " << a << ',' << "b= " << b << endl;
cout << "c= " << c << ',' << "d= " << d << endl;
cout << "e= " << e << ',' << "f= " << f << endl;
return 0;
}
雖然這樣可以實現,但是代碼的復用性太低了,每實現一種類型就得手動實現一個函數。
使用函數重載雖然可以實現,但是有以下幾個不好的地方:
- ?重載的函數僅僅是類型不同,代碼復用率比較低,只要有新類型出現時,就需要用戶自己增? 加對應的函數
- 代碼的可維護性比較低,一個出錯可能所有的重載均出錯
這樣是不是很麻煩呢?代碼量不僅變多了,代碼的復用性也比較低。所以,此時我們就可以使用函數模板來完成函數的編寫,如:
#include<iostream>
using namespace std;
//函數模板
template <class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1, b = 2;
double c = 3.000, d = 4.000;
char e = 'A', f = 'B';
Swap(a, b);
Swap(c, d);
Swap(e, f);
cout << "a= " << a << ',' << "b= " << b << endl;
cout << "c= " << c << ',' << "d= " << d << endl;
cout << "e= " << e << ',' << "f= " << f << endl;
return 0;
}
模板就如活字印刷術一樣,給我們一個模樣板子就可以實現出我們想要的字體
如果在C++中,也能夠存在這樣一個模具,通過給這個模具中填充不同材料(類型),來獲得不同材料的文字(即生成具體類型的代碼),那將會節(jié)省許多頭發(fā)。巧的是前人早已將樹栽好,我們只需在此乘涼。
泛型編程:編寫與類型無關的通用代碼,是代碼復用的一種手段。模板是泛型編程的基礎。
二、函數模板
2.1函數模板的概念
函數模板代表了一個函數家族,該函數模板與類型無關,在使用時被參數化,根據實參類型產生函數的特定類型版本。
2.2函數模板格式
template<typename T1, typename T2,......,typename Tn>
返回值類型 函數名(參數列表) { 函數體 }
template<typename T>//其中typename也可以改為class
void Swap( T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
注意:typename是用來定義模板參數關鍵字,也可以使用class(切記:不能使用struct代替class)
2.3函數模板的原理
函數模板是一個藍圖,它本身并不是函數,是編譯器用使用方式產生特定具體類型函數的模具。所以其實模板就是將本來應該我們做的重復的事情交給了編譯器
?在編譯器編譯階段,對于模板函數的使用,編譯器需要根據傳入的實參類型來推演生成對應類型的函數以供調用。比如:當用double類型使用函數模板時,編譯器通過對實參類型的推演,將T確定為double類型,然后產生一份專門處理double類型的代碼,對于字符類型也是如此。
?2.5函數模板的實例化
用不同類型的參數使用函數模板時,稱為函數模板的實例化。模板參數實例化分為:隱式實例化和顯式實例化
①隱式實例化:讓編譯器根據實參推演模板參數的實際類型
#include<iostream>
using namespace std;
//函數模板
template <class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1, b = 2;
char c = 'A', d = 'B';
Swap(a, b);
Swap(c, d);
}
隱式實例化時會發(fā)生的沖突
#include<iostream>
using namespace std;
//函數模板
template <class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1;
double b = 3.000;
Swap(a, b);//編譯階段報錯
}
上面程序中的Swap(a, b);語句會報錯,因為在編譯期間,當編譯器看到該實例化時,需要推演其實參類型通過實參a將T推演為int,通過實參b將T推演為double類型,但模板參數列表中只有一個T,編譯器無法確定此處到底該將T確定為int 或者 double類型而報錯
注意:在模板中,編譯器一般不會進行類型轉換操作,因為一旦轉化出問題,編譯器就需要背黑鍋
此時就有兩種解決方案,
(1)先強制類型轉換在傳參
Swap(a, (int) b);
//或
//Swap( (double) a, b);
(2)顯示實例化
②顯示實例化:在函數名后的<>中指定模板參數的實際類型
#include<iostream>
using namespace std;
//函數模板
template <class T>
void Swap(T a, T b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1;
double b = 3.000;
//顯示實例化
Swap<int>(a, b);//表示T此時為int類型
//Swap<double>(a, b); 表示T此時為double類型
}
如果類型不匹配,編譯器會嘗試進行隱式類型轉換,如果無法轉換成功編譯器將會報錯。
2.6模板參數的匹配原則
①?一個非模板函數可以和一個同名的函數模板同時存在,而且該函數模板還可以被實例化為這個非模板函數
#include<iostream>
using namespace std;
//專門交換int類型的函數
void Swap(int a,int b)
{
int tmp = a;
a = b;
b = tmp;
}
//通用交換模板函數
template <class T>
void Swap(T a, T b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1, b = 2;
char c = 'A', d = 'B';
Swap(a, b);//與非模板函數匹配,編譯器不使用模板實例化int類型函數
Swap(c, d);//與模板函數匹配,編譯器使用模板實例化double類型函數
//注意下面這行會發(fā)生隱式類型轉換
Swap<int>(c,d);//與模板函數匹配,編譯器使用模板實例化int類型函數
}
注意:最后一行交換會發(fā)生隱式類型轉換,產生一個臨時變量,這時不能傳引用,因為臨時變量具有常性
②?對于非模板函數和同名函數模板,如果其他條件都相同,在調動時會優(yōu)先調用非模板函數而不會從該模板產生出一個實例。如果模板可以產生一個具有更好匹配的函數, 那么將選擇模板
#include<iostream>
using namespace std;
//專門交換int類型的函數
void Swap(int a,int b)
{
int tmp = a;
a = b;
b = tmp;
}
//通用交換模板函數
template <class T>
void Swap(T1 a, T2 b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1, b = 2;
double d=3.000;
Swap(a, b);// 與非函數模板類型完全匹配,不需要函數模板實例化
Swap(a, d);// 模板函數可以生成更加匹配的版本,編譯器根據實參生成更加匹配的Swap函數
}
③模板函數不允許自動類型轉換,但普通函數可以進行自動類型轉換
三、類模板
3.1類模板的定義格式
template<class T1, class T2, ..., class Tn>
class 類模板名
{
// 類內成員定義
};
#include<iostream>
using namespace std;
//函數模板
template <class T>
class A
{
public:
A(T a,T b)
:_a(a)
,_b(b)
{}
void print()
{
cout << _a << ',' << _b << endl;
}
private:
T _a;
T _b;
};
int main()
{
A<int> a(2,4);//類模板必須顯示實例化
a.print();
}
注意:類模板中函數放在類外進行定義時,需要加模板參數列表
如:
#include<iostream>
using namespace std;
//函數模板
template <class T>
class A
{
public:
A(T a,T b)
:_a(a)
,_b(b)
{}
void print();
private:
T _a;
T _b;
};
注意:類模板中函數放在類外進行定義時,需要加模板參數列表
template <class T>
void print()
{
cout << _a << ',' << _b << endl;
}
int main()
{
A<int> a(2,4);
a.print();
}
因為模板參數也有作用域范圍,就如上面的A類,出了A類以后,需要定義print函數時就需要再寫一次模板參數,因為類模板的參數的作用域只作用域該類
3.2 類模板的實例化
類模板實例化與函數模板實例化不同,類模板實例化需要在類模板名字后跟<>,然后將實例化的類型放在<>中即可,類模板名字不是真正的類,而實例化的結果才是真正的類
?
//A表示類名,A<int>才表示類型
A<int> a1;
A<double> a2;
注意:類的實例化必須顯示實例化
四、非類型模板參數
模板參數分為類型形參與非類型形參。
類型形參:出現在模板參數列表中,跟在class或者typename之類的參數類型名稱。
非類型形參:就是用一個常量作為類(函數)模板的一個參數,在類(函數)模板中可將該參數當成常量來使用。
#include<iostream>
using namespace std;
namespace lx
{
// 定義一個模板類型的靜態(tài)數組
template<class T, size_t N = 10>
class array
{
public:
T& operator[](size_t index)
{
return _array[index];
}
const T& operator[](size_t index)const
{
return _array[index];
}
size_t size()const
{
return _size;
}
bool empty()const
{
return 0 == _size;
}
private:
T _array[N];
size_t _size;
};
}
注意:
1. 浮點數、類對象以及字符串是不允許作為非類型模板參數的。
2. 非類型的模板參數必須在編譯期就能確認結果。
五、模板的特化
5.1模板特化的概念:
通常情況下,使用模板可以實現一些與類型無關的代碼,但對于一些特殊的類型可能會得到一些錯誤的結果,需要特殊處理,比如:實現了一個專門用來進行小于比較的函數模板
#include<iostream>
using namespace std;
// 函數模板 -- 參數匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比較,結果正確
double d1 = 4.0, d2 = 5.0;
cout << Less(d1, d2) << endl; // 可以比較,結果正確
double* p1 = &d1;
double* p2 = &d2;
cout << Less(p1, p2) << endl; // 可以比較,結果錯誤
return 0;
}
?
?可以發(fā)現最后結果和我們的預期的結果不符,我們的本意是為了比較d1、d2的值的大小,結果它比較的卻是d1、d2的地址,與我們的本意相違了。那怎么才能讓我們達到預期的效果呢?
此時,就需要對模板進行特化。即:在原模板類的基礎上,針對特殊類型所進行特殊化的實現方式。模板特化中分為函數模板特化與類模板特化。
5.2函數模板特化
函數模板的特化步驟:
- ?必須要先有一個基礎的函數模板
- ?關鍵字template后面接一對空的尖括號<>
- ?函數名后跟一對尖括號,尖括號中指定需要特化的類型
- ?函數形參表: 必須要和模板函數的基礎參數類型完全相同,如果不同編譯器可能會報一些奇怪的錯誤。
?
#include<iostream>
using namespace std;
// 函數模板 -- 參數匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 對Less函數模板進行特化
template<>
bool Less<double*>(double* left, double* right)
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl;
double d1 = 4.0;
double d2 = 5.0;
cout << Less(d1, d2) << endl;
double* p1 = &d1;
double* p2 = &d2;
cout << Less(p1, p2) << endl; // 調用特化之后的版本,而不走模板生成了
return 0;
}
?可以發(fā)現此時與預期結果相符了
注意:一般情況下如果函數模板遇到不能處理或者處理有誤的類型,為了實現簡單通常都是將該函數直接給出。
bool Less(double* left, double* right)
{
return *left < *right;
}
該種實現簡單明了,代碼的可讀性高,容易書寫,因為對于一些參數類型復雜的函數模板,特化時特別給出,因此函數模板不建議特化。
5.3類模板的特化
類模板的特化又分為全特化和偏特化
(1)全特化:將模板參數列表中所有的參數都確定化
#include<iostream>
using namespace std;
template<class T1, class T2>
class Date
{
public:
Date() { cout << "Date<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//類模板全特化
template<>
class Date<int, char>
{
public:
Date() { cout << "Date<int, char>" << endl; }
private:
int _d1;
char _d2;
};
void TestVector()
{
Date<int, int> d1;
Date<int, char> d2;
}
注意:類模板特化模板參數的書寫位置
(2)偏特化:任何針對模版參數進一步進行條件限制設計的特化版本
①部分特化
將模板參數類表中的一部分參數特化
// 將第二個參數特化為int
template <class T1>
class Date<T1, int>
{
public:
Date() {cout<<"Date<T1, int>" <<endl;}
private:
T1 _d1;
int _d2;
};
②參數更進一步的限制
偏特化并不僅僅是指特化部分參數,而是針對模板參數更進一步的條件限制所設計出來的一個特化版本,如:
特化為指針類型
//兩個參數偏特化為指針類型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
private:
T1 _d1;
T2 _d2;
};
特化為引用類型?
//兩個參數偏特化為引用類型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
調用方式
Data<double , int> d1; // 調用特化的int版本
Data<int , double> d2; // 調用基礎的模板
Data<int *, int*> d3; // 調用特化的指針版本
Data<int&, int&> d4(1, 2); // 調用特化的指針版本
5.4模板特化的應用場景
當我們需要定義一個仿函數的時候,一般都是使用類模板來定義的,如:
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
在C++標準庫中的排序sort就是有一個傳該類型的模板參數進行排序的
?
#include<iostream>
using namespace std;
#include<vector>
#include <algorithm>
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
int main()
{
double d1 = 3.0;
double d2 = 4.0;
double d3 = 2.0;
vector<double> v1;
v1.push_back(d1);
v1.push_back(d2);
v1.push_back(d3);
// 可以直接排序,結果是升序
sort(v1.begin(), v1.end(), Less<double>());
auto it = v1.begin();
while (it != v1.end())
{
printf("%lf ", *it);
it++;
}
return 0;
}
可以發(fā)現完成了排序,但當我們傳的地址進去呢?
#include<iostream>
using namespace std;
#include<vector>
#include <algorithm>
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
int main()
{
double d1 = 3.0;
double d2 = 4.0;
double d3 = 2.0;
vector<double*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 可以直接排序,結果錯誤,不是升序,而v2中放的地址是升序
// 此處需要在排序過程中,讓sort比較v2中存放地址指向的日期對象
// 但是走Less模板,sort在排序時實際比較的是v2中指針的地址,因此無法達到預期
sort(v2.begin(), v2.end(), Less<double*>());
auto it1 = v2.begin();
while (it1 != v2.end())
{
cout << *(*it1) << ' ';
it1++;
}
return 0;
}
?
?可以發(fā)現它的結果并不是升序,與我們想要的結果不符,為什么會這樣呢?
原因:它調用sort函數排序時,比較的是我們傳進去的地址,而不是傳進去的地址存的值,所以它給地址排了個序。
這樣顯然不是我們想要的,所以這時就可以使用模板的特化來處理了
#include<iostream>
using namespace std;
#include<vector>
#include <algorithm>
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
// 對Less類模板按照指針方式特化
template<>
struct Less<double*>
{
bool operator()(double* x, double* y) const
{
return *x < *y;
}
};
int main()
{
double d1 = 3.0;
double d2 = 4.0;
double d3 = 2.0;
vector<double*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 可以直接排序,結果錯誤,不是升序,而v2中放的地址是升序
// 此處需要在排序過程中,讓sort比較v2中存放地址指向的日期對象
// 但是走Less模板,sort在排序時實際比較的是v2中指針的地址,因此無法達到預期
sort(v2.begin(), v2.end(), Less<double*>());
auto it1 = v2.begin();
while (it1 != v2.end())
{
cout << *(*it1) << ' ';
it1++;
}
return 0;
}
?
這就得到了我們預期的結果,所以模板的特化還是至關重要的。
六、模板分離編譯?
6.1什么是模板的分離編譯
一個程序(項目)由若干個源文件共同實現,而每個源文件單獨編譯生成目標文件,最后將所有目標文件鏈接起來形成單一的可執(zhí)行文件的過程稱為分離編譯模式。
6.2模板的分離編譯
?模板的聲明與定義分離開,在頭文件中進行聲明,源文件中完成定義:
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
這樣會發(fā)生報錯,原因如下:?
?
如果模板聲明和定義在不同的文件中,那么在鏈接的階段就會找不到函數的地址而發(fā)生報錯,所以建議?模板的聲明和定義在同一個文件中
6.3解決方案
①將聲明和定義放到一個文件 "xxx.hpp" 里面或者xxx.h其實也是可以的。推薦使用這種。
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.h
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
②模板定義的位置顯式實例化。這種方法不實用,不推薦使用。
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
//函數顯示實例化,為int類型
template
int Add<int>(const int& left,const int& right);
//類顯示實例化,為int類型
//template
//vector<int>;
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
這種方法是直接在函數或類定義的文件中實例化,等調用的時候就可以找到該地址了,因為鏈接時,它們已經實例化了,有了地址進入了符號表,在符號表中就能找到它們的地址,從而不會報錯。但這種方法的弊端很明顯,每次換一個類型就得自己顯示實例化一次,這樣就顯得有些呆板和麻煩了,所以不推薦這種方法。
七、模板總結
1.優(yōu)點
- 模板復用了代碼,節(jié)省資源,更快的迭代開發(fā),C++的標準模板庫(STL)因此而產生
- ?增強了代碼的靈活性
2.缺點文章來源:http://www.zghlxwxcb.cn/news/detail-467081.html
- 模板會導致代碼膨脹問題,也會導致編譯時間變長
- 出現模板編譯錯誤時,錯誤信息非常凌亂,不易定位錯誤
模板的知識就分享到這了,有遺漏或錯誤還望指出,如果對你有所幫助的話給波關注唄,感謝支持,886!文章來源地址http://www.zghlxwxcb.cn/news/detail-467081.html
到了這里,關于C++泛型編程之模板的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!