目錄
前言
1. 非類型模版參數
1.1 概念與講解
1.2? array容器
2. 模版的特化
2.1 概念
2.2 函數模版特化
2.3 類模版特化
2.3.1 全特化
2.3.2 偏特化
3.模版的編譯分離
3.1 什么是分離編譯
3.2 模版的分離編譯
3.3 解決方法
4. 模版總結
總結
前言
本篇文章主要講解的是模版進階的內容,其中有模版更深入的應用,內容豐富,干貨多多!
1. 非類型模版參數
1.1 概念與講解
模版參數分類類型形參與非類型形參。
類型形參即,出現在模版參數列表中,跟在class或者typename后面的參數類型名稱。
非類型形參,就是用一個常量作為類(函數)模版得一個參數,在類(函數)模版中可將該參數當成常量來使用。
我們來看下面的場景。
- 我們要完成一個靜態的棧,一般可以使用宏來定義N,需要存儲多少數據,修改宏的大小即可。如果在main函數中我們想讓第一個棧存儲10個數據,第二個棧存儲100數據。
- 此時,宏的弊端就體現出來。因為N只能表示一個數值,為了滿足上面的要求,將N定義為100。可是第一個棧只需要存儲10個數據,這樣就會造成空間上的浪費。
#define N 100//靜態的棧
template<class T>
class Stack
{
private:T a[N];int top;
};int main()
{Stack<int> st1; //10Stack<int> st2; //100return 0;
}
為了解決上面出現的問題,我們可以使用非類型模版參數。
//靜態的棧
template<class T, size_t N>
class Stack
{
private:T a[N];int top;
};int main()
{Stack<int, 10> st1; //10Stack<int, 100> st2; //100return 0;
}
- 非類型模版參數還可以給缺省值,類似于函數參數。
- 非類型模版參數是個常量,不可以修改。
//靜態的棧
template<class T, size_t N = 10>
class Stack
{
public:void func(){N++;//不可以修改N}private:T a[N];int top;
};int main()
{Stack<int> st1; //10Stack<int, 100> st2; //100st1.func();//會報錯return 0;
}
- C++20之前的版本只允許整型類型做非類型模版參數。
- C++20之后的版本支持所有內置類型做非類型模版參數,但是不支持自定義類型參數做非類型模版參數
template<double X, string str>
class Unkonwn
{};int main()
{Unkonwn<1.1, "xxxxx"> un;return 0;
}
1.2? array容器
非類型模版參數既然可以解決一些場景下的問題,在STL中的容器有沒有使用的呢?array容器就是用非類型模版參數。相當于一個定長數組,進行了封裝。
#include <array>
int main()
{array<int, 10> aa1;cout << sizeof(aa1) << endl;return 0;
}
相比于之前的數組,array容器有什么優勢呢?
- 之前的定長數組,檢查越界方面使用的是抽查機制,即檢查超出數組下標一兩位內存空間是否發生改變,如果超出數組下標太多,可能檢查不到,并且檢查的成本很大。只有你進行寫的操作可以檢查出來,如果進行讀操作,打印出來時,無法檢查出來。
- array容器是自定義類型,下標方括號訪問是運算符重載,可以對方括號內的參數進行限制,不管是寫還是讀操作,都能檢查出來。
#include <array>
int main()
{//嚴格的越界檢查array<int, 10> aa1;aa1[10] = 10;cout << aa1[11] << endl;int aa2[10];aa2[10] = 10; //大多數編譯器檢查的出來aa2[14] = 10; //一般的編譯器檢查不出來,除非是新版的cout << aa2[15] << endl; //檢查不出來return 0;
}
2. 模版的特化
2.1 概念
通常情況下,使用模版可以實現一些與類型無關的代碼,但對于一些特殊類型的肯呢個會得到一些錯誤的結果,需要進行特殊處理。如下面的場景,寫一個用來進行小于比較的函數模版。
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{if (_year < d._year)return true;elseif (_year == d._year)if (_month < d._month)return true;elseif (_month == d._month)return _day < d._day;return false;}
private:int _year;int _month;int _day;
};// 函數模板 -- 參數匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}int main()
{cout << Less(1, 2) << endl;// 可以比較Date d1(2024, 7, 8);Date d2(2024, 7, 5);cout << Less(d1, d2) << endl; // 可以比較Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 可以比較,但是結果錯誤return 0;
}
運行結果如下:
如上述代碼所示,Less函數使用模版后對內置類型和自定義類型都可以進行比較操作,但是Date*日期指針類型返回的結果是跟Date類型結果不同,因為p1表示d1日期類變量的地址,p2表示d2日期類變量的地址,只有對p1和p2進行解引用時,才是指向d1和d2兩個變量的內容,所以指針相關類型比較特殊。
在原模版類的基礎上,針對特殊類型所進行特殊的實現方式。模版特化中分為函數模版特化與類模版特化。
2.2 函數模版特化
函數模版特化要求:
- 必須要先有一個基礎的函數模版。
- 關鍵字template后面接一對空的尖括號<>。
- 函數名后跟一對尖括號,尖括號中指定需要特化的類型。
- 函數形參必須要和原模版函數的基礎參數類型完全相同,需要注意的是,如果不同編譯器可能會報一些奇怪的錯誤。
一開始用Less函數舉例子,指出對于指針類型無法比較,我們可以使用函數模版來解決。
template<class T>
bool Less(T left, T right)
{return left < right;
}template<>
bool Less<Date*>(Date* left, Date* right)
{return *left < *right;
}int main()
{cout << Less(1, 2) << endl;Date d1(2024, 7, 8);Date d2(2024, 7, 5);cout << Less(d1, d2) << endl; Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; //調用特化函數模版,進行比較return 0;
}
運行結果如下:
不過使用模版函數會有許多坑。我們一般對函數使用模版時,函數參數會對模版使用引用,如果這個函數不進行修改操作,會在前面加上const修飾,防止該變量被修改。
- 如下面代碼所示,一般人寫函數模版的特化,會寫成第一種。
- 這是錯誤的,因為T此時是Date*指針類型,const T& left中的const修飾的是left,且left本身存儲的是變量的地址,所以是指針變量本身不能改變。如果寫成第一種const加在Date*前,修飾的是指針指向的內容,跟原函數模版的函數參數類型不同,編譯時會報錯。
- 第二種函數模版的特化才是正確的寫法,const放在*符號之后,表示修飾left存儲變量的地址。
// 函數模板 -- 參數匹配
template<class T>
bool Less(const T& left, const T& right)
{return left < right;
}//1.
template<>
bool Less<Date*>(const Date* & left, const Date* & right)
{return *left < *right;
}//2.
template<>
bool Less<Date*>(Date* const & left, Date* const & right)
{return *left < *right;
}
2.3 類模版特化
類模版的特化分為全特化和偏特化。
2.3.1 全特化
全特化即是將模版參數列表中所有的參數都確定化。其中的格式跟函數模版特化類似
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; }
};void Test1()
{Data<int, double> d1; //走第一個構造函數Data<int, char> d2; //走特化的構造函數
}
運行結果如下:
2.3.2 偏特化
偏特化:任何針對模版參數進一步進行條件限制設計的特化版本。
- 部分特化:將模版參數表中的一部分參數特化。
//特化第二個參數int
template<class T1>
class Data<T1, int>
{
public:Data(){cout << "Data<T1, int> -偏特化" << endl;}
};
- 進一步限制參數類型。偏特化并不僅僅是指特化部分參數,而是針對模版參數更進一步的條件限制所設計出來的一個特化版本。
// 限定模版類型
template<typename T1, typename T2>
class Data<T1*, T2*>
{
public:Data(){cout << "Data<T1*, T2*> -偏特化" << endl;}
};void Test2()
{Data<int, double> d1;Data<int, char> d2;Data<int, int> d3;Data<int*, double*> d4;Data<int**, char*> d5;
}
運行結果如下:
再看下面的代碼,先對第二種偏特化的構造函數做一些修改,其中typeid(T1).name( )是獲取T1的類型名稱,下面一條也是。
// 限定模版類型
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;}
};void Test3()
{Data<int*, double*> d4;Data<int**, char*> d5;
}
運行結果如下,T1和T2分表是原來*符號前的類型,而不是原來的指針類型。
3.模版的編譯分離
3.1 什么是分離編譯
一個程序(項目)有若干個源文件共同實現,而每個源文件單獨編譯生成目標文件,最后將所有目標文件鏈接起來形成單一的可執行文件的過程稱為分離編譯模式。
3.2 模版的分離編譯
下面的代碼是模版函數和普通函數分離編譯的代碼,看看運行的結果如何。建立兩個文件,分別是Func.h和Func.cpp。
- Func.h文件存放函數的聲明。
//Func.h//模版函數
template<class T>
T Add(const T& left, const T& right);//普通函數
void func();
- Func.cpp文件存放函數的定義。
//Func.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;
}//普通函數
void func()
{cout << "void func()" << endl;
}
寫兩個測試函數,運行程序看結果。
//Test.cpp
#include "Func.h"
void test1()
{Add(1, 2); Add(1.0, 2.0);
}void test2()
{func();
}int main()
{test1();test2();return 0;
}
運行test1函數,報連接錯誤,說有無法解析的外部符號。
運行test2函數,結果如下,沒有問題。
普通函數分離編譯可以正常運行,模版函數分離編譯后運行會報鏈接錯誤,這是為什么呢?
- C/C++程序要運行起來,都要經過這幾個步驟:預處理—>編譯—>匯編—>鏈接。
- 在預處理階段,所有的.h文件都要在包含的位置展開。在編譯過程中,將Func.cpp和Test.cpp文件編譯成目標文件,分別是Func.o和Test.o。
- 并且會生成一個符號表,符號表中有函數名經過不同平臺規則的變化成新的名稱,并且會在函數的定義中,存放函數地址,方便鏈接。
- 但是模版函數在定義的部分,不知道要使用什么類型實例化,就不會將Add函數的地址存放在符號表中,那么就會出現上面報的連接錯誤,無法解析的符號。
?
?
3.3 解決方法
有兩種解決方法:
- 將聲明和定義放在同一個.hpp或者.h文件中
- 在模版函數定義的位置顯示實例化。
如下面的代碼所示,不過這種方法很少用。因為當你使用到這個函數其他類型的話,還需要再添加,十分麻煩。
#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);
4. 模版總結
模版的優點:
- 模版復用代碼,節省資源,提高開發效率。
- 增強代碼的靈活性。
缺點:
- 模版會導致代碼膨脹問題,使得編譯時間變長。
- 出現模版編譯錯誤時,錯誤信息非常凌亂,難以定位錯誤進行糾正。
總結
通過這篇文章,對于模版的使用有了更深入的了解,如果還有某些地方不夠熟悉,可以自己動手敲敲代碼。
創作不易,希望這篇文章能給你帶來啟發和幫助,如果喜歡這篇文章,請留下你的三連,你的支持的我最大的動力!!!