前言: 上篇博客我们学习了C++内存管理的方式,并和C语言的方式进行了对比,本篇博客我们依然是过渡篇,是为了学习后面的STL标准库做准备,本篇博客我们主要了解学习C++泛型编程的一种方式:模板。希望经过本篇博客的学习,大家有所收获!
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
什么意思呢?我们举个例子,假如我们想实现两个数的交换,但是我们又想交换不同类型的数,在C语言当中,我们只能通过定义不同的函数来实现,进入C++呢,以我们现在的知识,只能利用函数重载来实现同一函数名调用不同函数。
如下述一段代码:
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;
}观察上述代码就可以发现,虽然C++的函数重载实现了上述功能,但是代码复用率太低,有没有一种东西可以解决上述问题呢? 模板就来了
概念:函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定 类型版本。
模板参数: 使用 template <typename T> 或 template <class T> 声明。T 是一个占位符类型(称为类型参数),代表在编译时将被实际类型替换掉。可以使用多个类型参数(如 template <typename T, typename U>)
通用函数定义: 在模板声明之后,定义一个函数。在这个函数的参数、返回值或函数体内,使用占位符类型 T 来代替具体的类型。
见下述使用:
template<class T>
template<typename T> //与上述两种方式均可, 模板参数->类型,可以写多个参数根据情况而定
//template<typename T1, typename T2,......,typename Tn>
void Swap(T& x1, T& x2)
{
T x = x1;
x1 = x2;
x2 = x//如果此处; 我们不写是不会报错的,因为如果该函数模板 未被实例化,有些编译器是不会对模板内部进行检查的,,但是会对模板的声明做检查
//即{}或者上面参数部分等描述错误会仍然报错
}注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
如下述使用:
template<class T>
template<typename T>
void Swap(T& x1, T& x2)
{
T x = x1;
x1 = x2;
x2 = x;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
double c = 1.11;
double d = 2.21;
Swap(c, d);
return 0;
}原理如下:
编译时生成: 函数模板本身不是一个可以直接调用的函数。它只是一个蓝图。
按需生成: 当编译器在代码中遇到使用该模板函数的语句时(例如 Swap(a, b); 或 Swap(c, d);),它会:
<...>),推导出模板参数 T 的具体类型(这里是 int 和 double)。
Swap(int a, int b) 或 Swap(double c, double d))。
这个生成的实际函数称为模板函数或函数模板的一个特化/实例。

听了上面的介绍,问个问题上面我们调用的是否是同一个函数? 如果调试的时候我们可以看到,调用的是上述同一个模板,但是是同一个函数吗? 不是。 这里我们不能调用函数模板,调用的是函数模板实例化生成的对应类型的函数,可以调试的时候观察汇编代码。
所以总结一下:函数模板让你只写一次函数逻辑,然后让编译器根据你调用时使用的具体数据类型,自动生成处理该类型数据的实际函数版本。 它是实现“一次编写,处处使用(不同类型)”的关键工具,也就是说事实上该有的函数一个都没有少,只不过这部分函数编译器背后帮我们去解决了,我们只写一个模板就可以了
模板参数实例化分为:隐式实例化和显式实例化。
1️⃣隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);//这种就是模板隐式实例化,也就是说是编译器在背后自己推导出类型的
Add(d1, d2);
//但如果是这种形式
//Add(a1, d1);//编译器就无法处理
// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
return 0;
}2️⃣显式实例化:在函数名后的<>中指定模板参数的实际类型
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
Add(a1, (int)d1);
//或者显式实例化
Add<int>(a1, d1);// 显式实例化:在函数名后的<>中指定模板参数的实际类型
return 0;
}注意:如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
1️⃣情况1 :一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函 数
专门处理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<int>(1, 2);// 调用编译器实例化的Add版本
}2️⃣ 情况2:对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
void main()
{
Add(1, 2);// 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.1);// 编译器会优先选择非模板函数int Add(int, int),而非函数模板。
//也就是说这要非模板函数能匹配成功,但是这是在模板函数参数不匹配的情况下,我们退其次才选择丢失精度的非模板函数
//但如果只要template<class T1, class T2>,Add(T1 left, T2 right),模板能完全匹配的情况下就不会调用非模板函数
//即重载决议优先级:完全匹配(模板) > 需要转换的匹配(非模板),编译器优先选择参数类型完全匹配的模板函数
//编译器会自我判断哪种情况更好
//模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
}对于Add(1,2.1) 解释如下:
1. 函数重载解析规则:
int Add(int, int)要求两个int参数。调用时传入的1(int类型)完全匹配第一个参数,而2.1(double类型)可通过隐式类型转换(double→int)匹配第二个参数。因此非模板函数是可行的。
2. 模板函数的限制:
T Add(T left, T right)要求两个参数类型必须相同(均为T)。
Add(1, 2.1)传入int和double,编译器无法推导出唯一的T(T不能同时满足int和double),导致模板参数推导失败。因此模板函数不被考虑。
背景:如我们C语言当中定义一个栈
typedef int StackDataType;
typedef struct Stack
{
StackDataType* _Array;
int _top;
int _capacity;
}Stack; 这就有一个问题,我们如果有这也的应用场景,一个栈存int,一个栈存double,怎么办是不是必须要定义两个,那代码量是不是陡然提升
在C语言当中是没法解决这也的问题的
所以模板类就可以解决了
template <class T>
class Stack_CPP
{
public:
Stack_CPP()
{}
~Stack_CPP()
{}
void Push(T x)
{}
private:
T* _a;
int _size;
int _capacity;
};
int main()
{
Stack_CPP<int> st_cpp_int;//和类实例化的唯一区别就是在类型后面加<模板类型参数>
st_cpp_int.Push(1);//注意:这里和类实例化没有任何区别,仍然是有两个参数,有个隐含的this指针
st_cpp_int.Push(2);
Stack_CPP<double> st_cpp_double;//模板类就可以轻松解决上述问题
st_cpp_double.Push(1.1);
st_cpp_double.Push(2.1);
}细节注意:
1️⃣类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
2️⃣假设我们定义一个类外面的函数如下:
只要在类外面定义,每个函数前面都要写template<class T>
template<class T>//只要在类外面定义,每个函数前面都要写template<class T>
void Stack_CPP<T>::push(const T& x)//此时表示域不是Stack_CPP::了,而是Stack_CPP<T>::至此我们可以总结一下:C++解决了哪些C语言当中的问题 1. 忘记初始化和销毁 ->构造函数+析构函数 2. 没有封装,谁都可以修改结构体的数据 ->类+访问限定符 3. 如果想同时定义两个栈 ->模板
本篇博客我们了解学习了 C++模板是编写与类型无关的通用代码的泛型编程手段。函数模板能为不同数据类型生成函数实例,通过 template<typename T> 或 template<class T> 声明,使用时编译器按需实例化。类模板用于定义通用类,如模板类实现不同数据类型的栈结构,实例化时需指定类型。模板提升了代码复用性和灵活性,但要注意模板参数匹配原则,如非模板函数与同名模板函数同时存在时,编译器会优先选择匹配度更高的函数。掌握模板使用能有效减少重复代码,提高开发效率。