目录
引入
如果要实现一个比较大小的函数,我们会因为类型的不同而写处很多几乎一样函数。这样就会使代码复用率比较低,可维护低。这时候就需要用模板来解决这个问题。
模板是泛型程序设计的基础。(泛型编程:实质上就是不使用具体数据类型(int、double、float等),而是使用一种通用类型来进行程序设计的方法,泛泛的描述一下数据,这个方法可以大规模的减少程序代码的编写量)
模板
函数模板
定义
template<typename T1, typename T2..., typename Tn>
返回类型 函数名(参数列表)
{
//内容;
}
typename是定义模板的关键字,class可以替换typename。<>里的内容被称为模板参数列表。例:
template<class T>
bool judgment(const T& x, const T& y)
{
return x + y;
}
//下面的举例都会用到这个函数
函数模板其实是一个蓝图,它本身并不是函数,是编译器产生特定具体类型函数的模具。 所以其实模板就是将本来应该我们做的重复的事情交给了编译器。在编译阶段,编译器会推导传入参数的类型然后生成该类型的函数(下面的类模板也是一样的)。
实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。提到实例化就不得不说按需实例化,按需实例化是用它的时候才会实例化如果不用它就只会简单检查一下是否有错误并不会实例化。
模板参数实例化分为:隐式实例化和显示实例化。
隐式实例化
让编译器根据实参推演模板参数的实际类型。推演的过程叫参数化。
int main()
{
judgment(1, 2);
return 0;
}
如果函数里的参数类型不同,又因为judgment的模板参数只有一个且编译器不会尝试隐式类型转换。这就会导致编译器不知道是哪个类型就会报错。
显示实例化
在函数名后的<>中指定模板参数的实际类型。
int main()
{
cout << judgment<int>(1, 2);
return 0;
}
如果函数里的参数类型不同,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
匹配原则
- 模板函数和非模板函数可以同名。
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
类模板
定义
template<typename T1, typename T2..., typename Tn>
class 类模板名
{
//内容;
}
typename也是可以被class替换。例:
template<class T1, class T2>
class A
{
public:
A(const T1& a1, const T2& a2)
:_a1(a1)
,_a2(a2)
{}
bool judgment()
{
return _a1 > _a2;
}
private:
T1 _a1;
T2 _a2;
};
实例化
类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可。
类模板名字不是真正的类,而实例化的结果才是真正的类。
int main()
{
A<int> a(1, 1);
cout << a.judgment();
return 0;
}
注意:a的类型是A<int>不是A。
非类型模板参数
模板参数可以是非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。非类型的模板参数对类型只能使整型。例:
template<size_t N>
void A()
{
int* arr = (int*)malloc(sizeof(int) * N);
if (arr == nullptr)
{
perror("malloc");
return;
}
}
注意:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的
- 非类型的模板参数必须在编译期就能确认结果
特化
我们在使用模板时可以解决大部分问题,但是有些问题不能解决。例:
template<class T>
bool judgment(const T& x, const T& y)
{
return x > y;
}
int main()
{
int a = 0, b = 0;
cin >> a >> b;
cout << judgment(&a, &b);
return 0;
}
这是运行结果:
可以发现结果并不对,这时候结果应该输出0为什么呢?因为我们要值判断大小,但上面其实是判断地址的大小。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
函数模板特化
步骤:
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 要和原来的参数列表一样,只是把T换成特化的类型
template<>
bool judgment<int*>(int* const& x, int* const& y)
{
return *x > *y;
}
大家可能注意到了,模板函数里的参数列表并不是const int*& x而是int* const& x为什么呢?
因为:在原参数列表中const修饰的是x,如果只是单纯的替换const在*的左边修饰的是x的内容(*x),要把const放在*的右边。
一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
bool judgment(int* x, int* y)
{
return *x > *y;
}
类模板特化
全特化
全特化即是将模板参数列表中所有的参数都确定化。
template<>
class A<int ,int>
{
public:
A(const int& a1,const int& a2)
:_a1(a1)
,_a2(a2)
{}
bool judgment()
{
return _a1 > _a2;
}
private:
int _a1;
int _a2;
};
上面的类就只能让A <int, int> aa使用。
偏特化
部分特化
将模板参数类表中的一部分参数特化。
template<class T1>
class A<T1 ,int>
{
public:
A(const T1& a1,const int& a2)
:_a1(a1)
,_a2(a2)
{}
bool judgment()
{
return _a1 > _a2;
}
private:
T1 _a1;
int _a2;
};
上面的类就只能让A <T, int> aa使用。
参数进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
template<class T1, class T2>
class A<T1* ,T2*>
{
public:
A(T1* const& a1,T2* const& a2)
:_a1(a1)
,_a2(a2)
{}
bool judgment()
{
return *_a1 > *_a2;
}
private:
T1* _a1;
T2* _a2;
};
上面的类就只能让两个指针的类型使用。这里的*可以替换成&(引用)或其他的。
模板的分离编译
什么是分离编译呢?说白了就是定义和声明分开不在同一个源文件里。
对于模板的话并不是很建议分离和编译分开,尽量把它们放在同一个源文件里。
因为:在编译的时候会生成一个叫符号表它可以把函数的地址给存进去,但是它是模板根据上面的实例化可知它还没有使用所以就并不会实例化所以也就不会存入地址。在链接的时候就会找不到就会报错。(符号表并不只是单单存函数地址,还包括符号查找,类型检查等)