今天我们来学习一下模板&泛型编程。这次我们会介绍一下几个知识点:
1、泛型编程
2、模板函数&模板形参&函数重载
3、模板类
4、模板特化
5、模板分离编译
首先我们来看一个例子:如何编写一个通用的加法函数?
我们先介绍3种方法:
第1种方法:使用函数重载,针对每一个所需相同行为的不同类型重新实现它。
int Add(const &Left, const &Right)
{
return(Left + Right);
}
这种最为容易想到,实现起来也不难,但是也存在很大的缺陷。
【缺点】
(1) 只要有新类型出现,就要重现添加对应的函数;
(2) 除类型外,所有函数的函数体整体都相同,代码的复用率低;
(3) 如果函数只是返回值类型不同,函数重载不能解决;
(4) 一个方法有问题,所有的方法都有问题,不好维护。
第2种方法:使用公共基类,将共同用的代码放到公共的基础类里边
【缺点】
(1) 借助公共基类来编写通用代码,将失去类型检查的优点;
(2) 对于以后实现的许多类,都必须继承某个特定的类,代码维护更加困难。
第3种方法:使用特殊的预处理程序
#define ADD(a,b) ((a)+(b))
【缺点】
(1) 不是函数,不进行参数类型检测,安全性不高。
以上几种编写一个通用加法函数的方法都存在一些缺点,因给此我们给出了更好的方法
首先我们给出泛型编程和函数模板的定义:
泛型编程:编写与类型无关的逻辑代码,是代码复用的一种手段。模板是泛型编程的基础。
函数模板:代表了一个函数家族,该函数与类型无关,在使用时被参数化,根据实参产生函数的特定类型版本。
template
//返回值类型 函数名(参数列表)
{
...
}
定义模板关键字:
typename 是用来定义模板参数关键字,也可以使用class。但是建议使用typename。
注意:不能使用struct代替typename。
模板函数也可以定义为inline函数
template
inline T Add(const T left, const T right)
{
return (left + right);
}
注意:inline关键字必须放在模板形参表之后,返回值之前,不能放在template之前。模板本身不是类或者函数,编译器用模板产生指定的类或者函数的特定类型版本,产生模板特定类型的过程称为函数模板实例化。
既然函数模板是一个独立于类型的函数,可以产生函数的特定类型版本的东西,那么它怎么样使用呢?
下面我们来看一个例子:
#include
#include
using namespace std;
template
T Add(T left, T right)
{
return left + right;
}
int main()
{
cout << Add(1, 2) << endl;//1
cout << Add('1','2') << endl;//2
cout << Add(1, '2') << endl; //3
cout << Add(1, (int)'2') << endl;//4
system("pause");
return 0;
}
如果代码写成这个样子,那么程序将会报错:
不过这里有个实参推演的概念。
实参推演:从函数实参确定模板形参类型和值的过程称为模板实参推断,但是需要注意的是如果有多个参数,多个类型形参的实参必须完全匹配。否则编译就会出错。
那么程序报错的原因是什么呢?程序显示没有参数列表匹配的函数模板,因为我们的函数名模板实例参数模型为(int ,char)类型的,我们传递了两个不同类型参数给了函数模板,但是函数模板无法确定模板参数T的类型,所以编译报错。
那么我们发现问题后,将代码进行修改,得到以下代码:
#include
#include
using namespace std;
template
T Add(T left, T right)
{
return left + right;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add('1','2') << endl;
cout << Add(1, '2') << endl;
cout << Add(1, (int)'2') << endl;
cout << Add(1.01, 2.03) << endl;
system("pause");
return 0;
}
运行结果如下:
根据运行结果,我们可以画出下图:
在这里我们需要注意的是,函数模板的编译分为两过程(也可以认为模板被编译了两次):
如果这个时候我们在main函数再添加一个函数:
int Add(int left, int right)
{
return left + right;
}
int main()
{
cout << Add(2.1, 3.6) << endl;
system("pause");
return 0;
}
那么在这时候我们会想,这次编译器会去调用哪一个函数呢?是通用类型转化进而调用我们后面定义的Add函数,还是会调用函数模板产生一个新的函数呢?在这里会不会进行类型形参转换呢?
一般不会转换实参以匹配已有的实例化,相反会产生新的实例。
编译器只会执行两种转换:
1、const转换:接收const引用或者const指针的函数可以分别用非const对象的引用或者指针来调用
2、数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当做指向其第一个元素的指针,函数实参当做指向函数类型的指针。
所以上面的问题很容易得出答案了,编译器不会将2.1和3.6转换为int类型,从而调用已有的Add版本,而是重新合成一个double的版本,当然前提是能够生成一个模板函数。如果这个模板函数无法生成的话,那么只能调用已有的版本了。
第一种:
template
T Add(const T &left,const T &right)
{
return left + right;
}
int main()
{
Add(1.2, 3.4);
return 0;
}
面对这样的传参,编译器是可以完成1.2到const double &类型的转换,因为这样做是安全的。
第二种:
template
int sum(T *t)
{
//do something
}
int main()
{
int arr[10];
sum(arr);
return 0;
}
接下来我们来看一下模板参数的概念:
函数模板有两种类型参数:模板参数和调用参数。
关于模板参数,又有以下几个需要注意的地方:
1、模板形参名字只能在模板形参之后到模板声明或定义的末尾之间使用,遵循名字屏蔽规则。
2、模板形参的名字在同一模板形参列表中只能使用一次
3、所有模板形参前面必须加上class或者typename关键字修饰,(需要注意:在函数模板的内部不能指定缺省的模板实参)
除了定义类型参数,还可以在模板中定义非类型参数。接着说一说非模板类型参数的概念。
什么是非模板类型参数呢?非模板类型形参是模板内部定义的常量,一个非类型参数表示一个值而非一个类型。在需要常量表达式的时候,可以使用非模板类型参数。比如我们可以将数组的长度指定为非模板类型参数
模板形参说明:模板函数重载
int Max(const int& left, const int & right)
{ return left>right? left:right;
}
template
T Max(const T& left, const T& right)
{
return left>right? left:right;
}
template
T Max(const T& a, const T& b, const T& c)
{
return Max(Max(a, b), c);
};
int main()
{
Max(10, 20, 30);
Max<>(10, 20); //3.用模板生成,而不是调用显示定义的同类型版本
Max(10, 20);
Max(10, 20.12);
Max(10.0, 20.0); //显示告诉编译器T的类型
Max(10.0, 20.0);
return 0;
}
注意:函数的所有重载版本的声明都应该位于该函数被调用位置之前。现在我们再来看一个例子:
template
int compare(T t1, T t2)
{
if (t1 < t2) return -1;
if (t1 > t2) return 1;
return 0;
}
int main()
{
char *pStr1 = "1234";
char *pStr2 = "abcd";
cout << compare(pStr1, pStr2) << endl;
system("pause");
return 0;
}
程
序运行结果为: 1
正常情况下应该返回 -1;但是最终返回的是1,那么问题来了?为什么会返回1呢?我们打开监视窗口可以看到,在函数调用过时,直接将两个指针变量的地址传给模板函数,在比较时,直接比较的是两个地址的大小,而没有比较指针的内容。
因此上面的函数模板不能用来比较两个字符串,如果传递参数为字符串,返回的则是两个参数地址的比较,不是我们想要的结果。所以,又有了模板函数特化:
我们可以这样来定义:
模板函数特化形式如下:
1、关键字template后面接一对空的尖括号<>
2、函数名后接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参
3、函数形参表
4、函数体
template<>
返回值 函数名(参数列表)
{
...
}
但是需要注意以下几点:
1、特化的声明必须与特定的模板相匹配,否则就会出现错误;
2、在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不匹配,编译器将为实参模板定义中实例化一个实例。
举一个例子:
template <>
int compare(const char * const p1, const char * const p2)
{
return strcmp(p1, p2);
}
int main()
{
const char *pStr1 = "1234";
const char *pStr2 = "abcd";
char *pStr1 = "1234";
char *pStr2 = "abcd";
compare(pStr1, pStr2);
system("pause");
return 0;
}
const char* 与特化版本的参数列表完全匹配,调用了特化版本
char* 与特化版本的形参列表不匹配,在编译期间,通过参数判断,编译器通过模板生成了一个char* 类型的compare() 函数。
注意:特化不能出现在模板实例的调用之后,应该在头文件中包含模板特化的声明,然后使用该特化版本的每个源文件包含该头文件。