C++的模板是什么?有什么用?如果你想知道问题的答案,那么看这篇博客就对了,在这篇博客中,我们将探讨泛型编程,C++模板的具体内容
目录
模板概念
函数模板
显示实例化与隐式实例化
模板不支持声明和定义分离
类模板
模板概念
在了解泛型编程之前,我们先回顾一下生活常识,就是在质量差不多的情况下,手工产品一般都是比具有规模性统一生产的产品是要贵一些的。因为能规模性生产的东西一般都是有模型或模具的,工厂中的机器只要按照这个模具不停的生产就可以了,而人工不一样,人工的速度哪能比得上机器呢?完成同样的工作,人工就要花费远高于机器的成本,可见这种具有大量重复性的工作,还是得交给机器来完成
这其实是和我们编程类似,比如我们想写一个交换函数swap(),要是考虑到各种类型的话,就先拿内置类型,char, int, double等来说,交换char类型的要写一个,交换int类型的要写一个等等,我们就要实现很多个函数,但这些函数的代码基本都是一样的,只是数据类型不同罢了,这样具有重复性的代码编写,应该交给编译器去编写
void Swap(char& a, char& b)
{
char tmp = a;
a = b;
b = tmp;
}
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
void Swap(double& a, double& b)
{
double tmp = a;
a = b;
b = tmp;
}
//.......很多,剩下的我就不写了
int main()
{
int x = 10, y = 20;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
函数模板
那怎样让编译器去替我们做这样的重复性工作呢?首先我们同样先编写上面这个交换函数的代码,但是这段代码没有数据类型,也并不是说真的没有数据类型,而是我们用一个符号来代替这个数据类型,假设我们使用符号T,那么就可以把上面代码中的char, int, double 全给换成T,然后我们就只管调用就行,至于T具体是什么类型,到时候编译器根据传过来的参数自己推测是什么类型,而这有点像模板一样的东西其实就是C++中的函数模板
当然这个符号T可不是随便就搞出来的,我们得先声明一下,符号T是一个模板符号,具体怎么操作呢? 我们先使用模板的关键字template,用这个模板关键字来创建出一个符号T,这个符号T就是用来替代具体代码中的数据类型
template<typename T>
//注意这里可以写成 template<class T>
//也就是说,typename 和 class 是等价的
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
就这样我们就完成了一个交换函数模板的创建,接下来我们趁热打铁,再把函数模板巩固一下,接下来我们写一个add函数的模板
template<class T>
T Add(T& a, T& b)
{
return a + b;
}
int main()
{
int tmp1 = 10, tmp2 = 20;
double tmp3 = 1.1 ,tmp4 = 2.2;
cout << Add(tmp1, tmp2) << endl;
cout << Add(tmp1, tmp3) << endl;
return 0;
}
这段代码看着没有什么毛病,但是只有第一次调用Add可以编译通过,而第二次调用Add是没有办法编译通过的,这是什么情况导致的呢?
仔细看可以发现,我们第一次调用Add函数传过去的参数是两个int类型的,而第二次调用Add函数传过去的参数一个是int 类型,另一个是double类型,要了解这个原因,我们就要先了解一下模板的是如何进行工作的,我们拿上面的交换函数来说
从上面推演的过程中我们可以得知,假设我们传过去的是int类型的参数,在读取第一个参数时,T就被推演成了int类型,并且T被绑定成int类型
我们在定义add函数时,两个参数使用的都是数据类型T,因此这两个参数的类型应该保持一致,如果第一个参数是int类型,那么T被推演并绑定成int类型,第二个参数是double类型,而T又被推演成double类型,但是T已经是int类型了,这就与之前推演的结果产生冲突,从而导致编译失败
我们要想解决这个问题,可以使用多个模板参数类型,如下代码即可解决
template<typename T1, typename T2>
T1 add(const T1& value_1, const T2& value_2) {
return value_1 + value_2;
}
int main() {
int a = 10;
double b = 10.5;
cout << add(a, b) << endl;
return 0;
}
在上述代码中,你传过去一个int 和 一个double ,T1会被推演并绑定成int,T2会被推演并绑定成double,这样编译就不会产生冲突
但是这样吧,返回值你又该规定成什么类型呢?是返回T1类型,还是返回T2类型?
所以像add这种函数就老老实实使用同类型参数进行操作,不要搞一些没必要的骚操作
从上面的过程中,我们可以体会到,实际上函数模板只是一张蓝图,就像你定义结构体或者定义一个类一样,你不创建一个相关的对象,是没有实体的,函数模板同样如此,你没有进行实例化之前,编译器是不会创建这个函数的
显示实例化与隐式实例化
我们就先了解什么是隐式实例化,其实我们上述过程中,举的各种例子使用的都是隐式实例化,因为我们在使用函数模板时,我们自己没有指定模板参数T的类型,而是靠编译器自己去推演,这个由编译器自行推演,并实例化出一个相关类型的函数就是隐式实例化
那么显示实例化就是我们明确给出了T的类型,不需要你编译器去给我推演,你只要根据我提供的类型,把这个函数给实例化出来就行,我们来看一下显示实例化的使用
显式实例化:在函数名后的<>中指定模板参数的实际类型
template<typename T>
T add(const T& value_1, const T& value_2) {
return value_1 + value_2;
}
int main() {
int a = 10;
int b = 20;
//注意在调用这个函数时,这里我明确给出了T的类型为int
cout << add<int>(a, b) << endl;
return 0;
}
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
template<typename T>
T add(const T& value_1, const T& value_2) {
return value_1 + value_2;
}
int add(int v1, int v2) {
return v1 + v2;
}
int main() {
int a = 10;
int b = 20;
cout << add(a,b) << endl; //这个是调用我们自己定义的add函数
cout << add<int>(a, b) << endl;//这个是调用函数模板推演出的函数
return 0;
}
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
在上面的代码中,函数模板推演出的函数和我们自定义的函数一模一样,那么编译器就会优先调用我们自定义的函数
但是如果我们传过去一个int类型和一个double类型,而函数模板又有两个模板参数类型,那么在调用我们自定义的函数时,需要将double强转成int类型,而使用函数模板,可以将第一个T推演并绑定成int,第二个T推演并绑定成double,这个过程显然是函数模板推演出的函数更好的匹配我们传过去的函数,因此这里会优先调用函数模板推演出的函数
template<typename T1, typename T2>
T1 add(const T1& value_1, const T2& value_2) {
return value_1 + value_2;
}
int add(int v1, int v2) {
return v1 + v2;
}
int main() {
int a = 10;
double b = 20.5;
cout << add(a,b) << endl; //编译器会优先调用函数模板推演出的函数
return 0;
}
模板不支持声明和定义分离
不同于普通的函数,模板是不支持声明和定义分离的,因为模板的实例化过程是在编译的阶段完成的,也就是说在编译阶段,模板的定义和声明必须处于同一个文件中
//假设我们有一个头文件 `template.h`,其中包含一个模板函数的声明:
// template.h
template <typename T>
void foo(T t);
//我们还有一个源文件 `main.cpp`,其中包含了对模板函数的调用:
// main.cpp
#include "template.h"
int main() {
int i = 10;
foo(i);
return 0;
}
//现在,如果我们把模板函数的定义放在一个单独的源文件 `template.cpp` 中:
// template.cpp
template <typename T>
void foo(T t) {
// do something
}
上面这段代码是无法编译通过的,因为编译器在编译main.cpp时,虽然知道该函数模板的声明,但是在main.cpp中没有该函数模板的定义,定义是在template.cpp文件中,所以就无法通过编译
因此我们在编写模板时,通常要求模板的声明和定义放在同一个头文件中
类模板
类模板并不是一个真正的类,只有其被实例化出来才能是一个具体的类,也就是说类模板其实就是类的一个蓝图,但是不同于函数模板,类模板在实例化成一个类时,需要我们使用<>给出明确的参数类型,比如一个栈类,如果我们想将栈中存放int类型的数据,就要给出明确的int类型,stack<int> st; 这样编译器会实例化出一个数据类型为int的栈类,并创建一个该类的对象st
类模板的使用,极大提高了代码的可重复利用率,C++中的stl标准库都是采用类模板的写法,让string,vector,stack,queue等等能够嵌套各种数据类型
类模板的成员函数如果要定义在类外的话,必须要加上关键字template和相应的参数列表
需要注意的时,类模板的成员函数在未使用的情况下是没有相关的代码的
//这里就不包含头文件了
template <typename T>
class test{
public:
void test_fun();
private:
vector<T> v;
};
//成员函数定义在类外,需要这样定义,假设是int类型
void test<int>:: test_fun() {
//do something
}