@TOC
📝前言
前面我們學習了初階模版,本節我們將學習模版進階
🌠 非類型模版參數
模版參數分類為兩種:類型形參與非類型形參。
- 類型形參:出現在模板參數列表中,跟在class或者typename之類的參數類型名稱。
- 非類型形參:就是用一個常量作為類型(函數)模版的一個參數,在類(函數)模版中可將該參數當做常量來使用。
例如:
#define N 100
//靜態的棧
template<class T>
class Stack
{
private:
int _a[N];
int _top;
};
int main()
{
Stack<int> st1;
Stack<int> st2;
}
用宏定義來定義一個數組的大小,無法開出根據需求開出大小不同的棧,因此,非類型形參,用一個常量N來作為類型(函數模版)的一個參數。與類的構造函數的給缺省值做函數形參。
template<class T, size_t N = 10>
class Stack
{
private:
int _a[N];
int _top;
};
模版實例化:
Stack<int> st1; //棧大小 10
Stack<int,100> st2;// 100
Stack<int, 1000> st3;// 1000
形同於array數組:
//非類型模版參數
template<class T, size_t N = 10>
class array
{
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;
};
array<int , 100> aa1;
array<int , 1000>aa2;
總結: T是一個類型模版參數代表你需要的類型,N非類型模版參數,代表數組的大小。
注意:
- 浮點數、類對象以及字符串是不允許作為非類型模板參數的。
- 非類型的模板參數必須在編譯期就能確認結果(在使用非類型模板參數時,其實現需要在編譯時確定下來,這意味着不能使用動態計算的值或運行時才能得知的值作為非類型模板參數)。
第二點可能有點模糊,舉個例子:
// 數組容器類模板,大小由非類型模板參數決定
template <typename T, int size>
class Array {
private:
T data[size];
public:
// 構造函數
Array() {
std::cout << "Array of size " << size << " created." << std::endl;
}
// 訪問元素的操作
T& operator[](int index) {
return data[index];
}
// 其他數組操作的成員函數
};
// 獲取用户輸入的數組大小
int getUserInputSize() {
int size;
std::cout << "Enter the size of the array: ";
std::cin >> size;
return size;
}
int main() {
// 使用編譯時確定的大小創建數組
/* Array<int, 10> intArray;
intArray[0] = 42;
std::cout << "intArray[0] = " << intArray[0] << std::endl;*/
// 試圖使用運行時確定的大小創建數組,會導致編譯錯誤
int runTimeSize = getUserInputSize();
Array<int, runTimeSize> runtimeArray; // 編譯錯誤
return 0;
}
正常使用:
Array<int, 10> intArray; // 可以正常工作,因為 10 是一個編譯時可確定的值
但是我們不能這樣做:
int runTimeSize = getUserInputSize(); // 用户在運行時輸入數組大小
Array<int, runTimeSize> intArray; // 錯誤!runTimeSize 是在運行時才能確定的
這樣會導致編譯錯誤,因為編譯器無法在編譯時就知道 runTimeSize 的具體值,無法為 Array 模板生成正確的代碼。
結論:非類型模板參數只能使用編譯時就能確定下來的值,像整型字面量、枚舉值、指針或引用等。動態計算或運行時確定的值是無法作為非類型模板參數的。
🌉模版按需實例化
上面談到模版實例化時參數使用的情況,現在我們學習一下模板的重要特性。 在 C++ 中,模板是在使用時才被實例化的。也就是説,只有當程序中真正使用某個特定的模板實例時,編譯器才會為它生成具體的代碼。這個過程被稱為"按需實例化"。
- 基本數據類型的實例化:
template <typename T>
T add(T a, T b)
{
return a + b;
}
int main()
{
std::cout << add<int>(1, 2) << std::endl; // 實例化 add<int>
std::cout << add<double>(3.4, 5.6) << std::endl; // 實例化 add<double>
return 0;
}
在這個例子中,add 函數模板只有在被實際調用時才會實例化。當程序調用 add<int> 和 add<double> 時,編譯器會分別生成 add<int> 和 add<double> 的具體實現。
- 自定義類型的實例化:
template <typename T, int size>
class Array
{
private:
T data[size];
public:
void set(int index, T value)
{
data[index] = value;
}
T get(int index)
{
return data[index];
}
};
int main()
{
Array<int, 5> intArray;
intArray.set(0, 10);
std::cout << intArray.get(0) << std::endl; // 實例化 Array<int, 5>
Array<std::string, 3> stringArray;
stringArray.set(0, "hello");
std::cout << stringArray.get(0) << std::endl; // 實例化 Array<std::string, 3>
return 0;
}
在這個例子中,Array 類模板只有在被實際使用時才會實例化。當程序創建 intArray 和 stringArray 對象時,編譯器會分別生成 Array<int, 5> 和 Array<std::string, 3> 的具體實現。
- 編譯時錯誤排查:
template<class T, size_t N = 10>
class Stack
{
public:
void func()
{
N++;
}
private:
int _a[N];
int _top;
};
int main()
{
Stack<int,100> st1;
//Stack<int,100> st2;
//Stack<int, 1000> st3;
//按需實例化
// 不調用,不報錯,沒有使用就沒有實例化
st1.func();
return 0;
}
調用fun1()時,N++,會導致數組,越界,但是你不調用它,他就不報錯,沒有使用就咩有實例化,即使寫了在模版裏面,即使錯了,不使用,正常編譯就不會出錯。
這個錯誤只有在實例化時才會被發現,而不是在程序編譯時就被發現。
🌉實例化的打印
template<class T>
void PrintVector(const vector<T>& v)
{
//類模版沒實例化時,不去裏面差細節東西,無法確定
//加typename明確告訴是類型
typename vector<T>::const_iterator it = v.begin();
//auto it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
int main()
{
vector<int> v1 = { 1,2,3,4,5,6,7 };
vector<double> v2{ 1.1,2.2,3.3,4.4,5.5 };
PrintVector(v1);
PrintVector(v2);
}
🌠模板的特化
🌉概念
通常情況下,使用模板可以實現一些與類型無關的代碼,但對於一些特殊類型的可能會得到一些錯誤的結果,需要特殊處理,比如:實現了一個專門用來進行小於比較的函數模板
// 函數模板 -- 參數匹配
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;
}
可以看到,Less絕對多數情況下都可以正常比較,但是在特殊場景下就得到錯誤的結果。上述示例中,p1指向的d1顯然小於p2指向的d2對象,但是Less內部並沒有比較p1和p2指向的對象內容,而比較的是p1和p2指針的地址,這就無法達到預期而錯誤。
此時,就需要對模板進行特化。即:在原模板類的基礎上,針對特殊類型所進行特殊化的實現方式。模板特化中分為函數模板特化與類模板特化。
🌉 函數模板特化
函數模板的特化步驟:
- 必須要先有一個基礎的函數模板
- 關鍵字
template後面接一對空的尖括號<> - 函數名後跟一對尖括號,尖括號中指定需要特化的類型
- 函數形參表: 必須要和模板函數的基礎參數類型完全相同,如果不同編譯器可能會報一些奇怪的錯誤。
//函數模版 -- 參數模版
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(2024, 7, 7);
Date d2(2004, 7, 9);
cout << Less(d1, d2) << endl; //可以比較,結果正確
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; //可以比較,結果錯誤
//調用特化後的版本,就不走模版生成了
}
注意:一般情況下如果函數模板遇到不能處理或者處理有誤的類型,為了實現簡單通常都是將該函數直接給出。
bool Less(Date* left, Date* right)
{
return *left < *right;
}
現在我們只需加上類就可以比較了:
class Date
{
public:
Date(int year = 1900, 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);
}
friend ostream& operator<<(ostream& _cout, const Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
template<class T>
bool Less(const T& left, const T& right)
{
return left < right;
}
bool Less(Date* left, Date* right)
{
return *left < *right;
}
template<>
bool Less<Date*>(Date* const& left, Date* const& right)
{
return *left < *right;
}
int main()
{
Date d1(2022, 7, 8);
Date d2(2022, 7, 9);
cout << Less(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl;
return 0;
}
🌠類模板特化
🌉 全特化
全特化即是將模板參數列表中所有的參數都確定化。
///全特化
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Date<T1 , T2> " << endl;
}
private:
T1 _d1;
T2 _d2;
};
tempate<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<T1,T2> " << endl;
}
private:
int _d1;
char _d2;
};
void Testvector()
{
Data<int, char> d1;
Data<int, char> d2;
}
🌠偏特化
偏特化:任何針對模版參數進一步進行條件限制設計的特化版本。比如對於以下模板類:
//偏特化
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2> " << endl;
}
private:
T1 _d1;
T2 _d2;
};
偏特化有以下兩種表現方式:
- 部分特化 將模板參數類表中的一部分參數特化。
// 將第二個參數特化為int
template<class T>
class Data<T1, int>
{
public:
Data()
{
cout << " Data<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()
{
cout << "Data<T1&, T2&> " << endl;
}
private:
const T1 & _d1;
const T2 & _d2;
};
void test2()
{
Data<double, int> d1;
Data<int, double> d2;
Data<int*, int*> d3;
Data<int&, int&> d4;
}
測試一下:
//類模版
template<class T1,class T2>
class Data
{
public:
Data()
{
cout << "Data<T1 , T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//全特化
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int , char>" << endl;
}
};
//偏特化
template <class T1>
class Data<T1, int>
{
public:
Data()
{
cout << "Data<T1 , int> " << endl;
}
private:
T1 _d1;
int _d2;
};
//限定版本的類型
template <typename T1,typename T2>
class Data<T1*, T2*>
{
public:
Data()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
cout << "Data<T1* ,T2*>- 限定模版偏特化" << endl;
//T1 x1;
//T2* p1;
}
};
template <typename T1,typename T2>
class Data<T1&, T2&>
{
public:
Data()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
cout << "Data<T1& ,T2&>- 限定模版偏特化" << endl;
}
};
template <typename T1, typename T2>
class Data<T1&, T2*>
{
public:
Data()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
cout << "Data<T1& ,T2*>- 限定模版偏特化" << endl;
}
};
int main()
{
Data<int, int>d1;
Data<int, char> d2;
Data<int, double> d3;
Data<int*, double*> d4;
Data<int*, int**>d5;
Data<int&, int&> d6;
Data<int&, int*>d7;
return 0;
}
🌉 類模板特化應用示例
有如下專門用來按照小於比較的類模板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, 8);
Date d3(2022, 7, 9);
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*>());
}
通過觀察上述程序的結果發現,對於日期對象可以直接排序,並且結果是正確的。但是如果待排序元素是指 針,結果就不一定正確。因為:sort最終按照Less模板中方式比較,所以只會比較指針,而不是比較指針指 向空間中內容,此時可以使用類版本特化來處理上述問題:
// 對Less類模板按照指針方式特化
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return *x < *y;
}
};
特化之後,在運行上述代碼,就可以得到正確的結果
🌠 模板分離編譯
🌉 什麼是分離編譯
分離編譯(Separate Compilation)是編譯過程的一種模式,它允許將一個大型程序拆分成多個較小的、獨立的編譯單元,每個單元分別編譯成目標文件(通常是 .obj 或 .o 文件),最後再通過鏈接器將這些目標文件組合成一個單一的執行文件。
首先是頭文件 Calculator.h,它包含了類的聲明:
// Calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H
class Calculator {
public:
int add(int a, int b);
int subtract(int a, int b);
};
#endif // CALCULATOR_H
然後是源文件 Calculator.cpp,它實現了頭文件中聲明的類和成員函數:
// Calculator.cpp
#include "Calculator.h"
int Calculator::add(int a, int b) {
return a + b;
}
int Calculator::subtract(int a, int b) {
return a - b;
}
最後是主程序文件 main.cpp,它使用了 Calculator 類:
// main.cpp
#include <iostream>
#include "Calculator.h"
int main() {
Calculator calc;
int a = 10;
int b = 5;
std::cout << "The sum of " << a << " and " << b << " is " << calc.add(a, b) << std::endl;
std::cout << "The difference of " << a << " and " << b << " is " << calc.subtract(a, b) << std::endl;
return 0;
}
在 C 和 C++ 中,分離編譯是通過頭文件==(.h 或 .hpp)==來管理的,頭文件包含了函數和類的聲明,而源文件包含了它們的實現。這樣,當多個源文件需要使用相同的函數或類時,它們只需要包含相應的頭文件即可,而不需要重複實現代碼。
🌠 模板的分離編譯
假如有以下場景,模板的聲明與定義分離開,在頭文件中進行聲明,源文件中完成定義:
1.編寫頭文件: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;
}
3.定義主函數main.cpp,包含a.h
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
編譯器在遇到模板函數調用時,會根據具體的模板實參進行類型推導和函數實例化。頭文件中沒有包含模板定義:如果 a.h 中只包含了模板函數的聲明,而沒有包含模板的定義(就像代碼那樣),那麼編譯器在編譯 main.cpp 時將無法實例化 Add 函數,因為它不知道如何實現它。
我們知道,源文件.cpp在編譯時,是先經過單獨編譯的,而編譯main.cpp,即使包含了add的聲明,但是無法找到add的定義進行編譯,因此在鏈接時,當鏈接時,add(int,int),將會導致鏈接錯誤,找不到add的地址
🌉 解決方法
- 將聲明和定義放到一個文件 "xxx.hpp" 裏面或者xxx.h其實也是可以的。推薦使用這種。
定義和聲明,都放在一個頭文件裏面
template<class T>
T Add(const T& left, const T& right)
{
cout << "T Add(const T& left, const T& right)" << endl;
return left + right;
}
- 模板定義的位置顯式實例化。這種方法不實用,不推薦使用。
頭文件.h
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right);
源文件
.cpp
#include "Func.h"
template<class T>
T Add(const T& left, const T& right)
{
cout << "T Add(const T& left, const T& right)" << endl;
return left + right;
}
//顯示實例化 ,這種解決方式很被動 ,需要不斷添加實例化
template
int Add(const int& left, const int& right);
template
double Add(const double& left, const double& right);
. 模板總結 【優點】
- 模板複用了代碼,節省資源,更快的迭代開發,C++的標準模板庫(STL)因此而產生
- 增強了代碼的靈活性 【缺陷】
- 模板會導致代碼膨脹問題,也會導致編譯時間變長
- 出現模板編譯錯誤時,錯誤信息非常凌亂,不易定位錯誤
🚩.模板總結
【優點】
- 模板複用了代碼,節省資源,更快的迭代開發,
C++的標準模板庫(STL)因此而產生 - 增強了代碼的靈活性
【缺陷】
- 模板會導致代碼膨脹問題,也會導致編譯時間變長
- 出現模板編譯錯誤時,錯誤信息非常凌亂,不易定位錯誤