目录
前言
一、非类型模板参数
定义
二、模板的特化(步骤都一样)
1.概念
2.函数模板特化的步骤
3.类模板的特化
3.1全特化
3.2偏特化/半特化
三、模板的分离与编译
1.什么是分离编译?
2.模板的分离与编译
四、总结
前言
我们已经有浅谈了模板,大概了解什么是泛型编程、函数模板以及类模板的相关内容,同时在前面的几个容器的模拟实现中,我们也是用到大量模板的知识,但是那只是开始,接下来我们来看看模板还有哪些值得我们去了解与体会的地方!
一、非类型模板参数
这里我们需要在回顾一下函数参数与类型模板参数
- 函数参数
函数参数中的参数是对象,形如:
函数(T 对象1,T 对象2);
-
模板参数
模板的参数是类型,也叫类型模板参数,形如:
模板(class 类型1,……);
定义
那么什么是非类型模板参数呢?顾名思义,就是用一个常量作为类(函数)模板中的一个参数,在类(函数)模板中可以将该参数当成常量来使用!
形如:
//类型+常量
template<class T, size_t N = 10>
class A
{
private:
T a[N];//用来定义一个模板类型的静态数组
};
注意:
1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果。
二、模板的特化(步骤都一样)
1.概念
-
通常情况下, 使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 果 ,需要特殊处理!
-
本质就是针对某些特殊类型进行特殊化处理时,可以使用特化,比如Date类型,指针,引用等等
-
需要注意,特化应该要先有一个基础的模板!
2.函数模板特化的步骤
- 必须先有一个基础的模板!
比如:
// 基础的函数模板 -- 参数匹配 template<class T> bool Mgreater(T left, T right) { return left > right; } 如果T是指针呢?用该模板比较会出错,比的只是指针,而不是指针的内容!
-
关键字template后面接一对空的尖括号<>
-
函数名后跟一对尖括号,尖括号中指定需要特化的类型( 这个尖括号可有可无 )
-
函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
//对Mgreater函数的特化 template<> bool Mgreater(Date* left, Date* right) { return *left>*right; } int main() { Date* p1 = new Date(2022, 7, 7); Date* p2 = new Date(2022, 7, 8); cout << Less(p1, p2) << endl; //比较两个日期的大小,而不是指针的大小 }
需要注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给 出。
比如:
//对Mgreater函数的特化 bool Mgreater(Date* left, Date* right) { return *left>*right; } int main() { Date* p1 = new Date(2022, 7, 7); Date* p2 = new Date(2022, 7, 8); cout << Less(p1, p2) << endl; //比较指针的内容(日期大小),而不是指针本身 }
其他特化样例:
//指针 template<class T> bool Mgreater(T* left, T* right) { return *left > *right; }
实际上我们不建议函数模板进行特化,这样看起来很复杂,对于一些复杂的类型,特别给出即可!
3.类模板的特化
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<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; int main() { Data<int,char> a; return 0; }
3.2偏特化/半特化
- 部分特化(将参数列表中的一部分参数进行特化)
//部分特化,部分参数的类型已经定死,char定死了 template<class T> class Data<T,char>// { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; };
- 对参数进行更近一步的限制
//更进一步限制,两种都是不确定的类型,但是又是指定的类型 //T是不确定的,但是T*是确定的,就是只能是任意类型的指针 template<class T1,class T2> class Data<T1*,T2*>// { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; //引用+指针 template<class T1,class T2> class Data<T1&,T2*>// { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; //引用 template<class T1,class T2> class Data<T1&,T2&>// { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; int main() { Data<int&, string*> d7;//调用特化的指针版本 Data<int*,char*> d8;//调用特化的引用指针版本 Data<int&,char&> d9;//调用特化的引用版本 }
三、模板的分离与编译
1.什么是分离编译?
一个程序由若干个源文件共同实现,每个源文件各自进行编译生成目标文件最后将所有目标文件链接起来形成单一的可执行文件的过程!
2.模板的分离与编译
看代码:
// a.h,声明Add函数
template<class T>
T Add(const T& left, const T& right);
// a.cpp,定义Add函数
#include "a.h"
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
//test.cpp,调用Add函数
#include "a.h"
int main()
{
Add(1,2);
return 0;
}
//能否成功编译?
现象:
这是一段链接错误的警告,其实就是调用的地方找不到函数的地址!
原因:在编译时,头文件会展开,因此在调用Add函数的地方才知道T是什么类型的,但是呢在调用的地方只是包含了声明,没有定义,而定义的地方(Add.cpp)却不知道T是什么类型,那就没有办法去实例化出一个具体的代码,那就不会形成地址,那就没有链接的地方,call不了Add,所以会链接报错!
解决方案:
①在定义的位置显示实例化----->不推荐使用,因为一旦换了类型,又得在显示写一个实例化
②声明和定义放在一起--------->推荐使用
为何呢?
因为.h预处理展开后,实例化模板时,既有声明也有定义,直接就实例化,编译时,有函数的定义,直接就有地址,不需要链接时再去找!
所以模板不能最好不要声明和定义分开!
四、总结
【优点】1. 模板复用了代码,节省资源,更快的迭代开发, C++ 的标准模板库 (STL) 因此而产生2. 增强了代码的灵活性【缺陷】1. 模板会导致代码膨胀问题,也会导致编译时间变长2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误
今天的分享就到这里!谢谢你的阅读!