一、泛型编程
如何实现一个通用的交换函数Swap
呢?我们可以通过函数重载来完成。
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;
}
使用函数重载虽然可以实现,但是有一下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?就像浇筑一样:
C++
中存在这样的方法,就是模板。C++
中的模板(Templates)
是泛型编程的核心实现方式,泛型编程(Generic Programming)
是一种编程范式,旨在通过将数据类型参数化,使代码(如类、方法、算法等)
能够在不针对特定数据类型的前提下实现复用,同时保证类型安全。其核心思想是将 类型
作为参数
传递给代码实体,从而让同一逻辑可以适用于多种不同的数据类型,避免为每种类型重复编写相似代码。
二、函数模板
1、函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2、函数模板格式
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
此时我们就可以使用函数模板来实现Swap
函数:
template<typename T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
执行的代码段:
int i = 1, j = 2;
Swap(i, j);
double x = 1.1, y = 2.2;
Swap(x, y);
可以看到它们成功被交换了。
注意:typename
可以被替换成class
,但不能替换成struct
,其中的T
不是固定格式,你可以替换成你想替换的字母或单词,一般习惯用大写。
3、函数模板原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器,相当于你现在是包工头而不是工人了。
4、函数模板的实例化
上面2
中讲的这种是隐式实例化,也就是实参类型推演模板参数,当你像下面这样写时编译器就推演不出参数了:
int i = 0;
double x = 1.1;
Swap(i, x);
一个是int
一个是double
,编译器此时就懵了,会报出以下错误:
为了让讲的例子更合理,我们再写出一个模板Add
来讲解:
template<typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
执行的代码段:
int i = 0;
double x = 1.1;
cout << Add(i, x) << endl;
报错:
处理方案1:强转
隐式实例化:让编译器根据实参推演模板参数的实际类型
// 隐式实例化(实参类型推演模板参数)
cout << Add(i, (int)x) << endl;
cout << Add((double)i, x) << endl;
注意:由于Add
中的参数是引用,强转时产生的临时变量具有常性,所以必须使用const
引用,或者你不用引用,那就不需要加const
。
处理方案2:显示实例化
显式实例化:在函数名后的<>
中指定模板参数的实际类型
//显示实例化(显示指定模板参数)
cout << Add<int>(i, x) << endl;
cout << Add<double>(i, x) << endl;
处理方案3:多加一个typename
template<typename T,typename t>
T Add(T& a, t& b)
{
return a + b;
}
int i = 0;
double x = 1.1;
5、模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
// 与非模板函数匹配,编译器不需要特化
Add(1, 2);
// 虽然有匹配的但可以调用模板生成的
//指定调用编译器特化的Add版本
Add<int>(1, 2);
}
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数,而不会从该模板产生出一个实例。如果模板可以产生一个更加匹配的函数, 那么将选择模板。
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
// 与非函数模板类型完全匹配,
// 不需要函数模板实例化
Add(1, 2);
// 模板函数可以生成更加匹配的版本,
// 编译器根据实参生成更加匹配的Add函数
Add(1, 2.0);
}
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
模板函数的类型推导机制
模板函数在调用时需要通过实参自动推导模板参数。编译器要求实参类型与模板参数类型严格匹配,不进行自动类型转换,除非:
- 模板参数被显式指定(如
func<int>(3.14)
)。 - 使用了
const
、引用或指针类型(如T& 允许int到const int的转换
)。
原因:模板的设计目标是实现完全泛型的代码,类型推导必须精确。如果允许自动转换,可能导致意外行为(如int与double
相加时类型不一致)
普通函数的重载解析与类型转换
普通函数通过重载决议选择最佳匹配函数,并允许隐式类型转换:
- 编译器会根据实参与形参的匹配程度排序候选函数。
- 若存在多个可行函数,会选择转换代价最小的函数。
标准转换的优先级
在同一等级的标准转换中(如int→double和double→int)
,C++
进一步区分:
- 类型提升
(如int→long)
优先于类型转换(如int→double)
。 - 值保留转换
(如int→double)
优先于可能损失值的转换(如double→int)
。
原因:普通函数的重载机制允许为不同类型提供专门实现,而隐式转换是C++
语言的基础特性(如int到double
的转换)。
6、类模板
6.1 类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
我们以Stack
为例写类模板:
template<class T>
class Stack
{
public:
Stack(int n = 4)
:_a(new T[n])
,_top(0)
,_capacity(n)
{ }
void push(T x);
~Stack()
{
delete[] _a;
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
T* _a;
int _top = 0;
int _capacity = 0;
};
这就是类模板,假设我们要在类外面实现里面声明的函数push
时,我们需要这样实现:
template<class T>
void Stack<T>::push(T x)
{
//...
_a[_top++] = x;
}
也需要在函数上面加上template<class T>
,并且要声明在Stack<T>
类域中。
6.2 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化必须显示实例化,需要在类模板名字后跟<>
,然后将实例化的类型放在<>
中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
Stack<int> s1;
s1.push(1);
s1.push(2);
s1.push(3);
Stack<double> s2;
s2.push(1.1);
s2.push(2.2);
s2.push(3.3);
Stack
是类名,Stack<int>
才是类型。
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~