前言
在本文我们将学习模版的基础知识点,了解泛型编程。
一、泛型编程
1、引入
我们如何实现一个通用的交换函数呢?
我们先看一段代码,如下:
cpp
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
int temp = left;
left = right;
right = temp;
}
//......
从表面看使用函数重载似乎可以实现要求,但是函数重载有一下缺点:
- 重载的函数仅仅是参数类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
- 函数重载最麻烦的是,在编写程序的时候,我们就要确定可能要Swap的所有类型。如果希望能在用户提供的类型上使用此函数,这种策略就失效了。
- 代码的可维护性比较低,一个出错可能所有的重载均出错。
所以C++增加了模板 。
模板就类似活字印刷术,如我们需要打印不同颜色的文档,只需通过打印机(模板)填充不同颜色的墨水(类型),就可以获得不同颜色的文档(生成具体类型的代码)。
模板是C++中泛型编程的基础。
2、泛型编程
百度百科:
- 泛型编程一般指泛型。
- 泛型程序设计是程序设计语言的一种风格或范式。
- 泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型(即编写程序时不确定类型),在实例化时作为参数指明这些类型。
- 注:各种程序设计语言和编译器、运行环境对泛型的支持均不一样。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
一个模板就是一个创建类或函数的蓝图或者说公式。
当我们使用泛型函数时,我们需要提供足够的信息 ,将蓝图转换为特定的类或函数 。这种转换发生在编译时。下面我们将学习怎么定义模板。
二、函数模板
1、函数模板的概念
一个函数模板就是一个公式,代表了函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2、函数模板格式
template< typename T1, typename T2,......, typename Tn>//模板参数
返回值类型 函数名(参数列表){}
tip:
- 模板参数以关键字template开始,template翻译成中文就是模板的意思。
- 模板参数列表:模板参数列表是一个逗号分隔的一个或多个模板参数的列表,用<>包围起来。
- 模板参数表示在类或函数定义中用到的类型或值。当使用模板时,我们(隐式地或显式地)指定模板实参,将其绑定到模板参数上。
- 模板参数表示的实际类型在编译时根据模板的使用情况来确定。
- 模板参数针对的是广泛的类型,不是一个具体的类型。
- 注意:在模板定义中,模板参数列表不能为空。
代码示例:写一个通用的交换函数
cpp
template<typename T>//模板参数 ------ 类型
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
int main()
{
double d1 = 2.0;
double d2 = 5.0;
Swap(d1, d2);//使用Swap模板
int i1 = 10;
int i2 = 20;
Swap(i1, i2);
char a = '0';
char b = '9';
Swap(a, b);
return 0;
}
F10调试观察,是否都能完成交换:
因为模板参数T表示的实际在编译时根据Swap的使用情况来确定,所以Swap是一个通用函数模板。
3、模板类型参数&模板参数作用域
(1)模板类型参数
类型参数前必须使用关键字class或typename
template< typename T1,class T2>
tip:
- 在模板参数列表中,这两个关键字的含义相同,可以互相使用。一个模板参数列表中也可以同时使用这两个关键字。
- 一般,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。即类型参数可以用来指定返回类型或函数的参数类型,以及函数体内用于变量声明或类型转换。
- 我们通常将类型参数命名为T(取自type的首字母),但实际上我们可以使用任何名字。
(2)模板参数作用域
模板参数遵循普通的作用域规则。
一个模板参数名的可用范围是在其声明之后,至模板定义结束之前。
tip:每一个模板定义都以template开始,后跟一个模板参数列表。
4、函数模板的原理(实例化函数模板)
问题:这三个Swap的使用,调用的是模板吗?
答案是:不是,当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板的实参。即当我们调用Swap时,编译器使用函数实参的类型来确定绑定到模板参数T的类型。
tip:
- 函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
- 所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
tip:
- 在编译器编译阶段 ,对于模板函数的使用,编译器需要根据传入的实参类型来推演出的模板参数来为我们实例化一个对应类型的函数以供调用。
- 例如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后实例化一份专门处理double类型的代码。
- 用不同类型的参数使用函数模板时, 称为函数模板的实例化。
- 这些编译器生成的对应函数通常被称为模板的实例。
5、函数模板实例化的两种方式
用不同类型的参数使用模板时,称为函数模板的实例化。
模板参数实例化分为:隐式实例化和显式实例化。
(1)隐式实例化
隐式实例化:让编译器根据实参推演模板参数的实际类型。
cpp
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 1.1, d2 = 2.2;
//隐式实例化:让编译器根据实参推演模板参数的实际类型
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
return 0;
}
一个模板类型参数可以用作多个函数形参的类型。
例如:我们的Add函数接收两个const T&参数,两个实参是相同类型,编译器可以推演模板参数的实际类型,如果传两个不同类型的实参,编译器还可以推演吗?
cpp
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 3;
double b = 3.5;
/*
* Add(a, b)不能通过编译,
* 在编译阶段,编译器需要根据传入实参类型推演模板参数
* 通过实参a将T推演为int,通过实参b将T推演为double,但模板参数列表中只有一个T,
* 编译器无法确定此处到底该将T确定为int或者double类型而报错
*
* 简单说,就是模板参数T不明确
*
* 注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背锅
*/
//cout << Add(a, b) << endl;
//解决方案:
//①如果还是希望编译器根据实参推演模板参数,将函数模板定义为两个类型参数
//②用户自己来将实参强制转换
//③显式实例化
return 0;
}
tip:
模板类型参数与类型转换
:- 编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。
- 在模板中,编译器一般不会对实参进行类型转换操作,因为一旦转化出问题,编译器就需要背锅。
- 特殊:将实参传递给带模板类型的函数形参时,能自动应用的类型转换只有const转换(转换为常量)及数组或函数到指针的转换(注:需要函数形参不是引用类型)。
- 如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
(2)显式实例化
显式实例化:在函数名后的<>中指定模板参数的实际类型。
使用场景:在某些情况下,编译器无法推断出模板实参的类型。例如:
- 用户自己控制模板的实例化。
- 函数返回类型与参数列表中任何类型都不相同等等。
代码示例1:
cpp
template<class T1, class T2, class T3>
//编译器无法推断T1,它未出现在函数参数列表
T1 Sum(const T2& a, const T3& b)
{
return a + b;
}
int main()
{
int a = 10;
double b = 9.9;
//T1是显式指定的,T2和T3是从函数实参类型推演而来的
cout << Sum<int>(a, b) << endl;
cout << Sum<double>(a, b) << endl;
return 0;
}
tip:
- 显式模板实参按由左至右的顺序与对应的模板参数匹配。
- 第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配,依此类推。
- 注:只有尾部(最右)参数的显式模板实参才可以忽略,但是前提是它们可以从函数参数推断出来。
代码示例2:
cpp
template<class T>
void func(const T& a, const T& b)
{
cout << a << " " << b << endl;
}
int main()
{
int num1 = 10;
double num2 = 3.14;
//在模板中,编译器一般不会对实参做类型转换,所以传递给func的实参必须是同一类型的,否则编译报错,模板参数T不明确
func<int>(num1, num2);//T被显式指定为int,因此num2被转换为int
func<double>(num1, num2);
return 0;
}
tip:
- 在模板中编译器一般不会对实参做类型转换,因为一旦转化出问题,编译器就需要背锅。
- 对于模板类型参数已经显式指定了的函数实参,可以进行正常的类型转换。
总结:当编译器无法推断模板实参的类型时,使用显式实例化。
6、模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
cpp
// 专门处理int的加法函数
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
cout << "T Add(T left, T right)" << endl;
return left + right;
}
int main()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
return 0;
}
运行结果:
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从模板产生一个实例。如果模板可以产生一个具有更好匹配的函数,那么会选择模板。
cpp
// 专门处理int的加法函数
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
cout << "T1 Add(T1 left, T2 right)" << endl;
return left + right;
}
int main()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
return 0;
}
运行结果:
三、类模板
1、引入
思考:在同一个程序中,定义两个栈,分别存储int和double数据,我们怎么实现了?
cpp
typedef int DataType;
class StackInt
{
public:
StackInt(size_t capacity = 3)
:_array(new DataType[capacity])
, _capacity(capacity)
, _top(0)
{}
// 其他方法...
~StackInt()
{
if (_array)
{
delete[] _array;
_array = NULL;
_capacity = 0;
_top = 0;
}
}
private:
DataType* _array;
int _capacity;
int _top;
};
在C语言中,我们使用typedef重命名栈中存储数据的类型
我们已经有了一个存储int的栈,要想再有一个存储double的栈,我们直接在CV一份栈代码,只需typedef类型即可。
存储不同数据的栈,唯一的差异仅仅是类型不同,那能不能和函数模板一样,我们定义一个栈的模板,让编译器去帮我们实例化出对应的类。
类模板是用来生成类的蓝图。
2、类模板的定义格式
定义类模板:类似函数模板,类模板以关键字template开始,后跟模板参数列表。
cpp
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
代码示例:
cpp
template<class T>
class Stack
{
public:
Stack(size_t capacity = 3)
:_array(new T[capacity])
, _capacity(capacity)
, _top(0)
{}
// 其他方法...
~Stack()
{
if (_array)
{
delete[] _array;
_array = NULL;
_capacity = 0;
_top = 0;
}
}
private:
T* _array;
int _capacity;
int _top;
};
int main()
{
Stack<int> st1;//存储int
Stack<double> st2;//存储double
return 0;
}
tip:
- 与函数模板不一样的是,编译器不能为类模板推断模板参数类型。
- 使用类模板,我们必须显式实例化!
3、类模板的实例化
使用类模板时,我们必须显式实例化 ------在类模板名字后跟<>,然后将实例化的类型放在<>中即可。
类模板名字不是真正的类,而实例化的结果才是真正的类。
所以类模板的类名与类型不一样------类名:Stack,类型:Stack< T >(如果类模板实例化了就是具体的类型了)
tip:
- 一个类模板的每一个实例都形成一个独立的类。
4、类模板的成员函数
- 类模板成员函数的定义
- 与其他任何类一样,我们即可以在类模板内部,也可以在类模板外部为其定义成员函数。
- 定义在类模板内的成员函数被隐式声明为内联函数。
- 一般为了代码的可读性,代码长的定义在外部,代码短的定义在内部。
- 在外部定义时:
- 类模板的每一个实例都有其自己版本的成员函数。因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数必须以关键字template开始,后接类模板参数列表。
- 在类外定义一个成员时,必须说明成员属于哪个类。而且,从一个模板生成的类的名字中必须包含其模板实参。
cpp
template<class T>
class Stack
{
public:
Stack(size_t capacity = 3)
:_array(new T[capacity])
, _capacity(capacity)
, _top(0)
{}
// 其他方法...
//将析构定义在类外
~Stack();
private:
T* _array;
int _capacity;
int _top;
};
template<class T>
Stack<T>::~Stack ()
{
if (_array)
{
delete[] _array;
_array = NULL;
_capacity = 0;
_top = 0;
}
}