本篇博客全站熱榜最高排名:6
一、泛型編程思想
- 首先我們來看一下下面這三個函數(shù),如果學習過了 C++函數(shù)重載 和 C++引用 的話,就可以知道下面這三個函數(shù)是可以共存的,而且傳值會很方便
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
但是真的很方便嗎?這里只有三種類型的數(shù)據(jù)需要交換,若是我們需要增加交換的數(shù)據(jù)呢?再CV然后寫一個函數(shù)嗎?
-
這肯定是不現(xiàn)實的,所以很明顯函數(shù)重載雖然可以實現(xiàn),但是有一下幾個不好的地方:
- 重載的函數(shù)僅僅是類型不同,代碼復用率比較低,只要有新類型出現(xiàn)時,就需要用戶自己增加對應的函數(shù)
- 代碼的可維護性比較低,一個出錯可能所有的重載均出錯
- 那是否能做到這么一點,告訴編譯器一個模子,讓編譯器根據(jù)不同的類型利用該模子來生成代碼
這樣,我們先通過一個案例來做引入
- 才高八斗的魏武帝之子【曹植】曾經(jīng)寫過一首《洛神賦》,被后人譽為“千古第一絕世神仙詩”,因此在那個時候被很多文人所抄錄、傳唱,其共1089個字,想要一字不落地抄錄下來還是需要時間,那古代還有一些更長的詩賦,例如:《離騷》,足足有2500多字,若也是一個一個地抄錄下來的話,那會變得極為麻煩
- 于是呢古人又發(fā)明了一種東西叫做【活字印刷術】,有了此技術之后,人們不再需要每次去手抄一些詩賦書籍,只需要作一份模版字體,然后在其上刷上水,刷上墨,然后拿紙這么一印,就可以得到一份工整的印刷體了
?? 所以,總結上面的這么一個技術,C++的祖師爺呢就想到了【模版】這個東西,告訴編譯器一個模子,然后其余的工作交給它來完成,根據(jù)不同的需求生成不同的代碼
這就是??泛型編程:編寫與類型無關的通用代碼,是代碼復用的一種手段。模板是泛型編程的基礎
二、函數(shù)模版
知曉了模版的基本概念后,首先我們要來看的就是【函數(shù)模版】
1、函數(shù)模板概念
函數(shù)模板代表了一個函數(shù)家族,該函數(shù)模板與類型無關,在使用時被參數(shù)化,根據(jù)實參類型產(chǎn)生函數(shù)的特定類型版本
通過函數(shù)模板,可以編寫一種通用的函數(shù)定義,使其能夠適用于多種數(shù)據(jù)類型,從而提高代碼的復用性和靈活性。
2、函數(shù)模板格式
- 然后正式來聲明一個函數(shù)模版,這里我們要學一個新的關鍵字叫
template
,接下去加一個<>
尖括號,內部就是模版的參數(shù)了,可以使用typename
或者class
來進行類型的聲明(不可以用struct
),若是只有一個類型的話我們一般會叫做【T】,不過這個沒有強制要求
template<typename T1, typename T2,......,typename Tn>
- 接下去就可以使用上面所聲明的模版參數(shù)了,即上面的這個【T】,它和我們普通的函數(shù)參數(shù)可不一樣,后者是定義的是對象,而前者定義的是類型
返回值類型 函數(shù)名(參數(shù)列表){}
馬上,我們就來為上面的swap()
函數(shù)寫一個通用的函數(shù)模版把
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
- 然后來用一用這個函數(shù)模版,分別傳入不同數(shù)據(jù)類型的參數(shù),通過結果的觀察可以發(fā)現(xiàn)這個函數(shù)模版可以根據(jù)不同的類型去做一個自動推導,繼而去起到一個交換的功能
- 我們可以通過調試來進一步觀察,發(fā)現(xiàn)無論是對于【整型】還是【浮點型】,都會去走這個
Swap()
函數(shù),函數(shù)模版都可以進行自動的識別
?? 那我現(xiàn)在想問一個問題,請問它們調用的真的是同一個函數(shù)嗎?
- 相信很多同學都說是的,上面不是演示過了嗎?但是看完下面這一小節(jié)你就不會這么說了??
3、函數(shù)模板的原理
接下去我們來說說這個函數(shù)模版的原理,帶你理清編譯器內部究竟做了什么事情
- 對于函數(shù)模板而言其實是一個【藍圖】,它本身并不是函數(shù),是編譯器用使用方式產(chǎn)生特定具體類型函數(shù)的模具。所以其實模板就是將本來應該我們做的重復的事情交給了編譯器,讓我們來看看編譯器做了什么
- 可以發(fā)現(xiàn),在進行匯編代碼查看的時候,被調用的函數(shù)模版生成了兩個不同的函數(shù),它們有著不同的函數(shù)地址,因此可以回答上一小節(jié)所提出的問題了,兩次所調用的函數(shù)是不一樣的,是根據(jù)函數(shù)模版所生成的
還是不太懂的老鐵可以看看下面這張圖,就能明白了
- 在編譯器編譯階段,對于模板函數(shù)的使用,編譯器需要根據(jù)傳入的實參類型來推演生成對應類型的函數(shù)以供調用。比如:當用
int
類型使用函數(shù)模板時,編譯器通過對實參類型的推演,將T確定為int
類型,然后產(chǎn)生一份專門處理int
類型的代碼,對于浮點類型、字符型也是如此
?? 那我現(xiàn)在還想問,如果我使用的是兩個日期類Date
的對象呢,能不能對它們進行交換
- 答案是可以的,int、double、char這些都是內置類型,Date呢是自定義類型,很明顯它們都是類型,我們所定義的模版參數(shù)也是類型,那為何不可去做一個接受呢?從運行結果可以來看,確實是可以發(fā)現(xiàn)它們也發(fā)生了一個交換
?? 那如果是指針呢?也會去調用嗎?
- 如何你學的扎實得話,立馬就能反應過來了,對于指針而言也是內置類型,那既然是類型的話為何不能調用呢?
既然談到了這個【Swap】交換函數(shù),我們就順便來說說庫里的這個【swap】
- 仔細觀察下圖可以發(fā)現(xiàn)我將Swap前面大寫S改成了小寫s,它就是std標準庫的里面的一個函數(shù),也包含在STL的基本算法中
- 進到文檔里面進行查看我們可以發(fā)現(xiàn)這個函數(shù)確實已經(jīng)實現(xiàn)了【函數(shù)模版】,因此可以接收任何類型的數(shù)據(jù),所以在學習了函數(shù)模版后我們就不需要自己再去寫
swap()
函數(shù)了,直接用庫里的即可
4、函數(shù)模板的實例化
上面我們所定義的都是單個模版參數(shù),那多個模版參數(shù)是否可以定義呢?
- 這個當然是可以的,我們立馬來試試看吧
template<typename T1, typename T2>
T1 Func(const T1& x, const T2& y)
{
cout << x << " " << y << endl;
}
- 通過運行可以發(fā)現(xiàn),是完全可以做到的,去定義多個模版參數(shù),這個特性本文就不做距離了,等我們學習了一些STL之后再【模版進階】一文中詳解
然后我們來講講【函數(shù)模板的實例化】
?? 用不同類型的參數(shù)使用函數(shù)模板時,稱為函數(shù)模板的實例化。模板參數(shù)實例化分為:隱式實例化和顯式實例化
- 隱式實例化:讓編譯器根據(jù)實參推演模板參數(shù)的實際類型
- 知道模版可以定義多個參數(shù)之后,其返回值也可以是模版參數(shù)
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
- 下面這種就是【隱式實例化】,讓編譯器根據(jù)實參自動去推導模板參數(shù)的實際類型,然后返回返回不同類型的數(shù)據(jù)
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
// 根據(jù)實參傳遞的類型,推演T的類型
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
}
- 但是呢像下面這種就不可以了,因為
a1
是【int】類型,d1
是【double】類型,在編譯期間,當編譯器看到該實例化時,需要推演其實參類型 通過實參a1將T推演為int類型,通過實參d1將T推演為double類型,但模板參數(shù)列表中只有一個T, 編譯器無法確定此處到底該將T確定為int 或者 double類型而報錯。此時在函數(shù)調用完后進行返回時,編譯器也識別不出是哪個類型了,它們兩個就像在打架一樣,很難一絕高下
cout << Add(a1, d2) << endl
好比你做錯事了,你爸爸讓你罰站,不讓你吃飯,此時呢你媽媽回來了,讓你趕緊過來吃飯,那此時你該聽誰的呢?
- 那此時呢就需要爸爸媽媽會房間商量一下了,到底是以誰說的話為主呢
那當他們商量好了之后,就會有下面這兩種情況
- 這一種改法便是聽爸爸的,后者d1強轉為
int
類型然后再傳遞進去,此時就不會出現(xiàn)類型沖突的問題了
cout << Add(a1, (int)d2) << endl;
- 這種改法便是聽媽媽的,前者a1強轉為
double
類型然后再傳遞進去,也不會出現(xiàn)類型沖突的問題
cout << Add((double)a1, d2) << endl;
- 顯式實例化:在函數(shù)名后的<>中指定模板參數(shù)的實際類型
- 除了上面這種手動強轉的措施,還有一種辦法就是我們自己進行【顯式實例化】,如何你還有印象的話,可以翻上看看匯編,其實編譯器在底層就是轉換為了這種形式
// 顯式實例化,用指定類型實例化
cout << Add<int>(a1, d2) << endl;
cout << Add<double>(a1, d2) << endl;
雖然上面我們介紹了兩種處理方式,但是對于某些場景來說,卻只能進行【顯式實例化】
- 例如我在下面寫了一個函數(shù)模版,形參部分并不是模版參數(shù),而是普通的自定義類型,之后返回值才是,那此時我們就無法通過傳參來指定這個【T】的類型,只能有外部在調用這個模版的時候顯示指定
template<class T>
T* Alloc(int n)
{
return new T[n];
}
- 例如下面的這些,我們想開什么數(shù)據(jù)類型的空間,只需要顯示指定類型即可
// 有些函數(shù)無法自動推,只能顯示實例化
double* p1 = Alloc<double>(10);
float* p1 = Alloc<float>(20);
int* p2 = Alloc<int>(30);
5、模板參數(shù)的匹配原則
① 一個非模板函數(shù)可以和一個同名的函數(shù)模板同時存在,而且該函數(shù)模板還可以被實例化為這個非模板函數(shù)
- 可以看到,我在下面寫了一個專門處理int的加法函數(shù),為普通的函數(shù),又寫了一個函數(shù)模版,它們是可以進行共存的,在進行普通傳參的時候,就會去調用這個普通的Add函數(shù);若是顯式指明了類型的話,就會去調用這個函數(shù)模版讓編譯器生成對應的函數(shù)
// 專門處理int的加法函數(shù)
int Add(int left, int right)
{
return left + right;
}
// 通用加法函數(shù)
template<class T>
T Add(T left, T right)
{
return left + right;
}
int main(void)
{
Add(1, 2);
Add<int>(1, 2);
return 0;
}
② 對于非模板函數(shù)和同名函數(shù)模板,如果其他條件都相同,在調動時會優(yōu)先調用非模板函數(shù)而不會從該模板產(chǎn)生出一個實例。如果模板可以產(chǎn)生一個具有更好匹配的函數(shù), 那么將選擇模板
- 也是和上面類似的代碼,不過對于函數(shù)模版這一塊我使用到了兩個模版參數(shù),就是為了匹配多種數(shù)據(jù)類型
// 專門處理int的加法函數(shù)
int Add(int left, int right)
{
return left + right;
}
// 通用加法函數(shù)
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
int main(void)
{
Add(1, 2);
Add(1, 2.2);
return 0;
}
- 觀察調試結果我們可以發(fā)現(xiàn)
Add(1, 2)
優(yōu)先去調了普通的加法函數(shù),因為傳遞進去的是兩個【int】類型的參數(shù),完全吻合;但是呢對于第二個Add(1, 2.2)
來說,卻去調用了函數(shù)模版,因為第二個參數(shù)是【double】類型,普通的函數(shù)它也接不住呀,此時模版參數(shù)就可以根據(jù)這個類型來進行一個自動推導
③ 模板函數(shù)不允許自動類型轉換,但普通函數(shù)可以進行自動類型轉換
- 首先對于普通函數(shù)而言很好理解,看到
print
函數(shù)的形參所給的類型為【int】,但是在外界傳入了一個【double】類型的數(shù)值,如果你學習過 隱式類型轉換 的話,就可以知道這個浮點數(shù)傳入的話會發(fā)生一個轉換,這就叫做【自動類型轉換】
// 普通函數(shù),允許自動類型轉換
void print(int value) {
std::cout << "Integer: " << value << std::endl;
}
int main() {
print(3);
print(3.14);
return 0;
}
- 但是呢,對于模版函數(shù)來說是無法進行自動類型轉換的,例如下面這個,我為這個函數(shù)模版定義了一個模版參數(shù),但是在外界進行傳遞的時候卻傳遞進來兩種數(shù)據(jù)類型,為【int】或【double】,那么一個模版參數(shù)T就使得編譯器無法去進行自動推導
template <class T>
void print(T a, T b) {
cout << a << " " << b << endl;
}
int a = 1;
double b = 1.11;
print(a, b);
- 這個其實我們在上面也講到過,再來回顧一下,改進的方法有兩種,一個是【強制類型轉換】,還記得罰站的事情嗎;另一個則是【顯式實例化】,還記得我們看的匯編嗎
// 強制類型轉換
print(a, (int)b);
print((double)a, b);
// 顯式實例化
print<int>(a, b);
print<double>(a, b);
- 其實還有一種改進的方法,那就是增加模版參數(shù),因為一個模版參數(shù)接收兩種類型是無法進行自動推導的,此時若是有兩個模版參數(shù)的話就可以接收兩種類型了,不會出現(xiàn)錯誤
三、類模版
講完了函數(shù)模版后,我們再來說說類模版,也就是對一個類來說,也是可以定義為一個模版的
1、類模板的定義格式
- 首先來看到的就是其定義格式,函數(shù)模版加在函數(shù)上,那對于類模版的話就是加在類上
template<class T1, class T2, ..., class Tn>
class 類模板名
{
// 類內成員定義
};
我們以下面這個Stack類為例來進行講解
- 如果你學習了模版的相關知識后,一定會覺得這個類的限制性太大了,只能初始化一個具有整型數(shù)據(jù)的棧,如果此時我想要放一些浮點型的數(shù)據(jù)進來的話也做不到
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申請空間失敗!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
?? 如果沒有模版技術的話你會如何去解決這個問題呢?很簡單那就是定義多個類
-
這是我們同學最擅長的事,CV一下兩個棧就有了,
StackInt
存放整型數(shù)據(jù),StackDouble
存放浮點型數(shù)據(jù)
class StackInt
class StackDouble
但是本文我們重點要講解的就是【模版技術】,技術界有一句話說得好 “不要重復造輪子”
- 下面就是使用模版去定義的一個類,簡稱【模板類】,不限制死數(shù)據(jù)類型,將所有的DataType都改為【T】
template<class T>
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申請空間失敗!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(T data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
T* _array;
int _capacity;
int _size;
};
- 但是呢就上面這樣其實并不是最規(guī)范的寫法,還記得我們在學習C++類和對象講到過一個類要聲明和定義分離,那對于模板類也同樣適用,我們馬上來看看
template<class T> // 類模版
class Stack
{
public:
Stack(size_t capacity = 3);
void Push(T data);
~Stack();
private:
T* _array;
int _capacity;
int _size;
};
- 不過呢可以看到直接像我們之前那樣去進行類外定義似乎行不通,說
缺少類模版“Stack”的參數(shù)列表
,因為這個成員函數(shù)內部也使用到了模版參數(shù)T,那么這個函數(shù)也要變?yōu)楹瘮?shù)模版才行
- 但是在加上這個模版參數(shù)后,似乎還是有問題,
::
這個操作符我們在C++命名空間中有提到過,叫做【域作用限定符】,是我們使用命名空間去訪問特定成員變量或成員函數(shù)時使用的,對于類來說它一定要是一個類名 - 這里要強調一點的是對于普通類來說類名和類型是一樣的, 像構造函數(shù),它的函數(shù)名就是類名;可是對于模板類來說是不一樣,類名和類型不一樣,這里
Stack
只是這個模版類的類名罷了,但我們現(xiàn)在需要的是類型,此處就想到了我們在上面所學的【顯式實例化】,這個模板類的類型即為Stack<T>
- 以下即是對這個模版類中的成員函數(shù)在類外實現(xiàn)所需要變化成的模版函數(shù)
template<class T>
Stack<T>::Stack(size_t capacity)
{
_array = new T(capacity);
_capacity = capacity;
_size = 0;
}
- 那對于其他函數(shù)也是一致,均需要將它們定義為模版函數(shù),此時我們可以意識到一點的是對于模版函數(shù)來說,其模版參數(shù)的作用域就在這個函數(shù)內部,出了這個函數(shù)就無法使用了, 所以可以看到每個函數(shù)前面都需要其對應的模版參數(shù);而且對于模版類來說也是同理,只在這個類內起作用,即到收括號
};
為止,我們知道對于一個類來說也算是一個獨立的空間,成員函數(shù)是不包含在類內的,所以其在類外進行定義的時候就需要再重新定義模版參數(shù)
template<class T> // 每個函數(shù)或類前都要加上其對應的模版參數(shù)
void Stack<T>::Push(T data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
template<class T>
Stack<T>::~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
2、類模板的實例化
清楚了什么是類模版之后,我們就將上面的這個Stack類模版給實例化出來吧
?? 類模板實例化與函數(shù)模板實例化不同,類模板實例化需要在類模板名字后跟<>
,然后將實例化的類型放在<>
中即可,類模板名字不是真正的類,而實例化的結果才是真正的類
- 可以看到因為我們將這個類定義為了類模版,此時便可以去初始化不同數(shù)據(jù)類型的棧了,上面說到過
Stack
是類名,但是像Stack<int>
、Stack<double>
這些都是它的類型
int main(void)
{
Stack<int> s1; // int
Stack<double> s2; // double
Stack<char> s3; // char
return 0;
}
四、總結與提煉
最后我們來總結一下本文所學習的內容??
- 首先我們了解了什么是泛型編程的思想,通過曹植的《洛神賦》到【活字印刷術】,我們體會到了有一個通用模版的重要性,于是就引申出了C++中的模版這一個概念,對于模版呢,其分為 函數(shù)模版 和 類模版
- 首先呢我們介紹了什么是【函數(shù)模版】,新學習了一個關鍵字叫做
template
,用它再配合模版參數(shù)就可以去定義出一個函數(shù)模版,有了它,我們在寫一些相同類型函數(shù)的時候就無需去進行重復的CV操作了,在通過匯編觀察函數(shù)模版的原理后,清楚了我們只需要傳入不同的類型,此時模版參數(shù)就會去進行一個自動類型推導,從而產(chǎn)生不同的函數(shù)。函數(shù)模版定義好后還要對其實例化才能繼續(xù)使用,但此時要注意的一點是如果傳遞進去的類型個數(shù)與模版參數(shù)的個數(shù)不匹配的話,其就無法完成自動類型推導,因為這會產(chǎn)生一個歧義。所以想要真正學好模版,這點是一定要搞清楚的?。。?/li> - 接下去呢我們又學習了【類模版】,沒想到吧,類也可以變成一個模版,以
Stack
類為例,對于類模版而言,其類名和類型與普通類是不一樣的,這點要注意了,尤其體現(xiàn)在類的成員函數(shù)放在類外進行定義的時候,也要將其定義為函數(shù)模版,函數(shù)名前面指明其類型,這才不會出問題。有了類模版之后,我們去顯式實例化不同的數(shù)據(jù)類型后也可以讓模版參數(shù)去做一個自動類型推導從而得到不同數(shù)據(jù)類型的棧 - 總而言之,模版是C++的一個亮點所在,也是學習STL的基礎,望讀者扎實掌握??
以上就是本文要介紹的所有內容,感謝您的閱讀??文章來源:http://www.zghlxwxcb.cn/news/detail-541494.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-541494.html
到了這里,關于一同感受C++模版的所帶來的魅力的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!