在本文我们将学习模版的基础知识点,了解泛型编程。
我们如何实现一个通用的交换函数呢?
我们先看一段代码,如下:
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;
}
//……
从表面看使用函数重载似乎可以实现要求,但是函数重载有一下缺点:
所以C++增加了模板。
模板就类似活字印刷术,如我们需要打印不同颜色的文档,只需通过打印机(模板)填充不同颜色的墨水(类型),就可以获得不同颜色的文档(生成具体类型的代码)。
模板是C++中泛型编程的基础。
百度百科:
- 泛型编程一般指泛型。
- 泛型程序设计是程序设计语言的一种风格或范式。
- 泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型(即编写程序时不确定类型),在实例化时作为参数指明这些类型。
- 注:各种程序设计语言和编译器、运行环境对泛型的支持均不一样。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
一个模板就是一个创建类或函数的蓝图或者说公式。
当我们使用泛型函数时,我们需要提供足够的信息,将蓝图转换为特定的类或函数。这种转换发生在编译时。下面我们将学习怎么定义模板。
一个函数模板就是一个公式,代表了函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
template< typename T1, typename T2,……, typename Tn>//模板参数
返回值类型 函数名(参数列表){}
tip:
代码示例:写一个通用的交换函数
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;
}
因为模板参数T表示的实际在编译时根据Swap的使用情况来确定,所以Swap是一个通用函数模板。
类型参数前必须使用关键字class或typename
template< typename T1,class T2>
tip:
模板参数遵循普通的作用域规则。
一个模板参数名的可用范围是在其声明之后,至模板定义结束之前。
tip:每一个模板定义都以template开始,后跟一个模板参数列表。
问题:这三个Swap的使用,调用的是模板吗?
答案是:不是,当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板的实参。即当我们调用Swap时,编译器使用函数实参的类型来确定绑定到模板参数T的类型。
tip:
用不同类型的参数使用模板时,称为函数模板的实例化。
模板参数实例化分为:隐式实例化和显式实例化。
隐式实例化:让编译器根据实参推演模板参数的实际类型。
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&参数,两个实参是相同类型,编译器可以推演模板参数的实际类型,如果传两个不同类型的实参,编译器还可以推演吗?
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:
模板类型参数与类型转换
:
显式实例化:在函数名后的<>中指定模板参数的实际类型。
使用场景:在某些情况下,编译器无法推断出模板实参的类型。例如:
代码示例1:
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;
}
代码示例2:
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:
总结:当编译器无法推断模板实参的类型时,使用显式实例化。
// 专门处理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;
}
// 专门处理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;
}
思考:在同一个程序中,定义两个栈,分别存储int和double数据,我们怎么实现了?
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类型即可。
存储不同数据的栈,唯一的差异仅仅是类型不同,那能不能和函数模板一样,我们定义一个栈的模板,让编译器去帮我们实例化出对应的类。
类模板是用来生成类的蓝图。
定义类模板:类似函数模板,类模板以关键字template开始,后跟模板参数列表。
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
代码示例:
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:
使用类模板时,我们必须显式实例化——在类模板名字后跟<>,然后将实例化的类型放在<>中即可。
类模板名字不是真正的类,而实例化的结果才是真正的类。
所以类模板的类名与类型不一样——类名:Stack,类型:Stack< T >(如果类模板实例化了就是具体的类型了)
tip:
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;
}
}