当我们没有对泛型编程产生了解时,我们写一个交换函数可以是这么写:
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
这段代码确实实现了两个整型数据的交换,但如果我们想要交换的数据不是整型,而是字符型、浮点型、长整型等,那我们必须使用函数重载去手动定义多个函数:
void Swap(char& a, char& b)
{
char tmp = a;
a = b;
b = tmp;
}
void Swap(double& a, double& b)
{
double tmp = a;
a = b;
b = tmp;
}
这样直接的方式确实能够帮助我们完成任务,但是却存在许多问题:
1.这些重载的函数仅仅是类型不同,完成的任务都一模一样。只要涉及到一个新的类型,我们就必须手动写出一个新的函数。
2.如果一个函数出错,那么可能导致一连串的错误。
有没有什么办法能够解决并优化这方面的问题?我们把这个稳定映射到生活当中去:在冶炼的过程中,我们希望能够有绿色和蓝色的成品,但这并不意味着需要单独的为绿色或蓝色的成品从新设计一套制作流程。我们仅仅只需要一个模具,将铁水倒进模具中,在最后一步加上颜料即可。
在C++中,针对同种功能但参数类型不同的函数我们也可以使用模具的办法,我们把它叫做函数模板。而模板又是泛型编程的基础。
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
函数模板的基本格式为:
template <typename T1,typename T2 ......>
返回值 函数名(参数列表)
模板参数可以有多个,因为是模板,所以函数类型的类型不确定,故使用typename关键字(可以使用class替换)。函数模板定义好后,其使用范围在模板定义之后碰到的第一个函数。
template <typename T>
void Swap(T& a, T& b)//合法
{
T tmp = a;
a = b;
b = tmp;
}
//函数模板只对其定义之后的第一个函数生效
T Add(T& a, T& b)//不合法
{
return a + b;
}
我们定义好了交换函数的模板:
template <typename T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
我们可以正常的传参去调用函数:
int main()
{
int a = 1, b = 3;
Swap(a, b);
char c = '5', d = '7';
Swap(c, d);
double e = 2.2, f = 3.4;
Swap(e, f);
return 0;
}
我们要有一个常识,函数模板并不是一个函数,那我们调用的函数是怎么产生的呢?
编译器的功能是十分强大的,它能够帮助我们完成我们看似不可能完成的任务。
当我们定义好了函数模板之后,在以后需要调用此函数的时候,会先经过函数模板,函数模板会智能的将实参的类型转化为具体的模板参数类型,从而生成一份具体的函数。
当然编译器不是“傻子”,它并不是每次调用都会经过函数模板。例如第一次传入的两个类型都为整型,第二次还传入整型,那么第二次调用函数则不会经过函数模板了,而是直接调用第一次生成的函数。
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
隐式实例化,实际上就是让编译器根据实参的类型推演出模板参数的类型。
template <typename T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1, b = 3;
Swap(a, b);
char c = '5', d = '7';
Swap(c, d);
Swap(a, c);//当模板参数只有一个时,不能够这样去使用
//当前一个模板参数被推演成int,后一个模板参数被推演为char
//就会产生矛盾,引发报错
return 0;
}
当模板参数只有一个时,我们的实参类型又有两个。此时解决的方法只有两个:一是将不同的实参类型强转为相同的实参类型;二是使用显式实例化先确定模板参数的类型,即使实参的两个类型不同,在实参传给形参的过程中也会发生强制类型转换。
同时需要注意,强制类型转换之后的结果是一份临时变量,此临时变量具有常属性,所以模板参数需要使用const修饰。
template <typename T>
void Swap(const T& a, const T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1, b = 3;
Swap(a, b);
char c = '5', d = '7';
Swap(c, d);
Swap(a, (int)c);
//或者
Swap((char)a, c);
return 0;
}
显式实例化:在函数名之后使用<>指定模板参数的类型。
template <typename T>
void Swap(const T& a, const T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1, b = 3;
char c = '5', d = '10';
//显示实例化先确定模板参数的类型
//若实参与模板参数的类型不一致则会发生类型转换
Swap<int>(a, c);
Swap<char>(b, d);
return 0;
}
//非模板函数
int Add(int a, int b)
{
return a + b;
}
//同名的模板函数
template <typename T1,typename T2>
T1 Add(T1 a, T2 b)
{
return a + b;
}
int main()
{
Add(1, 3);//直接调用非模板函数
Add<int,int>(5, 8);//此时需要经过模板特化
return 0;
}
//非模板函数
int Add(int a, int b)
{
return a + b;
}
//同名的模板函数
template <typename T1,typename T2>
T1 Add(T1 a, T2 b)
{
return a + b;
}
int main()
{
Add(1, 3);//直接选择非模板函数
Add(5, 3.1);//调用模板函数更加方便
return 0;
}
如果我们不使用类模板,我们需要定义多个用来存储不同元素类型的栈,那将是一件麻烦事:
class Stack_Int
{
//...
public:
void Push(int x);
};
class Stack_Char
{
//...
public:
void Push(char x);
};
//...
所以C++中不仅有函数模板,也有类模板。
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
与函数模板一样,类模板的有效范围在模板定义之后碰到的第一个类中。
我们以栈类举例:
template <typename StackDate>
class Stack
{
public:
Stack(int capacity=4,int top=0)
:_a(new StackDate[capacity])
,_top(top)
,_capacity(capacity)
{}
~Stack()
{
delete[] _a;
_top = _capacity = 0;
}
void Push(StackDate x);
private:
StackDate* _a;
int _top;
int _capacity;
};
//在类模板外定义成员函数,需要指明此函数属于哪个类模板
template <typename StackDate>
void Stack<StackDate>::Push(StackDate x)
{
_a[_top++] = x;
}
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
那么我么想创建对象时,应该这么写:
int main()
{
//实例化之后才是真正的类
Stack<int> s1;
Stack<char> s2;
Stack<double> s3;
return 0;
}
我们上述的代码都是在一个文件下写的,所以在生成可执行文件的时候不涉及链接的问题。
而且我们知道,函数模板和类模板不是一个具体的函数和具体的类,想要使用真正的函数或者真正的类必须对模板实例化。所以,在多文件中,模板的声明与定义分离会造成链接错误。
例如我们在某个头文件中写下这段代码:
template <class T>
T Add(T& a, T& b);
然后在另一个源文件中对其定义:
template <class T>
T Add(T& a, T& b)
{
return a + b;
}
然后我们在主函数中去调用所谓的函数:
int main()
{
int a = 3, b = 4;
Add(a, b);
return 0;
}
此时就会发生报错:
为什么会发生链接错误?我们必须通过编译链接的角度去分析。我们知道,头文件在预处理的过程中会在源文件中展开,所以编译的时候仅仅处理两个文件。
此时编译器看到的只有两个文件:
与函数模板一样,类模板在多文件中声明和定义分离的时候,也会发生链接错误。那么解决方法有两种,一是声明和定义依然分离,在定义的文件当中显式实例化:
template <class T>
T Add(T& a, T& b)
{
return a + b;
}
//显示实例化:告诉编译器有类型
template int Add<int>(int&,int&);
那么这种方法可以确定是一种搓的方法。因为这样已经丧失模板的意义了,我们为何不干脆直接写普通的函数,而写一个模板呢?
所以方法二就是:不要把声明和定义分离。没有别的办法了,即使是语言的短板我们也必须去接收它,没有一种语言是完美的。
对于类模板也是一样,这里随便给一个类,主要是展示类模板的声明和定义分离如何显式实例化:
头文件中的类模板:
template <class T>
class A
{
public:
A(int size = 4);
private:
T* _a;
int _size;
};
源文件中的显示实例化:
template <class T>
A<T>::A(int size)
{
//……
}
//显示实例化
template class A<int>;