模板
- 引入模板
- 函数模板
- 概念
- 语法
- 函数模板的原理
- 函数模板实列化
- 隐式实例化
- 显示实例化
- 模板参数匹配原则
- 类模板
- 类模板的定义格式
- 类模板的实列化
- 泛型编程
- 补充知识
引入模板
当我们想实现一个通用的交换函数时,我们能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成需要的代码?
答案:是可以的。
在没有学习模板之前,通过之前学习的知识,我们知道了函数重载。可以通过函数重载实现不同类型的相同功能的同名函数。函数重载
函数重载的缺点:
- 代码复用率低,只要有新的类型出现,就需要用户增加对应的函数。
- 可维护性低,一个出错可能所有的重载都出错。
函数模板
概念
函数模板代表了一个函数家族,该函数模板和类型无关,在使用时根据实参类型产生函数的特定类型版本,模板参数化。
语法
template<typename T1, typedef T2,…, typedef Tn> 或者 template<class T1, typedef T2,…, typedef Tn>
返回值类型 函数名(参数列表) {}
eg:
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
注意: typename是用来定义模板参数的关键字,也可以使用class。
函数模板的原理
函数模板是一个蓝图,他本身不是函数,是编译器使用方式产生特定具体类型函数的模具。所以其实模板就是将本来我们做的重复的事交给编译器。
编译器在编译阶段,对于函数模板的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。
函数模板实列化
概念:用不同类型的参数使用函数模板。
隐式实例化
让编译器根据实参推演模板参数的实际类型。
template<typename T>
//这const要加,因为下面实参被强制类型转化了
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
Add(a1, a2);
Add(d1, d2);
//因为模板参数列表就一个T,编译器无法确定T是int类型还是double类型
//在模板中,编译器一般不会进行类型转换操作,因为如果转换出现问题,就是编译器的原因了
//Add(a1, d1); //err
//两种处理方式:1.用户自己强转 2.使用显示实列化
Add(a1, (int)d1);
Add((double)a1, d1);
return 0;
}
有写函数无法自动推,只能显示实列化。
eg:
template<class T>
T* Alloc(int n)
{
return new T[n];
}
int main()
{
//只能显示实列化
double* p1 = Alloc<double>(10);
//double* p1 = Alloc(10); //err
return 0;
}
显示实例化
显示实例化:在函数名后的<>中指定模板参数的实际类型。
template<typename T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a = 10;
double b = 20.1;
//显示实列化
Add<int>(a, b); //b 隐式类型转换
return 0;
}
类型不匹配编译器会尝试进行隐式类型转换,无法转换成功编译器会报错。
模板参数匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
eg:
int Add(int left, int right)
{
return left + right;
}
template<class T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
Add(1, 2); //与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); //调用编译器特化的Add版本
return 0;
}
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
eg:
int Add(int left, int right)
{
return left + right;
}
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
int main()
{
Add(1, 2); //与非模板函数类型完全匹配,不需要函数模板实列化
Add(1, 2.0); //模板函数可以生成更加匹配的版本
return 0;
}
- 在 C++ 中,模板函数不会自动进行类型转换,而普通函数会。
eg:
template <typename T>
void templateFunction(T value) {
std::cout << "Template function: " << value << std::endl;
}
void normalFunction(int value) {
std::cout << "Normal function: " << value << std::endl;
}
int main() {
int intValue = 10;
double doubleValue = 3.14;
templateFunction(intValue); // 不会进行类型转换,输出:Template function: 10
templateFunction(doubleValue); // 不会进行类型转换,输出:Template function: 3.14
normalFunction(intValue); // 进行了隐式类型转换,输出:Normal function: 10
normalFunction(doubleValue); // 进行了隐式类型转换,输出:Normal function: 3
return 0;
}
在上述代码中,
- templateFunction是一个模板函数,它可以接受任意类型的参数。无论传入的参数是int还是double,都不会进行类型转换,而是直接使用传入的参数类型。
- 而normalFunction是一个普通函数,它的参数类型是int。当我们传入一个double类型的参数时,编译器会自动进行隐式类型转换,将doubleValue的值转换成int类型,然后调用该函数。
因此,模板函数不允许自动类型转换,而普通函数可以进行自动类型转换。
类模板
类模板的定义格式
template<class T1, class T2, ...,class Tn>
class 类模板名
{
//类内成员定义
};
这里就不写个类的模板介绍了,后续的stl容器博客会有相关模板内容
注意: 类模板中的函数放在类外进行定义时,就需要加模板参数列表
eg:
template<class T>
Vector<T>::~Vector()
{
if (_a)
{
delete[] _a;
}
_size = _capacity = 0;
}
类模板的实列化
类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类
eg:
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
泛型编程
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。
模板是泛型编程的基础
补充知识
- 模板最重要的一点就是类型无关,提高代码复用率
- 模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换
- 只要支持模板语法,模板的代码就是可移植的。
- 模板的实参,在参数类型不同时,有时需要展示指定的类型参数
- 类模板是一个类家族,模板类是通过模板实列化的具体类。
- 类模板的虚拟类型是指类模板在使用时的占位类型
- 类模板的成员函数,全是模板函数。