文章目錄
- 寫在前面
- 1. 函數模板
- 1.1 函數模板的概念
- 1.2 函數模板的原理
- 1.3 函數模板的實例化
- 1.4 函數模板的實例化模板參數的匹配原則
- 2. 類模板
- 3. 非類型模板參數
- 4. 模板的特化
- 4.1 概念
- 4.2 函數模板特化
- 4.3 類模板特化
- 5. 模板分離編譯
- 6. 總結
寫在前面
進入C++以后,C++支持了函數重載也就是在同一作用域中可以存在功能類似的同名函數,例如swap函數來完成int,double,char等數據類型的交換。
void swap(int& e1, int& e2)
{int tmp = e1;e1 = e2;e2 = tmp;
}
void swap(double& e1, double& e2)
{double tmp = e1;e1 = e2;e2 = tmp;
}
void swap(char& e1, char& e2)
{char tmp = e1;e1 = e2;e2 = tmp;
}
但是函數重載面臨的問題是:
- 重載的函數僅僅是類型不同,功能是類似,所以代碼復用率比較低,而且一旦有新類型出現,就需要用戶自己 增加對應的函數。
- 代碼的可維護性比較低,一個出錯可能所有的重載均出錯
如果在C++中,能夠存在這樣一個模具,通過給這個模具中填充不同材料(類型),來獲得不同材料的鑄件(即生成具體類型的代碼)。類似于活字印刷術,模板可以幫助我們快速生成我們所需的代碼。
巧的是前人早已將樹栽好,我們只需在此乘涼。
泛型編程:編寫與類型無關的通用代碼,是代碼復用的一種手段。模板是泛型編程的基礎。
1. 函數模板
1.1 函數模板的概念
- 函數模板概念:函數模板代表了一個函數家族,該函數模板與類型無關,在使用時通過傳遞不同的類型參數來實例化不同類型的函數。這種機制使得代碼更加簡潔和通用,減少了重復代碼的編寫。
- 函數模板格式:定義一個函數模板時,需要使用關鍵字 template或者class(切記:不能使用struct代替class),后跟一個模板參數列表。
template<typename T1, typename T2,…,typename Tn>
返回值類型 函數名(參數列表){}
以下是一個簡單的例子,展示了如何使用模板來創建一個通用的函數模板,它實現了兩個數值的交換:
//template <typename T>
template <class T>
void swap(T& a, T& b)
{T temp = a;a = b;b = temp;
}
1.2 函數模板的原理
大家都知道,瓦特改良蒸汽機,人類開始了工業革命,解放了生產力。機器生產淘汰掉了很多手工產品。本質是什么,重復的工作交給了機器去完成。有人給出了論調:懶人創造世界。馬云曾經說過:"懶不是傻懶,如果你想少干,就要想出懶的方法。要懶出風格,懶出境界"。而函數模板是一個藍圖,它本身并不是函數,是編譯器根據使用方式產生特定具體類型函數的模具。所以模板就是將本來應該我們做的重復的事情交給了編譯器。
在編譯器編譯階段,對于模板函數的使用,編譯器需要根據傳入的實參類型來推演生成對應類型的函數以供調用。比如:當用double類型使用函數模板時,編譯器通過對實參類型的推演,將T確定為double類型,然后產生一份專門處理double類型的代碼。通過下面例子來理解一下:
#include <iostream>
using namespace std;
//函數模板
template<class T>
void Swap(T& e1, T& e2)
{T tmp = e1;e1 = e2;e2 = tmp;
}int main()
{int a = 10, b = 20;double x = 10, y = 20;Swap(a, b);Swap(x, y);return 0;
}
通過匯編代碼再看一下上面的兩次Swap函數調用,也能看出調用的函數不是同一個,因為它們call的地址都不同。
1.3 函數模板的實例化
用不同類型的參數使用函數模板時,稱為函數模板的實例化。模板參數實例化分為:隱式實例化和顯示實例化。
-
隱式實例化:讓編譯器根據實參推演模板參數的實際類型。
例如上面的Swap(a, b) 和 Swap(x, y),編譯器根據傳遞給模板函數的參數自動推導出模板參數的類型。實際上,在大多數情況下編譯器都能夠推導出模板參數的類型,但在下面這種情況,編譯器推導不出來模板參數的具體類型,導致編譯錯誤:
這是因為當調用 Add(e1, e2) 時,e1 的類型是 int,而 e2 的類型是 double,這種情況下編譯器無法自動推導出模板參數 T,因為 int 和 double 是不同的類型。
解決辦法一:自己來強制轉化,使得e1和e2類型相同。
解決辦法二:顯示實例化,下面就來介紹一下什么是顯示實例化。 -
顯示實例化:在函數名后的<>中指定模板參數的實際類型。
在調用 Add<int> 時,將 e2 轉換為 int 類型。
在調用 Add<double> 時,將 e1 轉換為 double 類型。
通過顯式類型轉換,確保傳遞給模板函數的參數類型一致,從而避免編譯錯誤。
1.4 函數模板的實例化模板參數的匹配原則
- 一個非模板函數可以和一個同名的函數模板同時存在,而且該函數模板還可以被實例化為這個非模板函數。
- 對于非模板函數和同名函數模板,如果其他條件都相同,在調動時會優先調用非模板函數而不會從該模板產生出一個實例。如果模板可以產生一個具有更好匹配的函數, 那么將選擇模板。
- 模板函數不允許自動類型轉換,但普通函數可以進行自動類型轉換。
在C++中,模板函數不會進行隱式類型轉換來匹配參數類型,而普通函數會進行隱式類型轉換。如果模板函數不能完全匹配參數類型,編譯器將嘗試調用非模板函數,這時可能會發生隱式類型轉換。
2. 類模板
- 類模板的定義格式:
template<class T1, class T2, …, class Tn>
class 類模板名
{
// 類內成員定義
};
這里,typename 是一個關鍵字,用于指定 T 是一個模板參數。你也可以使用 class 關鍵字來代替 typename,它們在這里是等價的。
在類模板中,如果要在類外定義成員函數,則需要在定義成員函數時提供模板參數列表。這是為了讓編譯器知道這些函數是屬于哪個模板類的實例。下面的例子,展示了如何在類外定義類模板的成員函數:
// 類模板定義
template <class T>
class MyClass
{
private:T data;
public:MyClass(T d); // 構造函數聲明void Print(); // 成員函數聲明
};// 在類外定義構造函數
template <class T>
MyClass<T>::MyClass(T d) : data(d) {}// 在類外定義成員函數
template <class T>
void MyClass<T>::Print()
{cout << data << endl;
}
- 類模板的實例化
類模板實例化與函數模板實例化不同,類模板實例化需要在類模板名字后跟<>,然后將實例化的類型放在<>中即可,類模板名字不是真正的類,而實例化的結果才是真正的類。實例化類模板意味著根據模板定義創建具體的類,這些具體類使用特定的數據類型。
int main()
{MyClass<int> m1;MyClass<double> m1;return 0;
}
3. 非類型模板參數
模板參數分類類型形參與非類型形參。
類型形參即:出現在模板參數列表中,跟在class或者typename之類的參數類型名稱。
非類型形參,就是用一個常量作為類(函數)模板的一個參數,在類(函數)模板中可將該參數當成常量來使用。
例如使用非類型形參來定義一個靜態數組:
#include <iostream>
#include <assert.h>using namespace std;
namespace zzb
{template<class T, size_t N = 10>class array{public:T& operator[](size_t index){assert(index >= 0 && index < _size);return _nums[index];}const T& operator[](int index) const{assert(index >= 0 && index < _size);return _nums[index];}size_t size() const{return _size;}size_t empty()const {return 0 == _size;}private:T _nums[N];size_t _size = N;};
}int main()
{zzb::array<int, 10> nums;return 0;
}
ps:浮點數、類對象以及字符串是不允許作為非類型模板參數的,也就說只有整數類型(包括枚舉)可以作為非類型模板參數。
而且非類型模板參數必須在編譯期確定,這意味著它們的值或大小必須在編譯時就能確定,而不能依賴于運行時的計算或輸入。這樣做是為了在編譯期間能夠生成對應的代碼,以便在程序運行時能夠直接使用這些參數值。
4. 模板的特化
4.1 概念
模板的特化是允許我們為特定類型提供定制的實現,通常情況下使用模板可以實現一些與類型無關的代碼,但對于一些特殊類型的可能會得到一些錯誤的結果,需要特殊處理,在這些情況下是非常有用的。
比如:我們定義一個通用的模板函數來進行小于比較。
// 通用模板函數,用于比較兩個值
template <typename T>
bool Less(const T& a, const T& b)
{return a < b;
}
當我們調用函數模板來比較兩個整數時,發現可以正常比較。
當傳參傳過去的是指針時,它是按照指針的大小來比較的,不是按照指針指向的內容來比比較的,不符合我們的預期。
可以看到,Less絕對多數情況下都可以正常比較,但是在特殊場景下就得到錯誤的結果。上述示例中,x是小于y的,但是Less內部并沒有比較a和b指向的對象內容,而按照地址的大小比較的,這就無法達到預期而錯誤。此時,就需要對模板進行特化。即:在原模板類的基礎上,針對特殊類型所進行特殊化的實現方式。模板特化中分為函數模板特化與類模板特化。
4.2 函數模板特化
函數模板的特化步驟:
1.必須要先有一個基礎的函數模板,它可以處理大多數情況。
// 通用模板函數,用于比較兩個值
template <typename T>
bool Less(const T& a, const T& b)
{return a < b;
}
2.關鍵字template后面接一對空的尖括號<>。
3.函數名后跟一對尖括號,尖括號中指定需要特化的類型。
4.函數形參表: 必須要和模板函數的基礎參數類型完全相同,如果不同編譯器可能會報一些奇怪的錯誤。
//函數模板的特化
template <>
bool Less<int*>(const int*& a, const int*& b)
{return *a < *b;
}
此時傳參再傳過去指針時,它就是按照指針指向的內容來比較的,符合我們的預期。這是因為特化模板在模板實例化時會優先于通用模板。
我們可以看出上面的特化版本看著特別奇怪 const 寫在int* 后面,因此一般情況下如果函數模板遇到不能處理或者處理有誤的類型,為了實現簡單通常都是將該函數直接給出。
bool Less(const int* a, const int* b)
{return *a < *b;
}
該種實現簡單明了,代碼的可讀性高,容易書寫,因為對于一些參數類型復雜的函數模板,特化時特別給出,因此函數模板不建議特化。
4.3 類模板特化
例如有如下專門用來按照小于比較的類模板Less:
template<class T>
struct Less
{bool operator()(const T& e1, const T& e2){return e1 < e2;}
};
調用sort函數排序一個整數數組,發下可以直接排序。
調用sort函數排序一個整型指針數組,可以直接排序,但是結果錯誤。
同理,這里也需要提供int*的特化版本,而特化分為全特化與偏特化。
1.全特化:將模板參數列表中所有的參數都確定化。
template<>
struct Less<int*>
{bool operator()(int* e1, int* e2){return *e1 < *e2;}
};
上面提供的就是int*的全特化版本,模板參數列表中所有的參數都確定化了,此時運行程序,結果正確。
2.偏特化:任何針對模版參數進一步進行條件限制設計的特化版本。
偏特化有以下兩種表現方式:
- 參數更進一步的限制:偏特化并不僅僅是指特化部分參數,而是針對模板參數更進一步的條件限制所設計出來的一個特化版本。
template<class T>
struct Less<T*>
{bool operator()(T* e1, T* e2){return *e1 < *e2;}
};
上面提供的就是T的偏特化版本,對模板參數列表中的參數進行了更進一步的條件限制,不僅能匹配int類型的,是能匹配所有類型的指針。
此時運行程序,結果也是正確的。
- 偏特化的另一種表現方式是部分特化:將模板參數類表中的一部分參數特化。
例如有如下類模板:
template <typename T1, typename T2>
class MyClass
{
public:void print(){cout << "MyClass <T1, T2>" << endl;}
};
對上面類進行部分特化:
template <typename T1>
class MyClass<T1, int>
{
public:void print(){cout << "MyClass <T1, int>" << endl;}
};
MyClass<T1, int> 是對第二個模板參數 T2 為 int 的情況進行的特化,在main 函數中,分別調用了基礎模板和部分特化模板,可以看到編譯器根據傳入的類型選擇了合適的模板版本。
5. 模板分離編譯
分離編譯:一個程序(項目)由若干個源文件共同實現,而每個源文件單獨編譯生成目標文件,最后將所有目標文件鏈接起來形成單一的可執行文件的過程稱為分離編譯模式。 關于編譯鏈接的相關知識可以看下之前的寫的文章:詳解C語言的編譯與鏈接,這里不再贅述。
而模板分離編譯的分離編譯是將模板的聲明和定義分離到不同的文件中,通常是聲明放到xxx.h,定義放到xxx.cpp。
假如有以下場景,模板的聲明與定義分離開,在頭文件中進行聲明,源文件中完成定義:
1.我們首先在頭文件中聲明函數模板,但不包括具體的實現。
//Swap.h
template<class T>
void Swap(T& e1, T& e2);
2.然后創建一個單獨的實現文件,包含模板的完整定義。
//Swap.cpp
#include "Swap.h"
template<class T>
void Swap(T& e1, T& e2)
{T tmp = e1;e1 = e2;e2 = tmp;
}
3.在使用模板的源文件test.cpp中,包含頭文件Swap.h,然后調用模板Swap來完成兩個數的交換。
//test.cpp
#include <iostream>
#include "Swap.h"
using namespace std;int main()
{int x = 10, y = 20;cout << "交換前 x: " << x << " " << "y: " << y << endl;Swap(x, y);cout << "交換后 x: " << x << " " << "y: " << y << endl;return 0;
}
運行程序,發現報如下錯誤:
這是因為,一個C/C++程序要變成一個可執行程序,要經過如下過程:
而編譯器對工程中的源文件是單獨編譯的,因此:
對于上面的問題有如下兩個解決辦法:
1.在模板定義的位置顯式實例化(不推薦)。
這種辦法存在一定的弊端,當再有兩個double類型的變量調用函數模板完成交換時,又會報錯。
要想解決這個錯誤,就只能在模板定義的地方再去實例化一份Swap<double>的出來。因此每當出現一個新類型去調用這個模板的時候,都需要去模板定義的地方去顯示實例化一份出來。這種顯式實例化方式只適用于我們能預先知道所需類型的情況且這在泛型編程中并不常見,下面來介紹另一種解決方式。
2. 將聲明和定義放到同一個文件 “xxx.hpp” 里面或者xxx.h(推薦)。
//Swap.h
template<class T>
void Swap(T& e1, T& e2)
{T tmp = e1;e1 = e2;e2 = tmp;
}
這也就意味著,當在test.cpp中包含Swap.h的以后,在test.cpp中可以找到函數模板的完整定義,因此可以根據需求實例化出任意需要的函數,就不會報鏈接錯誤了。
6. 總結
關于模板的優缺點總結如下:
優點:
1.代碼復用:通過模板,可以編寫通用的代碼,而不需要為每個數據類型編寫單獨的代碼,實現了代碼的高效復用。模板的使用加速了開發過程,因為可以更容易地引入新的數據類型,而無需修改大量現有代碼。C++ 標準模板庫(STL)的產生就是模板技術的重要應用之一,提供了大量高效的容器、算法和迭代器,極大地提高了開發效率。
增強代碼的靈活性:
2.泛型編程:模板允許編寫與類型無關的代碼,可以處理不同類型的數據,增強了代碼的通用性和靈活性。
缺陷:
1.代碼膨脹:模板的實例化會導致生成多個實例代碼,可能導致二進制文件變大,尤其是在大型項目中。模板實例化需要更多的編譯時間,特別是當模板被廣泛使用時,編譯時間會顯著增加。
2. 模板編譯錯誤信息復雜:模板編譯錯誤信息通常非常復雜且冗長,不易理解和調試,定位錯誤可能需要更多的時間和經驗。
至此,本片文章就結束了,若本篇內容對您有所幫助,請三連點贊,關注,收藏支持下。
創作不易,白嫖不好,各位的支持和認可,就是我創作的最大動力,我們下篇文章見!
如果本篇博客有任何錯誤,請批評指教,不勝感激 !!!