目录
在实现多种类型交换时,我们可以使用引用/指针的方式重载多个类型的函数使其进行交换,但是作为一名合格的程序员,这样做很浪费时间。这篇文章我将讲述模板(template)的入门知识,领略c++的魅力。
函数模板
//定义模板
template<class T>
template<typename X>
//多参数
template<class M,class N>
两种方法都可以定义模板,模板名自取且一般为大写。
交换模板
通过调试,每次进入使用了模板的Swap函数体内,并对int和double类型进行交换。
这里存在一个问题,我们调用的是同一个函数吗?
当然不是。
模板之所以称之为模板,是因为它是一个模具,本质还是去分别调用其他重载的函数。每次调用函数就回去推导并实例化对应类型的函数,原理:让编译器干活。
题外话:为什么不能用auto?
因为auto只能在编译阶段推导,而函数调用是在运行是阶段确定的,所以不能用auto
反汇编观察:
c++库里提供了一个swap函数模板,我们以后也可以使用它来进行交换。
两数求和
我们用两数求和实现一个模板来观察一下不同类型求和出现的问题。
template<class X>
X Add(const X& a, const X& b)
{
return a + b;
}
可以发现不同类型传参,导致模板的推导存在歧义(如果推导成int类型却识别到另一个类型不为int)存在问题,下面我们提供两种方式来解决。
隐式很好理解,显示写则是通过指明模板所要推导的类型,使其都转化为int类型,注意在这里这两种方式都产生了临时变量,函数需要加const进行修饰。
尽管都能解决问题,不过注意尽量不要不同类型进行加运算,以免发生精度丢失等问题。
调用顺序问题
有这样一种场景,如果我们写好了一种类型的加法函数和它的模板,编译器会去调用哪个的问题。
显式调用模板操作:
Add<int>(a, b);//<int,int>调用单参
Add<int>(a,c);//<int,double>调用双参,无双参模板报错
Add<int>(c,a);//c强转为int,<int,int>调用单参
Add<int,double>(a,b);//显式调用双参
Add<int,int>(a,b);//显式调双参
模板支持重载
可以看到编译器很聪明,我们写好的int类型的加法会优先调用,这样可以不用再实例化对应的函数了,第二次编译器再两个模板中选择了第二个,这又说明了编译器会调用最匹配的一个模板。
类模板
好比我们要实现一个栈,我用typedef定义一个内部存储int类型的栈,那之后我们每一个对象都只能存int类型,无法做到存double类型,要是再创建一个类又要自己实现大量重复代码,这肯定是不行的。为此,用模板写类很有必要。
重写栈类:
template <class M>
class Vector
{
public:
Vector(int capaicty = 4)
{
_a = new M[capaicty];//
_top = 0;
_capacity = capaicty;
}
M& operator[](int pos)
{
assert(pos < _top);
return _a[pos];
}
void pushback(const M& x);
~Vector()
{
delete[] _a;//开辟的是数组空间
_a = nullptr;
_capacity = _top = 0;
}
private:
M* _a;//
size_t _top;
size_t _capacity;
};
编译器生成不同类型栈
Vector <int> s1;
Vector <double> s2;
与函数模板不同,类模板实例化需要在后面加上<>,括号里为数据类型,编译器根据相应类型调用不同的类,以此构成一个完整的类类型。(普通类的类名是类名+类型)这里的Vector是我们自己取得类模板名字。
类模板声明和定义分离
注意类模板的声明和定义分离有点不一样,它们只能放在同一个文件里,否则会发生链接错误。
我们以~Vector为例来看:
~Vector();//声明
template <class M>//定义
Vector<M>:: ~Vector()
{
delete[] _a;
_a = nullptr;
_capacity = _top = 0;
}
这里要注意定义要在第一行跟上模板的定义且Vector后要加上<M>构成完整类型,一般不推荐将声明和定义分离。
这就是模板初阶的基本内容了,之后我们还会谈进阶模板,不要忘了一键三连啊!