目录
一.范式编程
二.函数模板
1.概念与格式
2.原理
3.实例化
4.匹配规则
三.类模板
一.范式编程
在写C++函数重载的时候,可能会写很多同一类的函数,例如交换函数:
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
针对不同的类型(int,double,char...)要写相似但不同的函数,如上代码就存在以下问题:
- 重载函数仅仅是类型不同,但只有有新类型出现,就必须增加对应的函数,代码复用率比较低
- 同时代码的可维护性也很低,一个函数出错需要修改,其余的重载也都需要进行修改
为了应对以上问题,模版应运而生。就像活字印刷中的模板一样,根据模板能够印出对应的字,根据颜料的不同,又能印出颜色不同的字,放在C++中也是同理,存在模板这样一个模具,在这个模具中填入不同材料(类型),得到不同的结果,这就是范式编程。
范式编程:编写与类型无关的通用代码,是代码复用的一种手段,模板是范式编程的基础。同时模板又有函数模板和类模板。
二.函数模板
1.概念与格式
函数模板代表了一个函数家族,该函数与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
使用的格式为:
template<class T1,class T2,......>
返回值类型 函数名(参数列表){ 函数主体 }
template就是使用模板的关键字,class可以使用typename替代,但不能使用struct替代,并且class和typename可以混用,可以出现template<class T1,typename T2>的情况,T1,T2...代表不同的类型,可以在函数的实现中使用。
具体到交换函数的函数模板就是这样:
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
可以发现,T可以是int,double,char等不同类型,对该模板传入不同的类型,就会得到不同的特定类型函数,这样就大大增加了代码复用率。
小练习:
其中4,6,7是正确声明,尤其注意7这样的class,typename混用是允许的。
2.原理
函数模板本质而言就是一个蓝图,它本身并不是函数。就像类和对象的区别一样,类是图纸,对象才是实际建造起来的房子。函数模板是编译器产生特定类型具体函数的模具,模板只是将本该我们做的重复事情交给了编译器。
在编译器编译阶段,对于模版函数的使用,编译器会根据传入的实参类型来推导生成对应的特定类型的函数,也就是对于不同类型,编译器会根据模板自动生成专门的函数来使用,注意这里调用的函数不是同一个函数。
3.实例化
当使用不同类型的参数来使用函数模板时,就被称为函数模板的实例化。函数模版的实例化分为隐式实例化和显示实例化。
隐式实例化:让编译器根据实参推导模板参数的实际类型
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.1;
//a1,a2都是int类型,因此编译器自动推导模板中T为int类型
cout << Add(a1, a2) << endl;
//同理,此时推导T为double类型
cout << Add(d1, d2) << endl;
}
然而,也会存在编译器无法推导的情况,例如此时,c1和c2类型不相同,c1为int,c2为double,那么此时编译器应该推导T为int还是double呢?编译器不知道,因此报出了错误。
此时可以使用多参数模板解决,就是在template声明模板时,再加一个类型T2,这样就能进行合适的推导了。当然,也可以使用显示实例化:
显示实例化:直接告诉编译器这个类型T是什么,此时哪怕实参不是类型T,也会走隐式类型转换
例如此时若指定T为int类型,那么double类型的c2就会隐式转换为20,double也是同理
4.匹配规则
- 一个非模板函数可以和另外一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
- 对于非模板函数和同名函数模板,如果其他条件都相同,那么调用时会优先调用非模板函数,从而不会实例化一个模板函数,反之,若模板可以实例化一个更匹配的函数,那么就会选择模板
//非模板函数,适用于int类型相加
int Add(int a, int b)
{
return a + b;
}
//函数模板,适用于相加
template <class T1, class T2 >
T1 Add(T1 a, T2 b)
{
return a + b;
}
int main()
{
Add(10, 10);//调用非模板
Add(10, 10.1);//调用模板
return 0;
}
注意:模板函数不允许自动类型转换,但普通函数是可以进行自动类型转换的。
三.类模板
类模板和函数模板类似,格式为:
template<class T1, class T2 ...>
class 类模板名
{
// 类内成员定义
};
例如声明一个简单的Stack栈的类模板:
template <class T>
class Stack
{
public:
Stack(int capacity = 4)
{
_arr = new T[capacity];
_size = 0;
_capacity = capacity;
}
void Push(const T& data)
{
if (_size == _capacity)
{
int capacity_s = _capacity * 2;
T* tmp = new T[capacity_s];
memcpy(tmp, _arr, sizeof(capacity_s));
_arr = tmp;
_capacity = capacity_s;
}
_arr[_size++] = data;
}
private:
T* _arr;
int _size;
int _capacity;
};
int main()
{
//实例化模板
Stack<int> s1;//int
Stack<double> s2;//double
s1.Push(1);
s1.Push(2);
s2.Push(2.15);
s2.Push(2.16);
}
那么这跟直接实现Stack类比较有什么好处呢?在直接实现的stack中,通常使用typedef数据类型来使用,可这也间接限制了栈储存不同类型数据的情况,若同时需要一个存放int类型一个存放double类型的栈,直接实现就很难了,需要单独去实现两个不同类型的stack,而模板就很好的解决了这一问题,直接传入不同的类型来实例化针对不同数据类型的栈:
// Stack是类名,Stack<int>才是类型
Stack<int> st1; // int类型的栈
Stack<double> st2; // double类型的栈
接下来是一些类模板的练习题,加深对模板的理解:
下面有关C++中为什么用模板类的原因,描述错误的是? ( )
答案:C,模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换
下列关于模板的说法正确的是( )
答案:D,对于A而言,前文在隐式类型转换时给出的Add(c1,c2)就是显然的报错,编译器无法自动推导,此时不能省略;对于B而言,类模板重点在后模板二字,是未实例化的模具,而模板类是根据 类模板 这个模具做出的类,是类模板实例化得到的具体类;对于C而言,template<class T, size_t N>这样的存在是允许的,虚拟类型指的是class T,而size_t N并非虚拟类型
下列描述错误的是( )
答案:D,模板类是一个家族,编译器的处理会分别进行两次编译,其处理过程跟普通类不一样