考虑一个问题:写一个通用的加法程序,如下:
int Add(int left, int right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
……
我们得把每种情况列出来,显然十分麻烦。
C++为我们提供了一种解决方式,叫
泛型编程
(编写与类型无关的逻辑代码,是代码复用的一种手段)。
模板是泛型编程的基础。
模板又分为函数模板与类模板,本文主要来了解函数模板。
一、函数模板
代表了一个函数家族,该函数与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
模板函数的格式 :
template
返回值类型 函数名(参数列表)
{ ... }
eg:
template
T Add(T left, T right)
{
return left + right;
}
这里typename也可以用class,但最好用typename。
二、实例化
上述函数模板构造好,它是如何工作的呢?
如下:
注意:模板会被编译两次
1、编译阶段,检查模板代码本身,查看是否出现语法错误,如:遗漏分号 ,但可能会忽略一些语法错误。
2、在实例化期间,检查模板代码,查看是否所有的调用都有效,如:实例化类型不支持某些函数调用
如果上述程序中left和right类型不同,实例化时,不会自动转化为已有的实例。
eg:
cout << Add(1.1, 2) << endl;
因此我们要如上强制转化或者显性实例化。
三、模板参数
1、模板形参表使用<>括起来
2、和函数参数表一样,跟多个参数时必须用逗号隔开,类型可以相同也可以不相同
3、定义模板函数时模板形参表不能为空
4、模板形参可以是类型形参,也可以是非类型新参,类型形参跟在class和typename后
5、模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型或自定义类型 使用方法完全相同,可用于指定函数形参类型、返回值、局部变量和强制类型转换
6、模板形参表中,class和typename具有相同的含义,可以互换,使用typename更加直观。但关键字typename是作为C++标准加入到C++中的,旧的编译器可能不支持。
(一)类型参数
(1)我们在定义模板时,关于它的形参需要注意它的作用域。
看如下代码:
typedef int T;
template
void test(T t)
{
cout << "Type t=" << typeid(t).name() << endl;
}
T g;
int main()
{
test(1);
cout << "Type g=" << typeid(g).name() << endl;
system("pause");
return 0;
}
运行结果为:
值得思考的是:变量t和g分别是哪个T定义的呢?
模板形参名字只能在模板形参之后到模板声明或定义的末尾之间使用,遵循名字屏蔽规则
因此如图所示:
(2)模板形参的名字在同一模板形参列表中只能使用一次,且每个形参前必须加上关键字typename(class)
eg: template
系统会提示重定义。
(二)非模板类型形参
是模板内部定义的常量,在需要常量表达式的时候,可以使用非模板类型参数。
eg:
template
void FunTest(T(&_array)[N])//此处表示数组的引用
{
}
四、模板函数重载
(1)函数模板重载
模板函数与重载是密切相关的。实际上,从函数模板产生的相关函数都是同名的,因此C++编译系统采用重载的解决方法调用相应函数。
函数模板本身可以用多种方式重载,这需要提供其他函数模板,指定不同参数的相同函数名。
例如,有以下程序:
template //函数模板1
void display(T *arr, int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
cout << *(arr + i) << " ";
}
cout << endl;
}
template
void display(T *arr, int i, int j) //函数模板2
{
int k = 0;
for (k = i; k < j; k++)
{
cout << *(arr + k) << " ";
}
cout << endl;
}
上述程序中有两个同名的函数display,他们的参数不同,构成承载。
结果如下:
函数模板也可以用其他非模板函数重载,例如,将上例程序函数模板二改为:
void display(double *arr, int i, int j) //函数模板2
{
int k = 0;
for (k = i; k < j; k++)
{
cout << *(arr + k) << " ";
}
cout << endl;
}
(2)函数调用的匹配顺序
我们来看一个例子:
template
T add(T x, T y)
{
cout << "模板函数:";
return x + y;
}
int add(int x, int y)
{
cout << "int:";
return x + y;
}
int main()
{
int a = 1;
int b = 2;
double f1 = 1.1;
double f2 = 2.2;
cout << add(1, 2) << endl;
cout << add(f1, f2) << endl;
system("pause");
return 0;
}
如果调用模板函数,会显示字样,观察一下运行结果:
所以我们有必要知道一些规则:
1、一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
2、对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调动非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
3、显式指定一个空的模板实参列表,该语法告诉编译器只有模板才能来匹配这个调用, 而且所有的模板参数都应该根据实参演绎出来。
4、模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
五、模板函数特化
有时候并不总是能够写出对所有可能被实例化的类型都合适的模板,在某些情况下,通用模板定 义对于某个类型可能是完全错误的,或者不能编译,或者做一些错误的事情。
模板特化:就是在实例化模板时,对特定类型的实参进行特殊处理,即实例化一个特殊的实例版本,
当以特化定义时的形参使用模板时,将调用特化版本,模板特化分为全特化和偏特化;
. 函数模板的特化,只能全特化。
模板函数特化形式如下:
1、关键字template后面接一对空的尖括号<>
2、函数名后接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参
3、函数形参表
4、函数体 template<> 返回值 函数名(参数列表)
{
// 函数体
}
我们先来看一下字符串比较的函数模板:
template
int compare( T p1, T p2)
{
if (p1 < p2)
return -1;
else if (p1>p2)
return 1;
return 0;
}
int main()
{
const char *s1 = "world";
const char *s2 = "hello";
cout << compare(s1, s2) << endl;
system("pause");
return 0;
}
应该返回1,看一下结果:
这是为什么呢?它直接比较的是p1,p2的地址。
我们可以用特化解决这种特殊情况:
template
int compare( T p1, T p2)
{
if (p1 < p2)
return -1;
else if (p1>p2)
return 1;
return 0;
}
template<>
int compare(const char *const p1, const char *const p2)
{
return strcmp(p1, p2);
}
int main()
{
const char *s1 = "world";
const char *s2 = "hello";
char *s3 = "world";
char *s4 = "hello";
cout << compare(s1, s2) << endl;
cout << compare(s3, s4) << endl;
system("pause");
return 0;
}
结果:
看以看出与特化版本的参数列表完全匹配会调用特化版本,不匹配的话在编译阶段会通过模板生成所需的函数。