模板
- 函数模板
- 泛型编程
- 基本概念
- 函数模板格式
- 函数模板的实现
- 函数模板的实例化
- 类模板
- 类模板格式
- 类模板的实例化
- 模板参数的匹配原则
- 复数的相加
函数模板
泛型编程
泛型编程是编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
基本概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
例如:实现交换函数时,倘若存在多种类型的数据进行交换:
int main()
{
Swap(1, 2); //整形数据交换
Swap(1.2, 3.4); //浮点型数据交换
Swap('1', '2'); //字符型数据交换
return 0;
}
按照通常情况下,我们需要编写多个不同数据类型的交换函数来实现以上交换功能:
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
void Swap(double& a, double& b)
{
double tmp = a;
a = b;
b = tmp;
}
void Swap(char& a, char& b)
{
char tmp = a;
a = b;
b = tmp;
}
倘若有多个数据类型的数据需要进行数据的交换,我们需要编写多份交换函数,且各个交换函数只有参数类型是不同的,其函数内部实现都是一样的(构成函数重载),这种方法实现下来就会比较繁琐,因此我们考虑能否只编写一个交换函数就能够实现多种类型的数据间交换,故而引入了模板的概念。
函数模板格式
template<typename T1,typename T2,…,typename Tn>
返回值类型 函数名(参数列表){}
typename 是用来定义模板参数的关键字,也可以使用 class,但是不能使用struct
在每一个函数模板之前都需要定义模板类型
参数只涉及单个类型时
引入函数模板的概念之后,我们可以对上述交换函数进行改进:
template<typename T> //只涉及一种数据类型
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
它可以实现多种类型的数据进行交换,测试如下:
参数涉及多个类型时
当函数参数涉及多种数据类型时,我们在函数模板定义中也可以设置多种数据类型:
template<typename T,typename S,class U>
void Print(T data1, S data2, U data3)
{
cout << data1 << " " << data2 << " " << data3 << endl;
}
函数模板的实现
在调用交换函数时,编译器首先需要对调用交换函数的数据类型进行推演,例如上例中在进行交换 a,b 两个数据内容时,编译器首先会推演出 a,b对象的数据类型为 int 类型,然后就会产生专门处理 int 数据类型的交换函数代码,也就是我们在底层看到的 call 调用到了 Swap 函数
函数模板的实例化
使用不同类型的数据来调用函数模板的过程称为函数模板的实例化。
(一)隐式实例化
隐式实例化也就是让编译器来自动推演函数模板的实际类型。
例如:
在下述代码中,在主函数中调用 Add 函数,在传递实参时编译器会自动推演出实参的数据类型,根据实参类型来确定函数模板的类型,并实例化出相应类型的交换函数代码实现实参数据的交换
template<class T>
T Add(T& left, T& right)
{
return left + right;
}
int main()
{
int a = 1, b = 2;
double c = 1.2, d = 3.4;
Add(a, b); //类型推演 int
Add(c, d); //类型推演 double
return 0;
}
而对于要进行相加的数据类型不一致时,推演会出错:
对于这种参数类型不一致的情况下编译器自动类型推演会出错,且编译器不会针对这种情况进行自动的隐式类型转换------------因为会产生歧义:到底是将 a 变量类型转换成与 c 相同的类型,还是将 c 变量类型转换成与 a 相同的类型?
故有两种解决方法,其一是由用户对参数进行类型强转:
int a = 1, b = 2;
double c = 1.2, d = 3.4;
Add((double)a, c); //用户进行强制类型转换
Add(a, (int)c);
另一种方法,即显式实例化。
(二)显式实例化
显式实例化也就是在函数名之后显式声明函数模板中参数的实际类型:
int a = 1, b = 2;
double c = 1.2, d = 3.4;
Add<double>(a, c); //显式声明函数模板类型
Add<int>(a, b);
经过显式定义之后调用的函数模板,在编译阶段就不会再进行类型的推演了
类模板
类模板格式
template<class T1,class T2,…,class Tn>
class 类名{ //类内成员的定义 };
例如,顺序表的实现:
class SeqList {
public:
SeqList(size_t cap = 10) //构造函数
:_arr(nullptr), _capacity(cap == 0 ? 3 : cap), _size(0)
{
_arr new int[10]; //int 类型空间的开辟
}
void PushBack(int data)
{
_arr[_size] = data;
++_size;
}
bool Empty()const
{
_size == 0;
}
void PopBack()
{
if (Empty())
return;
--_size;
}
int Front()
{
return _arr[0];
}
int Back()
{
return _arr[_size - 1];
}
~SeqList() //析构函数
{
if (_arr) {
free(_arr);
_arr = nullptr;
_capacity = _size = 0;
}
}
private:
int* _arr;
size_t _capacity;
size_t _size;
};
上述代码规定了顺序表的数据类型为 int ,倘若要存储其他数据类型,我们应如何应用模板的概念进行修改?
显然,只需要设置类模板,并将类中数据类型更换为类模板类型即可:
template<class T>
class SeqList {
public:
SeqList(size_t cap = 10) //构造函数
:_arr(nullptr), _capacity(cap == 0 ? 3 : cap), _size(0)
{
_arr new T[10]; //动态空间的申请
}
void PushBack(T data)
{
_arr[_size] = data;
++_size;
}
bool Empty()const
{
_size == 0;
}
void PopBack()
{
if (Empty())
return;
--_size;
}
T& Front()
{
return _arr[0];
}
T& Back()
{
return _arr[_size - 1];
}
T& operator[](size_t index) //下标运算符,可以对某位置元素进行修改
{
return _arr[intdex];
}
T& operator[](size_t index)const //下标运算符,不可以对某位置元素进行修改
{
return _arr[intdex];
}
~SeqList() //析构函数
{
if (_arr) {
free(_arr);
_arr = nullptr;
_capacity = _size = 0;
}
}
private:
T* _arr;
size_t _capacity;
size_t _size;
};
测试:
cout << s1[2] << endl; //下标运算符重载
s1[2] = 10; //调用第一个下标运算符重载函数
之前的博客在学习类和对象的时候,曾经说明过类成员函数的定义不仅可以在类内也可以在类外,那么采用类模板时的成员函数如何在类外进行定义?
template<class T>
class SeqList {
public:
SeqList(size_t cap = 10)
:_arr(nullptr), _capacity(cap == 0 ? 3 : cap), _sizeo(0)
{
_arr = new T[10];
}
//类中成员方法不仅可以在类内定义,也可以在类外定义
T& Front();
T& Back();
void PushBack(const T& data)
{
_arr[_size] = data;
_size++;
}
bool Empty()const
{
_size == 0;
}
void PopBack()
{
if (Empty())
return;
--_size;
}
~SeqList()
{
if (_arr) {
delete[] _arr;
_arr = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
T* _arr;
size_t _capacity;
size_t _size;
};
//
/
//类外定义
template<class T>
//每个函数模板实现之前都需要进行模板的定义
T& SeqList<T>::Front()
//返回值类型为模板类型,类外定义成员函数时需要限定作用域
{
return _arr[0];
}
//每个函数模板实现之前都需要进行模板的定义
template<class T>
T& SeqList<T>::Back()
{
return _arr[_size - 1];
}
类模板的实例化
类模板的实例化与函数模板的实例化不同,类模板实例化需要在类模板名字之后跟<>说明实例化的类型,即显式实例化
类模板名字不是真正的类,实例化之后的结果才是真正的类
模板参数的匹配原则
一个函数模板可以与同名的非函数模板 同时存在,并且该函数模板可以被实例化为非函数模板
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int Add(int a, int b)
{
return a + b;
}
对于非模板函数和同名的函数模板,若其他条件相同,在调用时会优先调用非模板函数,如果模板可以产生一个具有更好匹配的函数,则会选择模板
template<class T,class U>
T Add(const T& left, const U& right)
{
return left + right;
}
int Add(int a, int b)
{
return a + b;
}
int main()
{
Add(1, 2); //调用普通函数
Add(1, 2.0); //调用模板--------因为两个参数类型不一致,使用模板匹配度更高
return 0;
}
复数的相加
复数具有实部与虚部,因此在进行相加时候需要分别将实部相加,虚部相加,并返回相加后的结果
//首先定义实现加法的函数模板
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
定义复数类:
class Complex {
public:
Complex(double real, double image) //构造函数
:_real(real), _image(image)
{}
Complex operator+(const Complex& c)const
{
//由于复数具有实部和虚部两部分,因此在进行相加时候需要进行运算符重载
//方法一:
/*Complex ret(_real + c._real, _image + c._image);
return ret;*/ //存在一次值拷贝
//方法二:调用构造函数创建了一个无名对象,对象类型为 Complex,用来保存复数相加的结果,减少一次值拷贝的过程
return Complex(_real + c._real, _image + c._image);
}
private:
double _real;
double _image;
};
由因此此时加法函数实现的是复数类型的数据加法,故而在 Add 内部又会调用复数加法运算符重载函数实现复数相加
ps:
欢迎个人学者评论交流丫~