这里写目录标题
- 为什么会有模板
- 函数模板
- 如何解决类型不同而导致模板无法实例化的问题
- 类的模板
为什么会有模板
c语言在面对同一个功能不同的类型的数据时得创建出来多个不同名的函数来依次达到目的,比如说我们下面的代码:
#include<stdio.h>
int add1(int x, int y)
{
return x + y;
}
double add2(double x, double y)
{
return x + y;
}
double add3(int x, double y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 10;
double c = 10.0;
double d = 20.0;
printf("%d ", add1(a, b));
printf("%lf ", add2(c, d));
printf("%lf ", add3(a, c));
return 0;
}
这样我们才能正常的执行下面的三个打印函数:
很明显这个时非常的麻烦的,所以我们的c++为了解决上述取名字的问题就提出了函数重载这个概念,当函数参数个数,类型,顺序不同的时候我们就不用给这些函数取不同的名字,直接取成相同的名字就可以了,比如说上面的三个函数的参数类型不同,但是执行的功能确实一样的,那么我们这里就可以给这三个函数取成相同的名字,比如说这样:
int add(int x, int y)
{
return x + y;
}
double add(double x, double y)
{
return x + y;
}
double add(int x, double y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 10;
double c = 10.0;
double d = 20.0;
printf("%d ", add(a, b));
printf("%lf ", add(c, d));
printf("%lf ", add(a, c));
return 0;
}
但是我们通过观察发现,就算我们这里有了函数的重载我们这里依然还是有点点的麻烦啊,当一名cv工程师还是非常的累的,所以既然这里的功能都是一样的,那我们能不能让编译器自己通过我们传的参数和功能的实现原理来自行生成对应的函数呢?答案是可以的,c++给出了一个全新的内容叫模板,这个模板就刻印通过我们给的参数的类型以及提供的实现的功能原理,来自动的生成对应的函数,那么接下来我们就来看看如何写一个函数的模板出来。
函数模板
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。函数模板的格式为:
template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){}
那么这里就可以写一个一个交换数据的Swap函数,那么这个就非常的简单我们可以通过引用很好的实现,比如说我们下面的代码:
#include<iostream>
using namespace std;
void swap(int& x, int& y)
{
int c = x;
x = y;
y = c;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
cout << a << " " << b << endl;
return 0;
}
那么这就说明我们这里的函数实现的是正确的,但是这里有个问题就是这个函数只能交换整型的数据,如果我们想要交换其他类型的数据就只能再写一个函数来实现函数重载,但是这么做的话就又十分的麻烦,所以我们这里就可以采用模板的形式,首先在函数的前面加上这么一句话:
template<typename T>
void swap(int& x, int& y)
{
int c = x;
x = y;
y = c;
}
因为我们这里的函数就只有一个所以我们在写模板的时候就只用写一个typename ,然后这个T就是所谓的类型,编译器会根据你传的参数来推出这个T是什么,如果你传两个double类型的数据过来,那么编译器就会推出这里的T类型为double类型,如果你传过来的是两个int类型,那么编译器就会推出这里的T类型为int类型,所以接下来我们就要这个函数的参数进行修改,将这里的int&改成T&,因为x的类型不同,所以在函数体里面我们也得将int c改成T c,那么我们完整的代码就如下:
template<typename T>
void Swap(T & x, T & y)
{
T c = x;
x = y;
y = c;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
cout << a << " " << b << endl;
double c = 10.1;
double d = 20.2;
Swap(c, d);
cout << c << " " << d << endl;
return 0;
}
我们可以通过下面的代码来看一下这里也是成功的交换两组不同类型的数据:
那么这里有个问题就是我们这里两组数据调用的都是swap函数,那这里有个问题就是他们调用的是同一个函数吗?答案很显然不是的,我们这里调用的swap函数既不是上面的模板,也不是该模板生成的同一个函数,而是该模板生成的两个不同的函数,那么这里我们就可以通过汇编指令来进行验证:
这里我们就可以发现他这里call的两个函数是完全不一样的,所以这里就可以证明出我们调用的是两个不一样的函数,而不是一个函数或者模板,那看到这里想必大家心中一定有一个问题就是如果我们这里传的参数两个类型不一样的话,那又会出现什么样的情况呢?我们这里可以通过下面的代码来进行验证一下:
int main()
{
int a = 10;
double b = 10.1;
Swap(a, b);
cout << a << " " << b << endl;
return 0;
}
然后我们运行一下就可以看到编译器报出了这样的错误:
大致的意思就是我们的编译器无法根据你传的参数来推导出你这个T是什么类型,也就无法根据这个模板生成对应的函数,那么这时有小伙伴说啊,之前没有学模板的时候我们函数在传参时因为强制类型转换所以我们是可以传不同类型的数据的,那这里为什么就不能强制类型转换一下呢?答案很简单首先强制类型转换会产生一个临时常量出来,而我们这里的参数是引用类型并且没有加const修饰,所以会报错,其次强制类型转换发生的过程是在实参传递给形参的过程,而我们这里压根就还没有等到传参的时候,因为类型T无法推出而导致函数无法生成,所以连函数都没有造出来又哪来的传参呢?所以这也就是为什么我们这里没有强制类型转换没有调用函数的原因,那我们该如何解决这个问题呢?我们接着往下看。
如何解决类型不同而导致模板无法实例化的问题
第一个方法:用户自己来强制转化
既然编译器因为函数无法实例化出来而导致无法强制类型转换,那这里我们就可以人为的先对其进行强制类型转换,将这里的double类型的数据转换为int类型,这样就可以让其正常的实例化函数出来,并调用函数进行传参,比如说我们下面的代码:
int main()
{
int a = 10;
double b = 10.1;
Swap(a, (int)b);
cout << a << " " << b << endl;
return 0;
}
但是用这样的方法就会出现一个问题就是,我们这里是交换函数,而使用强制类型转换就必然会导致出现临时变量,而这样的话函数在接收的时候就得加个const出来以防权限放大,但是这样的话就与这个函数的功能相违背,因为这个是交换函数,加了一个const之后我们这里的数据就无法发生更改了,所以互相矛盾了,所以我们就换一个模板再来试试,我们来看看下面的这段函数:
template<typename T>
T Add(T& x, const T& y)
{
return x + y;
}
int main()
{
int a = 10;
double b = 20.1;
cout << Add(a, (int)b) << endl;
return 0;
}
这样我们就可以打印30出来,并且不会报错。
第二个方法:显示实例化
之前我们都是让编译器自己来推参数的类型是什么,其实我们也可以不用编译器自己来推,我们来告诉他就行其形式为:在调用函数的函数名后面加上一个尖括号,在尖括号里面写入模板中所对应的类型,比如说下面的代码:
template<typename T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a = 10;
double b = 20.1;
cout << Add<int>(a, b) << endl;
cout << Add<double>(a, b) << endl;
return 0;
}
那么第一个打印函数里面的ab就都会变成int类型,第二个打印函数里面的ab就都会变成double类型,我们来看看这段代码的运行结果为;
第三个方法:多参模板
我们模板中的参数可以不止一个,所以面对这种不同类型的传参我们就可以使用多惨模板来进行解决,那么这里的形式就如下:
template<typename T1,typename T2>
T2 Add(const T1& x, const T2& y)
{
return x + y;
}
int main()
{
int a = 10;
double b = 20.1;
cout << Add<int,int>(a, b) << endl;
cout << Add<double,double>(a, b) << endl;
cout << Add(a, b) << endl;
return 0;
}
打印的结果就如下:
那么这里就有一个问题,一个具体的函数可以和该函数对应的模板同时存在吗?答案是可以的,比如说我们下面的代码;
template<typename T1,typename T2>
T2 Add(const T1& x, const T2& y)
{
return x + y;
}
int add(int x, int y)
{
return x + y;
}
int main()
{
int x = 10;
int y = 10;
cout << Add(x, y) << endl;
return 0;
}
我们将这段代码运行一下就可以看到这里并没有报错:
但是这里就有一个问题就是,这里调用的函数是我们自己写的那个函数,还是编译器通过模板自己生成的函数呢?那么这里大家要知道的一点就是,编译器也是一个懒人他能不自己生成就不会自己生成,所以我们这里调用的就是我们自己写的那个函数,而不是模板生成的。当然你要是非要编译器生成个与非模板函数一模一样的函数出来也不是不行比如说我们下面的代码:
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
}
int main()
{
Test();
return 0;
}
我们通过汇编指令就可以看到:
这里调用的两个函数地址不一样,那这就说明我们这里调用了两个不同的函数,这也就进一步说明了我们上面说的那个性质。
类的模板
有函数模板的同时就会有类的模板,大家想象一下这个场景,我写了一个栈的数据结构,然后我用栈来存储了一些整型的数据,然后过了一会我发现我又得存储一些浮点型的数据,那这时候你是不是得把之前写的那个栈的数据结构复制一份出来,将里面int全部都改成double啊,当然我们会用typedef来进行简化,但是这依然很麻烦还是得cv一下,我们说当一名cv工程师是非常累的,所以我们这里就有了类的模板,比如说下面的模板:
template<class T>//模板中既可以使用typename也可以使用class
class Vector
{
public :
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
// 使用析构函数演示:在类中声明,在类外定义。
~Vector();
void PushBack(const T& data);
void PopBack();
// ...
size_t Size()
{
return _size;
}
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
跟函数的模板差不多,都是在头上加一个template<class T>
,然后在类中进行对应类型的替换,将原来的一些数据的类型修改成T即可,但是这里有一点不同的就是类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。比如说我们下面的代码:
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;