欢迎来到CILMY23的博客
🏆本篇主题为: 模板魔法:如何用C++模板编写通用代码
🏆个人主页:CILMY23-CSDN博客
🏆系列专栏:Python | C++ | C语言 | 数据结构与算法 | 贪心算法 | Linux
🏆感谢观看,支持的可以给个一键三连,点赞关注+收藏。
✨写在前头:
在阅读这篇之前,我们要先了解一个概念---“泛式编程”
"泛式编程"(通常称作“泛型编程”)是一种软件工程的编程范式,强调在编写算法时使用最为抽象的方式,从而使得算法可以在最宽泛的数据类型上操作。在泛型编程中,算法通常用于数据结构,与存储在其中的数据类型无关。这允许程序员以相同的代码处理各种数据类型,而无需对每个类型重复编写算法。
通用点理解就是有个模具,模具就是泛式编程的具体化(也就是本文的模板),而这种模具,通过给这个模具中填充不同材料(数据类型),来获得不同材料的铸件 (即生成具体类型的代码)
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。C++中,模板是泛型编程的基础。
目录
模板
一、模板的概念
在C语言中,我们写过一个交换函数。
void Swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
但是在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++中,模板是一种特性,它允许程序员编写与类型无关的代码。模板分为两种类型,一种是函数模板,一种是类模板。
函数模板
2.1 函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
当我们希望建立一个类型不固定的函数时,可以使用函数模板。这种函数通过使用泛型来处理不同的数据类型。例如,你可以通过函数模板编写一个排序函数,它可以对整数、浮点数、字符串等进行排序,而无需为每一种类型编写专门的排序代码。
2.2 函数模板的格式
函数模板的格式如下:
//定义单模板参数
template <typename T>
T someFunction(T arg1, T arg2) {
// 函数体
}
//定义多模板参数
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表) {
// 函数体
}
例如:
//单模板参数
//template<class T>
template<typename T>
void Swap(T& left,T& right)
{
T tmp = left;
left = right;
right = tmp;
}
int main()
{
return 0;
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
2.3 函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器,由编译器替我们完成了任务。
例如:
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a1 = 5, a2 = 6;
double d1 = 5.5, d3 = 6.6;
cout << Add(a1, a2) << endl;
cout << Add(d1, d3) << endl;
//不能通过
cout << Add(a1, d1) << endl;
return 0;
}
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此,但是不能传类型不同的实参,在推导的时候就会出现错误。(例如上述的Add(a1, d1),必须调成对应的类型Add((double)a1, d1))
2.4 函数模板实例化
编译通过推出类型,用函数模板,生成对应的函数,这个过程叫做模板实例化
在上述过程中,我们知道了函数模板的原理,解决函数传参存在歧义的问题,有两种实例化,一种是显式实例化,一种是隐式实例化。
☘️☘️显式实例化:在函数名后的<>中指定模板参数的实际类型
int main()
{
int a1 = 5, a2 = 6;
double d1 = 5.5, d3 = 6.6;
cout << Add(a1, a2) << endl;
cout << Add(d1, d3) << endl;
//存在歧义,不能通过
//cout << Add(a1, d1) << endl;
//显式实例化
cout << Add((double)a1, d1) << endl;
cout << Add(a1, (int)d1) << endl;
cout << Add<int>(a1, d1) << endl;
cout << Add<double>(a1, d1) << endl;
return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换(就如第一种显式实例化转换一样Add((double)a1, d1)),如果无法转换成功编译器将会报错。
但是有没有其他情况呢?
如果我们使用多参数来匹配这里的int 和 double也是可以的,就比如下述代码
template<class T1, class T2>
T1 Add(const T1& a, const T2& b)
{
return a + b;
//按类型范围大的进行计算,int 和 double 选double
//返回类型是T1,所以可能发生数据截断
}
int main()
{
int a1 = 5, a2 = 6;
double d1 = 5.5, d3 = 6.6;
cout << Add(a1, d1) << endl;
return 0;
}
不过一般不会这么写,能不用就不用。
☘️☘️隐式实例化:让编译器根据实参推演模板参数的实际类型
其实这里的隐式实例化就是我们写的第一个例子
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a1 = 5, a2 = 6;
double d1 = 5.5, d3 = 6.6;
cout << Add(a1, a2) << endl;
cout << Add(d1, d3) << endl;
return 0;
}
2.5 模板参数的匹配原则
⛵⛵一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数,它们不会产生歧义,会优先调用非模板函数。
例如:
template<class T>
//函数模板
T Add(const T& a, const T& b)
{
return a + b;
}
//非模板函数
int Add(const int& a, const int& b)
{
return a + b;
}
int main()
{
int a1 = 5, a2 = 6;
//使用现成的非模板函数
cout << Add(a1, a2) << endl;
return 0;
}
⛵⛵对于非模板函数和同名函数模板,如果非模板函数不够匹配,而模板可以产生一个具有更好匹配的函数。那么将选择模板。
例如:
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int Add(const int& a, const int& b)
{
return a + b;
}
int main()
{
int a1 = 5, a2 = 6;
double d1 = 5.5, d3 = 6.6;
//优先使用非模板函数
cout << Add(a1, a2) << endl;
//非模板函数不匹配,模板函数生成一个
cout << Add(d1, d3) << endl;
return 0;
}
⛵⛵强制调用模板
例如:
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int Add(const int& a, const int& b)
{
return a + b;
}
int main()
{
int a1 = 5, a2 = 6;
double d1 = 5.5, d3 = 6.6;
//优先使用非模板函数
cout << Add(a1, a2) << endl;
//非模板函数不匹配,模板函数生成一个
cout << Add(d1, d3) << endl;
//强制调用模板
cout << Add<int>(a1, a2) << endl;
return 0;
}
类模板
模板除了可以定义函数模板,还可以定义类模板
3.1 类模板的概念
类模板是C++中的一项功能,用于编写灵活的容器和其他数据结构,它们可以与任意类型的对象一起工作。它们的工作方式与函数模板类似,能够确保代码的泛化和类型独立性。类模板可以简化代码、提高重用性,并且增加程序的可维护性。
3.2 类模板的格式
//单参数模板
template<class T>
class 类模板名
{
// 类内成员定义
};
//多参数模板
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
类模板是通过关键字 template 定义的,后面跟着模板参数列表。
3.3 typedef的缺陷
同样是为了解决类型问题,为什么typedef没能解决?
例如:我们写了一个简单的栈类
typedef int STDataType;
class Stack
{
public:
void Push(const STDataType& x)
{
//....
}
private:
STDataType* _a;
int _capacity;
int _top;
};
int main()
{
return 0;
}
问题的发生主要是因为,我们如果想存一个int数据类型的栈,一个double数据类型的栈,要么我们从类名上入手,要么我们要更改typedef,这样就会使我们的代码量增加,而如果使用模板,我们就能解决这个问题,编译器会匹配对应的模板,生成对应的类。
3.4 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
template <typename T>
class Stack
{
public:
void Push(const T& x)
{
//....
}
private:
T* _a;
int _capacity;
int _top;
};
int main()
{
Stack<int> s1;
//s1 是int类
Stack<double> s2;
//s2 是double类
return 0;
}
3.5 类模板的声明和定义分离
类模板的声明和定义一般都在一个文件,不分离到两个文件中,因为模板是在编译时实例化的,全部的模板定义(包括成员函数的定义)必须对编译器可见。这就意味着不能像非模板类一样简单地将成员函数的定义放到源文件中。
💡总结
1️⃣模板是一种特性,它允许程序员编写与类型无关的代码。
2️⃣函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
3️⃣typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
4️⃣ 编译通过推出类型,用函数模板,生成对应的函数,这个过程叫做模板实例化
5️⃣模板不能声明和定义分离
6️⃣模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
7️⃣类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类
8️⃣之前类型和类名是一样的,现在类型和类名不一样了。
🛎️感谢各位同伴的支持,本期C++就讲解到这啦,如果你觉得写的不错的话,可以给个一键三连,点赞,关注+收藏,若有不足,欢迎各位在评论区讨论。