C++ 的泛型编程是基于模板实现的,而 C++ 的模板采用的是代码膨胀技术。 例如 std::list 容器,如果你将 int
类型的数据存进去,C++ 编译器就为你生成一个专门用来存 int 类型数据的列表数据结构。
这里我们就需要思考一下模板是什么,我们为什么要使用模板以及怎么去使用模板?
模板是基于用户为模板参数提供的参数在编译时生成普通类型或函数的构造。
这种说法可能有些不太容易理解,这里我们先引入一个东西叫做模具,这个东西相信都不陌生吧
实际上C++模板原理和这个雪糕模具一样,也是通过添加的参数来实例化出一个函数。
这里我们先一起来看一个简单的函数–实现两个数的交换
void Swap(int& x1, int& x2)
{
int tmp = x1;
x2 = x1;
x1 = tmp;
}
这个函数看起来非常的简单,那么现在问题来了,如果说我现在需要实现两个double类型的浮点数进行交换那么我们该如何去处理呢?
从前面的知识我们可以知道,C++是支持函数重载的,可以通过修改参数列表的属性来实现重载Swap函数,没错这样确实可以解决问题,说白了就是再写一个函数将类型修改为double即可。但是有没有想过,如果我又要实现float类型的交换或者更复杂一点的int与folat类型的交换又怎么解决?
还是一味的使用函数重载来实现么?答案当然是否定的,函数重载确实是一个比较好的方法,但是在这种场景下已经不再适用了,甚至会有些弊端:
- 重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错 这个时候模板的作用就体现出来了。
这个时候就可以体现出模板带来的好处
说了这么多,看到这了还是不知道怎么用?别急,
在C++中将模板分为了两大类
- 函数模板
- 类模板
下面我们将一一介绍
所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板(Function
Template)。
template
返回值类型 函数名(参数列表){}
这样看可能有些小伙伴还是看不太明白,我们直接上手定义一个用于上述交换函数的模板:
template<typename T>
void Swap(T &x1, T&x2)
{
T tmp = x1;
x2 = x1;
x1 = tmp;
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
他的原理其实也十分简单,就跟模具一样,在编译过程中,会根据你传递的x1以及x2的类型进行自动推演然后生成对应类型的函数。
说的更具体一点,假定x1与x2都是类型,那么T就会自动推演成int类型,然后生成对应的交换函数:
void Swap(int& x1, int& x2)
{
int tmp = x1;
x2 = x1;
x1 = tmp;
}
怎么证明确实进行了推演呢?实践是检验真理的唯一标准,我们使用下列函数来进行测试:
#include
#include
using namespace std;
template<typename T>
void Swap(T &x1, T&x2)
{
T tmp = x1;
cout << typeid(tmp).name();
x2 = x1;
x1 = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
return 0;
}
因为x1和x2是传递过去的参数,可能有人会钻牛角尖,所以这里我们直接查看用T声明的tmp变量的数据类型
我们直接使用typeid.name查看tmp的数据类型
可以看到这里确实推演出的T为int类型,如果还有小伙伴有疑问的话可以下来自己替换类型进行测试,我这里就不再举例说明了。
前面我们所提到的方法实际上是函数模板的隐式实例化,在编译时自己就推演参数类型了。
但是C++ 没有办法限制类型参数的范围,我们可以使用任意一种类型来实例化模板。但是模板中的语句(函数体或者类体)不一定就能适应所有的类型,可能会有个别的类型没有意义,或者会导致语法错误。
比如说在模板函数内比较值的大小,这对一些基本数据类型有用,但是却不能用来比较结构体、类和数组等数据——我们并没有针对它们进行重载。对于指针来说,比较的是地址大小,而不是指针指向的数据,所以也没有现实意义。总之,我们必须对它们进行单独处理。
模板是一种泛型技术,它能接受的类型是宽泛的、没有限制的,并且对这些类型使用的算法都是一样的(函数体或类体一样)。但是现在我们希望改变这种“游戏规则”,让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的显式具体化(Explicit Specialization)。
具体怎么显式具体化呢?我们来看下面这个代码:
#include
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a = 10;
double b = 11.1;
cout << Add<int>(a, b);
return 0;
}
可以看到不同的显式实例化模板的结果是不一样的,这个可以根据需求自行调整。
这个匹配原则就是一个比较有意思的东西了,不知道有没有小伙伴思考过如果说已经有了一个具体函数和一个函数模板,并且该函数模板可以实例化成该具体函数,那么在调用时系统会调用具体函数还是模板实例化出来的函数呢?我们给出测试代码:
#include
using namespace std;
int Add(const int& left, const int& right)
{
return left + right;
}
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a = 10;
int b = 11;
cout << Add(a, b);
return 0;
}
我们直接进入调试模式看看:(因为不太方便制作动图,我们查看调用堆栈窗口查看具体调用的哪个函数)
这里可以看到调用的是具体的函数而非模板实例化函数。其实这里跟前面的类和对象中的默认构造函数有些类似,自己创建好的东西肯定会被优先调用。
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
我们用它来创建一个简单的类:
template<typename T>
class A
{
public:
T a;
T b;
T func(T a, T b);
};
上述代码中,在类 A 中声明了两个T类型的成员变量 a 和 b,还声明了一个返回值类型为 T 并带两个 T 类型参数的成员函数 func()。
定义了类模板就要使用类模板创建对象以及实现类中的成员函数,这个过程其实也是类模板实例化的过程,实例化出的具体类称为模板类。
如果用类模板创建类的对象,例如,用上述定义的类模板 A 创建对象,则在类模板 A 后面加上一个 <>,并在里面表明相应的类型,示例代码如下所示:
A<int> a;
这样类 A 中凡是用到模板参数的地方都会被int类型替换。如果类模板有多个模板参数,创建对象时,多个类型之间要用逗号分隔开。
使用类模板时,必须要为模板参数显式指定实参,不存在实参推演过程,也就是说不存在将整型值 10 推演为 int 类型再传递给模板参数的过程,必须要在 <>
中指定 int 类型,这一点与函数模板不同。
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟**<>**,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
// A类名,Aint>才是类型
A<int> s1;
A<double> s2;