模板初阶
目录
1. 泛型编程
2. 函数模板
2.1 函数模板概念
2.2 函数模板格式
2.3 函数模板的原理
2.4 函数模板的实例化
2.5 模板参数的匹配原则
3. 类模板
3.1 类模板的定义格式
3.2 类模板的实例化
1. 泛型编程
如何实现一个通用的交换函数呢?
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;
}
//.......
使用函数重载虽然可以实现,但是有以下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对用的函数。
- 代码的可维护性比较低,一个出错可能所有的重载均出错。
那么这个使用我们就可以告诉编译器一个摸具,让编译器根据不同的类型利用该模子来生成代码。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
2. 函数模板
2.1 函数模板概念
函数模板代表一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.2 函数模板格式
template<typename T1,typename T2,.....,typename Tn>
返回值类型 函数名(参数列表){}
#include <iostream>
using namespace std;
//函数模板
//泛型编程
template<typename T>//这里一般写T,因为T是type的缩写
//typename说明T是类型的名称
//template<class T>//那么typename也可以被修改为class。
//typename和class在这个地方没有区别,但是在以后的地方会有区别。
void Swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}
int main()
{int x = 1, y = 0;double m = 1.1, n = 2.2;Swap(x, y);Swap(m, n);cout << "x:" << x << " y:" << y << endl;cout << "m:" << m << " n:" << n << endl;return 0;
}
运行结果:
注意:typename是用来定义模板参数关键字的,也可以使用class(切记:不能使用struct代替class)
这里的交换int类型的Swap函数和交换double类型的Swap函数调用的不是一个函数,函数调用要建立栈帧,这个tmp的大小都不一样,一个是int类型4个字节,另一个是double类型8个字节,所有不可能是调用同一个函数,虽然调试的时候是调用函数模板,但是底层还是调用两个函数,我们还是从汇编层可以看出来的。
第一个掉调了Swap<int>这个函数,后四位是14F6,第二个调用了Swap<double>这个函数,后四位是14E7,这两个函数都是模板生成的,所有我们就得看看模板的原理。
2.3 函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的摸具。所有其实模板就是将本来应该我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
如果实参部分的两个类型不一样的话这里就会报错, 将实参部分的x传递给模板的x,将T推导为int类型,又将n传给函数模板中的y,将y推导为double类型,所有编译器在编译的时候要生成一个形参为double类型,另一个形参为int类型,这样是生成不了的,所有我们必须将模板参数的设置成两个,模板参数可以是一个,也可以是多个。多个模板参数之间用逗号分割。
#include <iostream>
using namespace std;
//函数模板
//泛型编程
template<typename T>//这里一般写T,因为T是type的缩写
//typename说明T是类型的名称
//template<class T>//那么typename也可以被修改为class。
//typename和class在这个地方没有区别,但是在以后的地方会有区别。
void Swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}
//模板参数跟函数参数很相似
//不同的是模板参数用尖括号,函数参数用圆括号
//模板参数前面是关键字,函数参数前面是类型
//模板参数后面是类型,具体什么类型不重要,函数参数后面是变量。
template<class T1, class T2>
void Swap(T1& x, T2& y)
{T1 tmp = x;x = y;y = tmp;
}
int main()
{int x = 1, y = 0;Swap(x, y);double m = 1.1, n = 2.2;Swap(m, n);char a = 'a', b = 'b';Swap(a, b);Swap(x, n);return 0;
}
匹配不上第一个函数模板那么就匹配第二个函数模板,第一个和第二个是可以同时存在的,它们实例化出的函数构成重载。
当然这个里面会存在数据丢失的问题,int给double,double给int这种操作存在数据丢失。 上面的函数模板你用哪些就生成哪些,比如没有用float,那么就不会生成float类型的Swap函数。
模板是在编译的时候生成实例化函数。编译器编译成指令的时候就没有模板这个概念了,就只要函数了,跟直接定义是一样的。
2.4 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显示实例化。
1. 隐式实例化:让编译器根据实参推演模板参数的实际类型
#include <iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
int main()
{int a1 = 10, a2 = 20;double d1 = 10.1, d2 = 20.1;//自动推导类型 - 隐式实例化Add(a1, a2);Add(d1, d2);/*隐式实例化就是根据需要的类型去推导这个T,推出这个T的类型去生成。生成对应的函数。*///Add(a1, d1);/*如上语句,那么模板参数只有一个,但是一个传int,一个传double的时候应该怎么办呢?1.写两个模板参数*///2.强转cout << Add(a1, (int)d1) << endl;cout << Add((double)a1, d1) << endl;//3.显示实例化return 0;
}
2. 显示实例化:在函数名后的<>中指定模板参数的实际类型
#include <iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
int main()
{int a1 = 10, a2 = 20;double d1 = 10.1, d2 = 20.1;//3.不推导 - 显示实例化cout << Add<int>(a1,d1) << endl;cout << Add<double>(a1, d1) << endl;return 0;
}
一般来说不用强转和显示实例化来解决问题,都是写两个模板函数。
那么显示实例化主要解决什么问题呢?
#include <iostream>
using namespace std;
template<class T>
T Add(const T& left,const T& right)
{return left + right;
}
template<class T>
T* Func(size_t n)//这里的参数没有带模板参数T
{return new T[n];
}
int main()
{//以前实参传给形参是直接可以推导T的类型,但是Func这个函数不一样//这个T是什么也不直到,这样的场景就得显示实例化Func<int>(10);// - 显示实例化Func<double>(20);// - 显示实例化return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
2.5 模板参数的匹配原则
1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
#include <iostream>
using namespace std;
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{return (left + right)*5;
}
//这个普通函数和模板是可以同时存在的。
int main()
{cout << Add(1, 2) << endl;//优先调用普通函数,有现成的调用现成的,cout << Add<int>(1, 2) << endl;//就想调模板函数可以显示实例化cout << Add(1.1, 2.2) << endl;//参数不匹配肯定也是调模板函数return 0;
}
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个例子。如果模板函数可以产生一个具有更好匹配的函数,那么将选择模板。
#include <iostream>
using namespace std;
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{return (left + right)*5;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{return (left + right) * 10;
}
int main()
{cout << Add(1, 2) << endl;//调现成的Add函数cout << Add<int>(1, 2) << endl;//显示实例化,调函数模板cout << Add(1.1, 2.2) << endl;//调模板参数是一个的函数模板//实参是同一个类型就会先调用同一个类型,更匹配一些。cout << Add(1, 2.2) << endl;//调模板参数是两个的函数模板//编译器调用的原则是调最匹配的。/*第一个现成的int类型Add函数和第二个显示实例化生成的int类型的Add函数不会发生冲突。因为int类型的Add函数和显示实例化生成的int类型的Add函数有点相似,但是函数名不一样,函数名不一样在允许同时存在。*/return 0;
}
从汇编我们可以看到,显示实例化生成的int类型的Add函数是Add<int> 。
3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
库里面有现成的swap函数,所有我们在写代码的过程中就不需要自己写交换函数了。
#include <iostream>
using namespace std;
int main()
{int a = 1, b = 2;swap(a, b);cout << "a:" << a << endl;cout << "b:" << b << endl;return 0;
}
3. 类模板
3.1 类模板的定义格式
//类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{// 类内成员定义
};
#include<iostream>
using namespace std;//typedef int STDataType;
using STDataType = int;//C++11更喜欢用using替代typedefclass Stack
{
public:Stack(size_t capacity = 4){_array = new STDataType[capacity];_capacity = capacity;_size = 0;}void Push(const STDataType& data);
private:STDataType* _array;size_t _capacity;size_t _size;
};
这样写的话总感觉不用类模板也是可以的,想用哪个类型就using中替换即可,但是并不是这样的。假设我要创建两个栈,一个栈存int类型的数据,另一个栈存double类型的数据,但是这个using或者typedef只能定义一个,解决不了问题。
所有我们还得用模板 - 用这个模板可以生成存放各种类型的栈,具体T是什么就看自己需要这个栈存什么数据了,暂时还是不知道的。
#include<iostream>
using namespace std;
//类模版 - 有的地方叫模板类,是不准确的,主要还是模板,实例化生成类
//而且不能传参实例化,也就是说不能隐式实例化,必须显示实例化。
template<typename T>
class Stack
{
public:Stack(size_t capacity = 4){_array = new T[capacity];_capacity = capacity;_size = 0;}void Push(const T& data);
private:T* _array;size_t _capacity;size_t _size;
};
int main()
{Stack<int> st1;Stack<double> st2;//Stack<int>和Stack<double>不是同一个类型,而是给一个模板实例化成int,生成//一个存放int类型的栈,把里面的T都替换为int。Stack<double>就是把里面的T全部//替换成double,也就是说生成了两个类,return 0;
}
模板不建议声明和定义分离到两个文件.h和.cpp会出现链接错误。如果同一个文件声明和定义可以这样写。
#include<iostream>
using namespace std;
//类模版 - 有的地方叫模板类,是不准确的,主要还是模板,实例化生成类
//而且不能传参实例化,也就是说不能隐式实例化,必须显示实例化。
template<typename T>
class Stack
{
public:Stack(size_t capacity = 4){_array = new T[capacity];_capacity = capacity;_size = 0;}void Push(const T& data);
private:T* _array;size_t _capacity;size_t _size;
};
//以前我们声明和定义是类里面声明,类外面定义,类外面定义指定类域就可以了
//但是在类模板中这样写编不过,
//void Stack::Push(const T& data)
//{
// // 扩容
// _array[_size] = data;
// ++_size;
//}
//因为类模板上面的模板参数是给类模板用的,也就是说只有在类里面可以用T,函数也是一样
//如果想用就需要再次声明模板函数,而且类模板这里不能用类名去代替类型
template <class T>
void Stack<T>::Push(const T& data)
{// 扩容_array[_size] = data;++_size;
}
int main()
{Stack<int> st1;Stack<double> st2;//Stack<int>和Stack<double>不是同一个类型,而是给一个模板实例化成int,生成//一个存放int类型的栈,把里面的T都替换为int。Stack<double>就是把里面的T全部//替换成double,也就是说生成了两个类,return 0;
}
3.2 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是正真的类,而实例化的结果才是正真的类。
int main()
{//Stack是类名,Stack<int>是类型Stack<int> st1;//intStack<double> st2;//doubleStack<char> st3;//charreturn 0;
}