目錄
前言:
1 非類型模板參數
2 按需實例化
3 模板特化
4 模板的分離編譯
前言:
前面模板我們會了簡單的使用,這里帶來模板的進階,當然,也就那么幾個知識點,并不太難。
1 非類型模板參數
先來看這樣一段代碼:
#define N 100
template<class T>
class Arr
{
public:private:T _arr[N];
};
如果我們想要創建一個整型數組,可以使用這個類來創建,但是我們面臨一個問題就是該數組的大小是固定的,我們想要簡單控制這個數組的大小,可以使用宏,但是還是不夠簡便,因為宏不方便調試不說,實際上也是指定了大小,那么我們想要使用一個類,來創建不同大小的數組該怎么辦?
這里使用到的就是非類型模板參數,如下:
template<class T,size_t N = 100>
class Arr
{
public:private:T _arr[N];
};
int main()
{Arr<int,10> a1;Arr<int,1000> a2;Arr<int> a3;return 0;
}
這里就得到了我們想要的不同大小的數組,那么,來個問題,編譯器一共實現了幾個類?
答:編譯器這里一共實現了3個類,編譯器根據模板參數的不同,就實現了不同的類。這里的非類型模板參數,我們可以理解為常量,如這里的N,但是在C++11只支持整型,連浮點數都不可以,只支持整型,比如int size_t char一類的,在C++ 20之后才可以支持其他類型。
這里涉及到了數組,那么引入一個小的知識點就是對于越界來說,array 數組 vector有著不同的反應:
int main()
{int arr[10];arr[10];arr[15] = 1;return 0;
}
對于普通數組來說,普通的越界只讀來說,比如arr[10]是檢查不來錯誤的,越界寫來說,也是很多抽查不出來的,比如這段代碼在vs2019上就不會報錯。
那么對于array來說:
int main()
{std::array<int,10> array;array[10];return 0;
}
任何讀寫越界都會報錯,但是呢,這是c++委員會后面加的,但是挺雞肋的,因為我們有vector。
int main()
{std::vector<int> v;v.reserve(1000);return 0;
}
vector對越界的讀寫都會報錯,這是一方面,其次是array是靜態的數組,也就是大小定了,并且,它所屬的空間是棧,棧的空間相對堆來說就會小很多,所以面臨開大空間的時候,array就不吃香了,vector沒事,因為可以動態開辟。
2 按需實例化
先看這樣一段代碼:
template<class T,size_t N = 100>
class Arr
{
public:T& operator[](size_t i){size(1);return _arr[i];}size_t size(){return _size;}bool empty(){return _size == 0;}
private:T _arr[N];size_t _size = 0;
};
int main()
{Arr<int> a1;a1.empty();return 0;
}
從語法層面來說,size()函數沒有參數,那么我們傳參數的話就會導致報錯對吧?可是,實際上:
代碼是沒有報錯,也就是說size()傳參數是對的嗎?
不,這是因為按需實例化。
在主函數里面,我們實例化了a1,并且調用了empty函數,但是我們沒有調用operator[]函數,那么編譯器就不會實例化operator函數,因為我們沒有調用,既然沒有實例化size函數,那么傳什么都不會報錯,這就是按需實例化。
再細節一點來說,編譯器會根據模板實例化->實例化一個半成品模板->再實例化為一個具體的類或者函數->最后才是語法編譯,所以沒有語法報錯。
3 模板特化
特化我們可以理解為特殊化處理,比如我們在棧和隊列的時候實現的日期類的比較,就可以不用仿函數來實現比較,可以用特化來處理。
日期類:
class Date
{
public:friend ostream& operator<<(ostream& _cout, const Date& d);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);}
private:size_t _year; size_t _month;size_t _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}
template<class T>
bool less(const T& a,const T& b)
{return a < b;
}
int main()
{Date d1(2024, 5, 20);Date d2(2024, 5, 21);cout << (d1 < d2) << endl;return 0;
}
比如,使用函數模板,對于重載了比較符號的比較是沒問題的,如果使用指針就會報錯,因為默認是按照指針比較的,這里就可以使用特化:
int main()
{Date* p1 = new Date(2020, 1, 1);Date* p2 = new Date(2020, 1, 2);cout << (p1 < p2) << endl;return 0;
}
這段代碼是有問題的是不用多說的,下面是解決方案:
template<class T>
bool Less(T left, T right)
{cout << "bool Less(T left, T right)" << endl;return left < right;
}template<>
bool Less<Date*>(Date* p1,Date* p2)
{return *p1 < *p2;
}int main()
{Date d1(2022, 7, 7);Date d2(2022, 7, 8);Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl;return 0;
}
這里的語法就比較怪了,現在使用的是函數模板,有點像函數重載的感覺,當然,我們也可以直接重載一個出來:
bool Less(Date* left, Date* right)
{return *left < *right;
}
那么,調用是怎么調用的呢?
這里就和半成品,成品是一個道理,重載的函數就相當于成品,特化的函數快成品了,模板就是個半成品,調用的順序也就說的通了。
以上是函數模板的特化,看起來就像是函數重載,接著是類中的模板特化:
//普通
template<class T1,class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }};//全特化
template<>
class Data<int, char>
{
public:Data() { cout << "Data<int, char>" << endl; }
};//偏特化
template<class T1>
class Data<T1, char>
{
public:Data() { cout << "Data<T1, char>" << endl; }
};
使用方式和函數模板其實是差不多的,在特化這里分為全特化和偏特化,特化也不是什么特別的東西,其實就是對參數的進一步限制而已。
4 模板的分離編譯
使用模板的時候,定義和聲明最好放在一個文件,.h和.c文件分離會報錯的,這里簡單舉個例子:
// 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;
}
在調用這個函數的時候就會報錯,只需要想清楚一個簡單的問題就可以了,兩個T是不是一樣的T,能否用.h文件里面的T去平替.cpp里面的T,當然是不可以的,所以這里,就會報錯,報的是鏈接錯誤,.h文件編譯成功后,.cpp里面的文件是沒有編譯好的,因為T不知道是什么類型,調用的時候就會報錯。
在鏈接階段,編譯器按照修飾好之后的函數名在符號表里面尋找函數,不知道類型就沒有生成修飾好的函數名,那么就會報如下類似的錯:
感謝閱讀!