目录
一、认识模板
1.什么是模板
2.模板的分类
二、函数模板
1.泛型和函数模板
2.函数模板的格式
三、类模板
四、实例化
1.隐式实例化
2.显式实例化
3.隐式类型转换
4.模板参数的匹配原则
一、认识模板
1.什么是模板
模板(Template)指C++程序设计设计语言中采用类型作为参数的程序设计,支持通用程序设计。C++ 的标准库提供许多有用的函数大多结合了模板的观念,如STL以及IO Stream。模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。
2.模板的分类
模板分为函数模板和类模板,函数模板针对参数类型不同的函数;类模板针对数据成员和成员函数类型不同的类。
模板支持与了类型无关的代码的编写。
二、函数模板
1.泛型和函数模板
编写与类型无关的通用代码,是代码复用的一种手段。
比如说,我们定义一个交换intl类型元素值的函数。
void swap(int& a, int& b)
{
int temp = 0;
temp = a;
a = b;
b = temp;
}
这个函数可以交换int类型的变量值,但是只能操作int类型。如果我们需要交换double类型的变量,按照C++的逻辑就需要再次定义一个新的重载函数。
void swap(double& a, double& b)
{
double temp = 0;
temp = a;
a = b;
b = temp;
}
实现所有类型变量的交换需要写太多逻辑相同的函数,过于冗余。那么,如果我们不指定这个参数的类型,用一个泛型去替代这些类型,让编译器去推导这个参数的具体类型,那么我们就只需要一个函数定义了。
这样的泛型函数就叫做模板,内部变量的类型叫做泛型。
2.函数模板的格式
template<typename T1, typename T2,......,typename Tn>//typename也可以换成class
返回值类型 函数名 ( 参数列表 )
{}
template<typename T1>//用template关键字定义泛型
void swap(T1& a, T1& b)//内部变量也用泛型
{
T1 temp = 0;
temp = a;
a = b;
b = temp;
}
可以定义多个泛型T1、T2、T3等等,一个泛型只能推导出一个具体类型。T1可以推导为int类型,但是不能又推导为int,又推导为其他类型。
三、类模板
我们定义一个没有完全写好的栈
#define TYPE int
class Stack
{
public:
Stack()
:_a((TYPE*)malloc(sizeof(TYPE)* 4))
,_size(0)
, _volume(0)
{}
void push(TYPE data)
{
_a[_size++] = data;
}
private:
TYPE* _a;
int _size;
int _volume;
};
虽然通过改变TYPE对应的类型可以改变存储数据的类型,但是如果我们想同时使用一个储存int类型元素的栈和一个储存double类型元素的栈,那么还需要定义另一个栈的类,依旧冗余。所以类似于函数,类也可以使用泛型。这种使用泛型的类也叫做类模板,与函数模板相似。
template<typename T1>
class Stack
{
public:
Stack()
:_a((T1*)malloc(sizeof(T1)* 4))
,_size(0)
, _volume(0)
{}
void push(T1 data)
{
_a[_size++] = data;
}
private:
T1* _a;
int _size;
int _volume;
};
四、实例化
1.隐式实例化
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数和类的声明,相当于建筑的图纸,只有使用到这个函数和类时才会定义。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
简单说就是本来我们应该多去写的Swap函数和Stack类的重复工作去给编译器做了。
这是我们之前的函数模板
template<typename T1>//用template关键字定义泛型
void swap(T1& a, T1& b)//内部变量也用泛型
{
T1 temp = 0;
temp = a;
a = b;
b = temp;
}
对于不同类型的变量进行swap,当编译器遇到符合模板但没有定义的函数,编译器就会自己生成一个对应的重载函数,如果这个函数之前定义过那就使用之前定义过的函数。
template<typename T1>
void swap(T1& a, T1& b)
{
T1 temp = 0;
temp = a;
a = b;
b = temp;
}
int main()
{
int a = 1;
int b = 2;
swap(a,b);
//交换两个int变量a和b,编译器会根据模板定义一个int类型的交换函数,如下:
//void swap(int& a, int& b)
double c = 1.0;
double d = 2.0;
swap(c,d);
//交换两个double变量c和d,编译器会根据模板再次定义一个double类型的交换函数,如下:
//void swap(double& a, double& b)
int e = 1;
int f = 2;
swap(e,f);
//再次交换两个int变量e和f,编译器会继续使用之前定义的int类型交换函数
//void swap(int& a, int& b)
return 0;
}
最后的结果还是定义了很多不同类型的重载函数,与我们自己定义许多不同类型的函数的结果是一样的。但是这时不再需要程序员去一个一个类型定义函数,大大简化了程序的代码量。
模板对于类也是一样的效果,也会自己定义许多不同类型的类去使用,但是类只支持显式实例化。
2.显式实例化
我们不光可以让计算机自己推断类型,而且也可以自己指定生成哪一种类型参数的函数和类。
template<typename T1>//定义泛型T1,在这里也表示下面的函数中会用到T1
void Swap(T1& a, T1& b)
{
T1 temp = 0;
temp = a;
a = b;
b = temp;
}
template<typename T1, typename T2>//定义泛型T2,T1在前面已经定义了表示会用到T1和T2
void print(T1& a, T2& b)//注意,这个T1经过推导只能对应一个类型,不能在上面的函数中是int下面的是double
{
cout << a << ' ' << b << endl;
}
int main()
{
int a = 1;
int b = 2;
double c = 1.0;
Swap<int>(a, b);//在函数后面加<类型>就可以指定定义和使用对应类型的函数
print<int, double>(a, c);//<>内部多个类型用,隔开对应上面的template
cout << a << ' ' << b << endl;
return 0;
}
//输出:
//2 1
//2 1
之前说过,类只支持显式实例化,不能通过编译器推断。类模板也是一个声明或者说是一个蓝图,只有使用了这个类型的函数或者类才会被定义。
template<typename T>
class Stack
{
public:
Stack()
:_a((T*)malloc(sizeof(T)* 4))
, _size(0)
, _volume(0)
{}
void push(T data)
{
_a[_size++] = data;
}
private:
T* _a;
int _size;
int _volume;
};
int main()
{
Stack<int> s1;//定义一个储存int类型的类,类型为Stack<int>
Stack<double> s2;//定义一个储存double类型的类,类型为Stack<double>
s1.push(1);//int类型栈可以插入数据
s2.push(1.0);//double类型栈也可以插入数据
return 0;
}
3.隐式类型转换
我们可以清楚地看到T会推导出两个变量类型int和double,此时T还是会推导为int,但是b参数会隐式转化为int,我们知道这个转换的临时参数是有常性的,所以需要用const修饰。
template<typename T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a = 10;
double b = 20.2;
cout << Add<int>(a, b) << endl;//调用int类型Add函数,b生成一个临时int常量传递给函数
return 0;
}
在不能完成隐式类型转换时,程序就会报错。
4.模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换