目錄
前言:
本章學習目標:
1.非類型模版參數?
1.1使用方法?
1.2注意事項
1.3?實際引用
2.模版特化?
?2.1概念
2.2函數模板特化
?2.3類模板特化
2.3.1全特化
2.3.2偏特化?
?3.模版分離編譯
?編輯?3.1失敗原因
?編輯?3.2解決方案
4 總結?
前言:
本章節是在學習完STL之后,對高階模版進行的總結,模板給泛型編程注入了靈魂,模板提高了程序的靈活性,模板包括:非類型模版參數、全特化、偏特化等,同時本文還會對模板聲明和定義不能分離的問題做出介紹。
本章學習目標:
1.非類型模版參數?
模板參數分類 :類型形參與非類型形參。
類型形參即:? ? 出現在模板參數列表中,跟在class或者typename之類的參數類型名稱。
非類型形參:? ? 就是用一個常量作為類(函數)模板的一個參數,在類(函數)模板中可將該參數當成常量來使用。
1.1使用方法?
非類型模版參數 既然是使用常量作為參數,那么浮點數、類對象以及字符串是不允許作為非類型模版參數的,???非類型模版參數必須在編譯期就能確認結果。
利用非類型模版參數創建一個可以自由調整大小的整形數組代碼如下:
#include <assert.h>
using namespace std;template <size_t N>
class arr
{
public:int& operator[](size_t pos){assert(pos >= 0 && pos < N);return _arr[pos];}size_t size() const{return N;}private:int _arr[N];
};
int main()
{arr<5> a1;arr<10> a2;arr<15> a3;cout <<a1.size() << endl;cout << a2.size() << endl;cout << a3.size() << endl;return 0;
}
定義一個模板類型的靜態數組
template <class T ,size_t N>
class arr
{
public:T& operator[](size_t pos){assert(pos >= 0 && pos < N);return _arr[pos];}size_t size() const{return N;}private:int _arr[N];
};
int main()
{arr<int,5> a1;arr<double,10> a2;arr<char,15> a3;cout <<a1.size()<< typeid(a1).name() << endl;cout << a2.size() << typeid(a2).name()<< endl;cout << a3.size() << typeid(a3).name()<< endl;return 0;
}
1.2注意事項
?非類型模板參數要求類型為?整型家族,其他類型是不被編譯器通過的,如果我們使用非整形的家族成員就會報錯,代碼如下:
//浮點型,非標準
template<class T, double N>
class arr
{/*……*/
};
?整形家族:char? ? ?short? ? ? int? ? ? bool? ? ? ? ?long? ? ? ? ? ?longlong
1.3?實際引用
?在?C++11
?標準中,引入了一個新容器?array
,它就使用了?非類型模板參數,為一個真正意義上的?泛型數組,這個數組是用來對標傳統數組的。
注意:?部分老編譯器可能不支持使用此容器
新引入arry非類型模版參數的代碼?如下:
#include <iostream>
#include <cassert>
#include <array> //引入頭文件using namespace std;int main()
{int arrOld[10] = { 0 }; //傳統數組array<int, 10> arrNew; //新標準中的數組//與傳統數組一樣,新數組并沒有進行初始化//新數組對于越界讀、寫檢查更為嚴格 優點arrOld[15]; //老數組越界讀,未報錯arrNew[15]; //新數組則會報錯arrOld[12] = 0; //老數組越界寫,不報錯,出現嚴重的內存問題arrNew[12] = 10; //新數組嚴格檢查return 0;
}
array
?是泛型編程思想中的產物,支持了許多?STL
?容器的功能,比如?迭代器?和?運算符重載?等實用功能,最主要的改進是?嚴格檢查越界行為。
需要提醒的是:arry能做到嚴格的越界檢查 得益于 []的重載,對下標進行了嚴格檢查。
2.模版特化?
?2.1概念
通常情況下,使用模板可以實現一些與類型無關的代碼,但對于一些特殊類型的可能會得到一些錯誤的結果,需要特殊處理,比如:使用?日期類對象指針?構建優先級隊列后,若不編寫對應的仿函數,則比較結果會變為未定義 代碼如下:
// 函數模板 -- 參數匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}
int main()
{cout << Less(1, 2) << endl; // 可以比較,結果正確Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl; // 可以比較,結果正確Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 可以比較,結果錯誤return 0;
}
造成每次結果不一樣是因為 我們每次都日期類對象比較的時候 系統每次分配的地址都是隨機的,我們對地址進行比較是不符合實際情況的。?
2.2函數模板特化
1. 必須要先有一個基礎的函數模板
2. 關鍵字template后面接一對空的尖括號<>
3. 函數名后跟一對尖括號,尖括號中指定需要特化的類型
4. 函數形參表:?必須要和模板函數的基礎參數類型完全相同,如果不同編譯器可能會報一些奇怪的錯誤。
上述日期進行比較的函數模版進行特化之后,代碼如下:
// 函數模板 -- 參數匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}
// 對Less函數模板進行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{return *left < *right;
}
int main()
{cout << Less(1, 2) << endl;Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl;Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 調用特化之后的版本,而不走模板生成了return 0;
}
?2.3類模板特化
?模板特化主要用在類模板中,它可以在泛型思想之上解決大部分特殊問題,并且類模板特化還可以分為:全特化和偏特化,適用于不同場景
后面用的日期類舉例比較多,先把日期類放出來
class Date
{
public:Date(int year = 1970, int month = 1, int day = 1): _year(year), _month(month), _day(day){}bool operator<(const Date& d)const{return (_year < d._year) ||(_year == d._year && _month < d._month) ||(_year == d._year && _month == d._month && _day < d._day);}bool operator>(const Date& d)const{return (_year > d._year) ||(_year == d._year && _month > d._month) ||(_year == d._year && _month == d._month && _day > d._day);}private:int _year;int _month;int _day;
};
2.3.1全特化
?全特化指?將所有的模板參數特化為具體類型,將模板全特化后,調用時,會優先選擇更為匹配的模板類
//原模板
template<class T1, class T2>
class Test
{
public:Test(const T1& t1, const T2& t2):_t1(t1),_t2(t2){cout << "template<class T1, class T2>" << endl;}private:T1 _t1;T2 _t2;
};//全特化后的模板
template<>
class Test<int, char>
{
public:Test(const int& t1, const char& t2):_t1(t1), _t2(t2){cout << "template<>" << endl;}private:int _t1;char _t2;
};int main()
{Test<int, int> T1(1, 2);Test<int, char> T2(20, 'c');return 0;
}
對模板進行全特化處理后,實際調用時,會優先選擇已經特化并且類型符合的模板。
2.3.2偏特化?
?偏特化,指?將泛型范圍進一步限制,可以限制為某種類型的指針,也可以限制為具體類型
//原模板---兩個模板參數
template<class T1, class T2>
class Test
{
public:Test(){cout << "class Test" << endl;}
};//偏特化之一:限制為某種類型
template<class T1>
class Test<T1, int>
{
public:Test(){cout << "class Test<T, int>" << endl;}
};//偏特化之二:限制為不同的具體類型
template<class T>
class Test<T*, T*>
{
public:Test(){cout << "class Test<T*, T*>" << endl;}
};int main()
{Test<double, double> t1;Test<char, int> t2;Test<Date*, Date*> t3;return 0;
}
?
偏特化(尤其是限制為某種類型)在?泛型思想?和?特殊情況?之間做了折中處理,使得?限制范圍式的偏特化?也可以實現?泛型
- 比如偏特化為?
T*
,那么傳?int*
、char*
、Date*
?都是可行的
應用實例:
?有如下專門用來按照小于比較的類模板 :Less
#include<vector>
#include <algorithm>
template<class T>
struct Less
{bool operator()(const T& x, const T& y) const{return x < y;}
};
int main()
{Date d1(2022, 7, 7);Date d2(2022, 7, 6);Date d3(2022, 7, 8);vector<Date> v1;v1.push_back(d1);
v1.push_back(d2);v1.push_back(d3);// 可以直接排序,結果是日期升序sort(v1.begin(), v1.end(), Less<Date>());vector<Date*> 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<Date*>());return 0;
}
// 對Less類模板按照指針方式特化
template<>
struct Less<Date*>
{bool operator()(Date* x, Date* y) const{return *x < *y;}
};
?3.模版分離編譯
?
早在?模板初階?中,我們就已經知道了?模板不能進行分離編譯,會引發鏈接問題
?3.1失敗原因
?聲明與定義分離后,在進行鏈接時,無法在符號表中找到目標地址進行跳轉,因此鏈接錯誤
下面是?模板聲明與定義寫在同一個文件中時,具體的匯編代碼執行步驟
test.h?
#pragma once//聲明
template<class T>
T add(const T x, const T y);//定義
template<class T>
T add(const T x, const T y)
{return x + y;
}
?main.cpp
#include <iostream>
#include "Test.h"using namespace std;int main()
{add(1, 2);return 0;
}
?
?聲明與定義在同一個文件中時,可以直接找到函數的地址
編譯器 生成可執行文件的四個步驟:
- 預處理:頭文件展開、宏替換、條件編譯、刪除注釋,生成純凈的C代碼
- 編譯:語法 / 詞法 / 語義 分析、符號匯總,生成匯編代碼
- 匯編:生成符號表,生成二進制指令
- 鏈接:合并段表,將符號表進行合并和重定位,生成可執行程序
當模板的?聲明?與?定義?分離時,因為是?【泛型】,所以編譯器無法確定函數原型,即?無法生成函數,也就無法獲得函數地址,在符號表中進行函數鏈接時,必然失敗?
?3.2解決方案
?解決方法有兩種:
- 在函數定義時進行模板特化,編譯時生成地址以進行鏈接
- 模板的聲明和定義不要分離,直接寫在同一個文件中
//定義
//解決方法一:模板特化(不推薦,如果類型多的話,需要特化很多份)
template<>
int add(const int x, const int y)
{return x + y;
}
?
//定義
//解決方法二:聲明和定義寫在同一個文件中
template<class T>
T add(const T x, const T y)
{return x + y;
}
這也就解釋了為什么涉及?模板?的類,其中的函數聲明和定義會寫在同一個文件中 (.h
),著名的?STL
?庫中的代碼的聲明和定義都是在一個?.h
?文件中
為了讓別人一眼就看出來頭文件中包含了?聲明?與?定義,可以將頭文件后綴改為?
.hpp
,著名的?Boost
?庫中就有這樣的命名方式?
?
4 總結?
?
模板是 STL 的基礎支撐,假若沒有模板、沒有泛型編程思想,那么恐怕 "STL" 會變得非常大
模板的優點:
模板復用了代碼,節省資源,更快的迭代開發,C++的標準模板庫(STL)因此而產生
增強了代碼的靈活性
模板的缺點:
模板會導致代碼膨脹問題,也會導致編譯時間變長
出現模板編譯錯誤時,錯誤信息非常凌亂,不易定位錯誤位置
總之,模板 是一把雙刃劍,既有優點,也有缺點,只有把它用好了,才能使代碼 更靈活、更優雅