一. 前言
在我们学习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++中,也能够存在这样一个模具,通过给这个模具填充不同颜色的材料(类型),从而得到形状相同但颜色不同的月饼(生成具体类型的代码),那将会大大减少代码的冗余。巧的是前人早已将树栽好,我们只需在此乘凉。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础
三. C++模板的分类
三. 函数模板
函数模板概念
程序员定义一种形式的函数,这种函数可以用不同的数据类型操作,但具有相同的功能逻辑。通过使用函数模板,可以大大减少代码的重复,并提高代码的复用性和灵活性。
函数模板定义格式
template <typename Type>
ReturnType FunctionName(Parameters) {
// 函数体
}
//template <typename Type> 是模板声明,指出这是一个模板,并定义了一个类型参数 Type。typename 可以替换为 class,两者在这里作用相同。
//ReturnType 是函数返回的数据类型,它可以依赖于模板类型。
//FunctionName 是函数的名称。
//Parameters 是函数接受的参数,这些参数的类型可以是模板类型 Type。
示例
代码实例:
template<typename T>
void Swap(T& left, T& right)
{
{
T temp = left;
left = right;
right = temp;
}
}
int main()
{
int LeftI = 2;
int RightI = 3;
cout << LeftI << RightI << endl;
Swap(LeftI,RightI);
cout << LeftI << RightI << endl;
double LeftD = 2.5;
double RightD = 3.5;
cout << LeftD << RightD << endl;
Swap(LeftD, RightD);
cout << LeftD << RightD << endl;
char LeftC = 'A';
char RightC = 'B';
cout << LeftC << RightC << endl;
Swap(LeftC, RightC);
cout << LeftC << RightC << endl;
return 0;
}
我们通过这个函数模版,分别传入不同数据类型的参数,通过结果的观察可以发现这个函数模版可以根据不同的类型去做一个自动推导,继而去起到一个交换的功能。
函数模板的原理
请问上述真的是调用一个函数吗?
当然不是,这里我们三次Swap不是调用同一个函数,这里可以通过反汇编观察到:Swap根据不同的类型通过模板定制出专属的类型的函数,然后再调用
可以发现,在进行汇编代码查看的时候,被调用的函数模版生成了三个不同的函数,它们有着不同的函数地址
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然后产生一份专门处理int类型的代码,对于double,字符类型也是如此
对于一些常用的函数如swap,库里面已经定义好了模板,我们只需要直接用即可
函数模板的实例化
用不同类型的参数使用模板时,称为模板的实例化。模板实例化分为隐式实例化和显示实例化。
隐式实例化
隐式实例化发生在模板代码被实际使用时。编译器根据模板使用时提供的具体类型参数自动生成模板的一个实例。这意味着编译器在遇到模板函数或类模板的特定类型的调用或声明时,自动生成必要的代码。
例如,给定一个模板函数:
template <typename T>
void Swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
当你调用这个函数:
int x = 1, y = 2;
Swap(x, y); // 编译器将隐式地为int类型实例化Swap模板
编译器自动为 int
类型参数创建 Swap
函数的实例,这个过程称为隐式实例化。
显式实例化
显式实例化是你明确告诉编译器为特定类型生成模板的实例,而不是让编译器根据需要来决定。通过显式实例化,编译器会在实例化的地方生成模板的代码,而不管模板是否会被用到,这常用于减少编译时间和控制模板的实例化位置。
//在函数名后的<>中指定模板参数的实际类型
template void Swap<int>(int&, int&); // 显式地实例化int类型的Swap模板
这段代码不调用 Swap
函数,但它告诉编译器生成处理 int
类型的 Swap
函数的代码。
显式实例化确保模板只被实例化一次,这对于减少编译时间和解决链接问题是有帮助的,尤其是在大型项目中或者模板定义与实例化在不同的编译单元时。
总结来说,隐式实例化基于代码中的使用自动进行,而显式实例化基于程序员的指示进行。
例如,还是Swap模板函数:
template <typename T>
void Swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
调用Swap函数
int a1 = 10, a2 = 20;
double b1 = 10.0, b2 = 20.0;
//显示实例化
Swap<int>(a1,a2);
Swap<double>(b1,b2);
Swap<double>(a1,b2);//使用显示实例化时,如果传入的参数类型与模板参数类型不匹配,
//编译器会尝试进行隐式类型转换,显式指定T为double,int1将隐式转换为double.
//如果无法转换成功,则编译器将会报错。
外传 注意:以下使用情况可能一不小心就会出错
template<class T> T Add(const T& left, const T& right) { return left + right; } int main() { int int1 = 10, int2 = 20; double double1 = 10.0, double2 = 20.0; Add(int1, double2); //err 编译器推不出来 //该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型 //通过实参int1将T推演为int,通过实参double2将T推演为double类型,但模板参数列表中只有 //一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错 //注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅 }
在函数模板调用时,每个模板参数都必须是明确的,编译器将尝试从每个实参推导出对应的模板参数类型。在这个例子中,编译器将尝试通过
int1
推导出T
是int
类型,同时又通过double2
推导出T
是double
类型。显然,这两种类型是冲突的,因为模板参数列表中只有一个T
,并且在C++模板中,编译器通常不会自动进行类型转换来匹配模板参数。解决方法:
方法1: 使用相同类型的参数
确保调用
Add
函数时使用相同类型的参数:int main() { int int1 = 10, int2 = 20; Add(int1, int2); // 正确,T被推导为int double double1 = 10.0, double2 = 20.0; Add(double1, double2); // 正确,T被推导为double }
方法2: 显式指定模板参数
显式指定模板参数
T
的类型,这将导致非模板参数类型的隐式转换:int main() { int int1 = 10; double double2 = 20.0; Add<double>(int1, double2); // 显式指定T为double,int1将隐式转换为double }
方法3: 修改函数模板以接受两个不同的类型参数
修改
Add
函数模板以接受两个不同的类型参数,并明确指定返回类型:template<class T1, class T2> auto Add(const T1& left, const T2& right) -> decltype(left + right) { return left + right; } int main() { int int1 = 10; double double2 = 20.0; auto result = Add(int1, double2); // result的类型将是T1和T2之和的类型,即double }
模板参数的匹配原则
一. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
#include <iostream>
using namespace std;
//专门用于int类型加法的非模板函数
int Add(const int& x, const int& y)
{
return x + y;
}
//通用类型加法的函数模板
template<typename T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a = 10, b = 20;
int c = Add(a, b); //调用非模板函数,编译器不需要实例化
int d = Add<int>(a, b); //调用编译器实例化的Add函数
return 0;
}
二. 对于非模板函数和同名的函数模板,如果其他条件都相同,在调用时会优先调用非模板函数,而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么选择模板
//专门用于int类型加法的非模板函数
int Add(const int& x, const int& y)
{
return x + y;
}
//通用类型加法的函数模板
template<typename T1, typename T2>//这里传了两个模板,所以能传入两种不同类型的参数
T1 Add(const T1& x, const T2& y)
{
return x + y;
}
int main()
{
int a = Add(10, 20); //与非模板函数完全匹配,不需要函数模板实例化
int b = Add(2.2, 2); //函数模板可以生成更加匹配的版本,编译器会根据实参生成更加匹配的Add函数
return 0;
}
三. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
#include <iostream>
using namespace std;
template<typename T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a = Add(2, 2.2); //模板函数不允许自动类型转换,不能通过编译
return 0;
}
因为模板函数不允许自动类型转换,所以不会将2自动转换为2.0,或是将2.2自动转换为2。
模板支持多个模板参数
template<class K, class V> //两个模板参数
void Func(const K& key, const V& value)
{
cout << key << ":" << value << endl;
}
int main()
{
Func(1, 1); //K和V均int
Func(1, 1.1);//K是int,V是double
Func<int, char>(1, 'A'); //多个模板参数也可指定显示实例化不同类型
}
总结:通过使用函数模板,可以写出更通用、更灵活的代码,同时减少重复和错误。编译时的类型检查提供了比C语言中的宏更高的类型安全性。函数模板在C++标准库中被广泛使用,例如在STL(标准模板库)的各种算法和容器中。这使得STL非常强大,因为它不仅能够处理几乎任何类型的数据,还保持了代码的紧凑和高效。
四. 类模板
类模板的概念
程序员定义一个蓝图,用于生成具体化的类,这些类可以处理多种数据类型。与函数模板类似,类模板目的在于提高代码的复用性、减少冗余,并提升类型安全性。通过类模板,可以创建出适用于任何数据类型的类结构,而无需对每种类型编写专门的代码。
类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
例如:
template<class T>
class Score
{
public:
void Print()
{
cout << "数学:" << _Math << endl;
cout << "语文:" << _Chinese << endl;
cout << "英语:" << _English << endl;
}
private:
T _Math;
T _Chinese;
T _English;
};
注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。
template<class T>
class Score
{
public:
void Print();
private:
T _Math;
T _Chinese;
T _English;
};
//类模板中的成员函数在类外定义,需要加模板参数列表
template<class T>
void Score<T>::Print()
{
cout << "数学:" << _Math << endl;
cout << "语文:" << _Chinese << endl;
cout << "英语:" << _English << endl;
}
外传:注意:类模板,是不支持,声明,定义,测试分开写的,会出现链接编译错误,如下:
解决办法:
类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后面根<>,然后将实例化的类型放在<>中即可。类模板名字不是真正的类,而实例化的结果才是真正的类
template <class T>
class ExamScores {
private:
vector<T> scores; // 存储成绩的动态数组
public:
// 向成绩列表中添加一个成绩
void addScore(T score) {
scores.push_back(score);
}
// 打印所有成绩
void printScores() const {
cout << "成绩列表: ";
for (T score : scores) {
cout << score << " ";
}
cout << endl;
}
};
int main() {
ExamScores<double> myScoresD; // 创建一个存储double类型成绩的实例
// 添加成绩
myScoresD.addScore(90.5);
myScoresD.addScore(87.2);
myScoresD.addScore(78.6);
// 打印double类型成绩
myScoresD.printScores();
ExamScores<int> myScoresI; // 创建一个存储int类型成绩的实例
myScoresI.addScore(90);
myScoresI.addScore(80);
myScoresI.addScore(78);
// 打印int类型成绩
myScoresI.printScores();
ExamScores<std::string> myScoresC; // 创建一个存储std::string类型成绩的实例
myScoresC.addScore("优秀");
myScoresC.addScore("良好");
myScoresC.addScore("不及格");
// 打印成绩
myScoresC.printScores();
return 0;
}