1、非类型模板参数
模板参数分为类型参数和非类型参数。
如果要定义一个静态数组
#define N 10
template<class T>
class Array
{
private:
T _a[N];
};
Array<int> a1;
Array<double> a2;
这就是类型模板参数,实例化那里定义了对象参数,模板那里定义类的类型参数。但是这样只能固定大小,不能用同一个类定义不同大小的数组。非类型模板参数定义的是常量。
//#define N 10
template<class T, size_t N>
class Array
{
private:
T _a[N];
};
int main()
{
Array<int, 10> a1;
Array<double, 100> a2;
return 0;
}
非类型模板参数也可以用缺省值,但只能是整型常量。布尔类型也可以。
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
{
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)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
//Array<int, 10> a1;
//Array<double, 100> a2;
//func1(a1);
//func2(a2);
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;
}
结果确实是1 1 0,但是现在我不想这样比较。虽然指针可以调用Less比较,但是还想要单独写出来
template<class T>
bool Less(T left, T right)
{
return left < right;
}
template<class T>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
这就是特化,对于某些类型特殊化处理。模板可以泛型编程,每个类型都可以使用成员函数,但是也可以对某个类型单独拿出来。
特化必须要先有一个基础的函数模板,函数形参要和模板函数的基础参数类型相同。
模板特化是在原生类型不符合需求时单独写一个函数。类模板是在类名后加<类型>。特化对于类模板特化更有意义。
2、偏特化
template<class T>
struct Less
{
bool operator()(const T& l, const T& r)
{
return l < r;
}
};
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less<Date>()(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less<Date*>()(p1, p2) << endl;
int* p3 = new int(1);
int* p4 = new int(2);
cout << Less<int*>()(p3, p4) << endl;
刚才写到是全特化,针对某一类型做特化,除此之外,我们还可以做偏特化。
template<class T>
struct Less
{
bool operator()(const T& l, const T& r)
{
return l < r;
}
};
template<class T>
struct Less<T*>
{
bool operator()(const T* l, const T* r)
{
return *l < *r;
}
};
偏特化并不是在使用一个具体的类型,而是用模板,来代表某些相同特征的类型,比如都是指针,有些泛型编程的意思。如果传自定义类型的指针呢?那就要用这个类型对应的方法了,这就需要程序员自定义。
偏特化可以将模板参数表中的一部分参数特化,比如限定为<T, int>,让模板参数限制为某一类型。两种特化可以共存,哪个合适走哪个,由编译器决定。不过如果限制为引用类型,引用必须初始化,就需要在类里面的函数参数中给缺省值。
3、模板分离编译
在Func.h文件里这样写
template<class T>
T Add(const T& Left, const T& right);
void func();
Func.cpp文件里这样写
template<class T>
T Add(const T& Left, const T& right)
{
return left + right;
}
void func()
{
cout << "func" << endl;
}
Test.cpp里调用Add和Func函数
#include "Func.h"
int main()
{
Add(1, 2);
func();
}
调用Add会出现链接错误,而func则没事。常见的声明和定义分离。类模板不能这样,普通函数可以。
现在有三个文件func.h func.cpp test.cpp,编译器先对它们进行预处理,展开头文件,变成func.i,以及test.i;编译阶段,要开始检查语法,生成汇编代码,func.s test.s;汇编阶段,将汇编代码转换成二进制机器码, func.o test.o;链接阶段,合并生成可执行文件a.out。
从编译阶段到汇编阶段,编译器只会生成func的汇编代码,而没有Add。
jmp指令前面的就是函数地址,push前面的就是函数第一句的地址,编译阶段完成后,编译器把func转换成一堆指令后,就会在func.o也就是汇编阶段完成了函数就有地址了,指令也就是上面这部分。而Add则不能这样,因为Add没有实例化,编译器无法确定T。
在函数调用处,都会有call的指令。Add也可以过编译阶段,因为它有声明,一般有声明就有定义,所以编译器也把它看作合乎规则的,但是最多生成.s文件。到链接之前,只有call指令,两个函数地址也还没有完全生成,到了链接的时候,编译器就会按照修饰后的函数名去找对应的地址,.o文件里面会有符号表去映射对应的地址。符号表里有func函数的地址,就把这个函数链接好了,但没有Add。之前两个cpp文件各自经过处理,互不影响,所以也不知道对方什么情况,test.cpp里有对Add的调用,给了具体的变量类型,但是func.cpp中的Add它不知道自己要变成什么样,所以最后就无法链接,找不到Add的地址。
为了解决这个问题,我们可以显式实例化。
template
int Add<int>(const int& left, const int& right);
但是显式实例化一次只能写一个类型。常用的办法就是不分离,都放在.h文件里,因为头文件会展开。声明和定义放在一起,直接就实例化了,编译时就有地址,链接时不需要去找。
4、模板总结
模板复用了代码,节省资源,更快地迭代开发,C++标准模板库(STL)因此而诞生
增强了代码的灵活性
模板会导致代码膨胀问题,也会导致编译时间变长
出现模板编译错误时,错误信息非常凌乱,不易定位错误
结束。