阅读导航
- 前言
- 一、模版的概念
- 二、函数模版
- 1. 函数模板概念
- 2. 函数模板定义格式
- 3. 函数模板的原理
- 4. 函数模版的实例化
- 🚩隐式实例化
- 🚩显式实例化
- 5. 函数模板的匹配原则
- 三、类模板
- 1. 类模板的定义格式
- 2. 类模板的实例化
- 四、非类型模板参数
- 1. 概念
- 2. 定义
- 五、模板的特化
- 1. 概念
- 2. 函数模版特化
- 3. 类模版特化
- ⭕全特化
- ⭕偏特化
- 4. 模版特化应用示例
- 六、模板分离编译
- 1. 什么是分离编译
- 2. 模版的分离编译
- 七、模版的优缺点
- 【优点】
- 【缺点】
- 温馨提示
前言
前面我们讲了C语言的基础知识,也了解了一些数据结构,并且讲了有关C++的命名空间的一些知识点以及关于C++的缺省参数、函数重载,引用 和 内联函数也认识了什么是类和对象以及怎么去new一个 ‘对象’ ,以及学习了几个STL的结构也相信大家都掌握的不错,接下来博主将会带领大家继续学习有关C++比较重要的知识点—— 模版(template)。下面话不多说坐稳扶好咱们要开车了😍
一、模版的概念
模板是C++中的一种编程工具,它允许使用通用代码来定义函数和类,以适应多种类型或值的需求,从而实现代码的复用和泛化。模板实质上是一种参数化的类型或值的规范。通过模板的使用,可以提高代码的复用性和拓展性,使得代码更加通用并能适应不同类型或值的需求。模板可以在编译时生成针对不同类型或值的代码,从而提高代码的效率和灵活性。在C++中,有两种类型的模板:函数模板和类模板。下面博主来逐个介绍。
二、函数模版
1. 函数模板概念
函数模板允许定义一个通用的函数
,其中一些或全部的参数的类型可以是参数化的。使用函数模板时,编译器根据实际使用的参数类型,自动生成对应的函数代码。
2. 函数模板定义格式
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
{
}
🚨注意:typename
是用来定义模板参数关键字,也可以使用class
(切记:不能使用struct
代替class
)
使用模版定义一个交换函数
template<typename T>
void Swap( T& left, T& right)
{T temp = left;left = right;right = temp;
}
使用模版定义一个取较大值函数
template <typename T>
T getMax(T a, T b) {return a > b ? a : b;
}int main() {int maxInt = getMax(2, 5); // 使用函数模板实例化为 int 类型的函数double maxDouble = getMax(3.14, 2.5); // 使用函数模板实例化为 double 类型的函数// ...
}
3. 函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
4. 函数模版的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。函数模板的实例化过程可以分为两个步骤:模板参数推断和模板函数生成。
-
模板参数推断:编译器根据实际传入的参数类型推导出模板参数的具体类型。编译器会尝试根据传递的参数类型来匹配模板参数,并确定参数的具体类型。如果无法进行准确的匹配,则可能会产生模板参数推断失败的错误。
-
模板函数生成:根据推断出的模板参数类型,编译器生成特定类型的函数代码。编译器使用推断出的参数类型来替换函数模板中的模板参数,生成与传递的参数类型匹配的函数定义。
⭕函数模板的实例化是在编译时完成的,它提前为不同的参数类型生成了不同的函数定义,以提高代码的重用性和执行效率。
⭕模板参数实例化分为:隐式实例化和显式实例化
🚩隐式实例化
下面是一个示例,展示函数模板隐式实例化的过程:
template <typename T>
T getMax(T a, T b) {return a > b ? a : b;
}int main() {int maxInt = getMax(2, 5); // 实例化为 int 类型的函数,参数推断为 intdouble maxDouble = getMax(3.14, 2.5); // 实例化为 double 类型的函数,参数推断为 doublechar maxChar = getMax('a', 'b'); // 实例化为 char 类型的函数,参数推断为 char// ...
}
在这个例子中,编译器会根据传递的参数类型自动推断模板参数的类型,并生成对应类型的函数代码。实例化后会生成 getMax
函数的具体定义,其中的模板参数 T
被替换为相应的类型。
🚩显式实例化
模板的显式实例化是指在编译时明确告诉编译器需要实例化的模板类型,以生成对应的函数定义。
在函数名后的<>中指定模板参数的实际类型
下面是一个示例,展示函数模板显式实例化的过程:
template <typename T>
T getMax(T a, T b) {return a > b ? a : b;
}
int main() {int maxInt = getMax<int>(2, 5); // 显式实例化的 int 类型的函数int maxDouble = getMax<double>(1.1, 5.2)// 显式实例化的 double类型的函数// ...
}
通过显式实例化,可以在编译时生成特定类型的函数定义,避免了模板参数推断和函数生成的开销,提高了代码的执行效率。
5. 函数模板的匹配原则
-
最佳匹配原则:编译器会尝试找到与调用参数最匹配的函数模板来实例化。在函数模板的候选函数中,编译器会根据实际参数类型进行以下规则的匹配:
a. 完全匹配:如果有一个函数模板能够完全匹配实际参数的类型,那么它将被选择为最佳匹配。
b. 类型转换匹配:如果有多个函数模板能够通过一系列的类型转换(如隐式类型转换)匹配实际参数的类型,那么转换次数最少的模板将被选择为最佳匹配。
c. 模板特化匹配:如果存在与调用参数类型完全匹配的模板特化,那么它将被选择为最佳匹配。
d. 不匹配:如果没有找到合适的模板来匹配调用参数的类型,那么将导致编译错误。
-
函数模板的特例化规则:当函数模板的特化版本和常规模板同时存在时,编译器会优先选择特化版本。
下面的代码展示了函数模板匹配原则的应用:
template <typename T>
void print(T value) {std::cout << value << std::endl;
}template <>
void print(int value) {std::cout << "Specialized: " << value << std::endl;
}void print(double value) {std::cout << "Non-template: " << value << std::endl;
}int main() {print(5); // 调用特化版本的 print,输出 "Specialized: 5"print(3.14); // 调用非模板函数 print,输出 "Non-template: 3.14"print("Hello"); // 调用普通模板函数 print,输出 "Hello"return 0;
}
在上述示例中,当调用 print
函数时,根据参数类型的不同,编译器将根据匹配原则选择最佳匹配的函数版本。如果有特化版本,将优先选择特化版本。如果没有特化版本,会选择模板函数中最适合的版本来实例化。
三、类模板
1. 类模板的定义格式
定义多个类型
template<class T1, class T2, ..., class Tn>
class 类模板名
{// 类内成员定义
};
定义单个类型
template <typename T>
class ClassName {// 类模板的成员和方法声明及定义
};
在上述格式中,template <typename T>
表示定义了一个类模板,T
是一个类型参数,它可以在类的成员和方法中使用。你可以根据需要使用其他的类型参数名称。
下面的代码展示了一个简单的类模板的定义:
🚨注意:Pair 不是具体的类,是编译器根据被实例化的类型生成具体类的模具template <typename T>
class Pair {
private:T first;T second;public:Pair(T f, T s) : first(f), second(s) {}T getFirst() const {return first;}T getSecond() const {return second;}void setFirst(T f) {first = f;}void setSecond(T s) {second = s;}
};
Pair
是一个类模板,拥有两个泛型成员变量 first
和 second
,以及一些泛型成员函数。通过类模板,我们可以定义一个通用的配对(Pair)类,用于存储任意类型的一对值。
2. 类模板的实例化
⭕类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
例如:
int main() {Pair<int> p(3, 4); // 实例化一个 Pair 类型对象,其中 T 被替换为 intPair<double> q(1.5, 2.7); // 实例化一个 Pair 类型对象,其中 T 被替换为 doubleint first = p.getFirst(); // 获取 p 对象中的第一个值 3double second = q.getSecond(); // 获取 q 对象中的第二个值 2.7p.setSecond(7); // 设置 p 对象的第二个值为 7return 0;
}
在上面的代码中,我们使用不同的具体类型参数实例化了 Pair
类模板,并使用相应的对象进行操作。编译器会根据实际传递的类型参数替换类模板中的类型参数 T
,生成对应的类定义和对象实例化。
四、非类型模板参数
1. 概念
非类型模板参数是指在C++中,模板参数可以不仅仅是类型,还可以是常量表达式。非类型模板参数允许在模板实例化时传递常量值作为参数,并在编译时对其进行计算和使用。
⭕通过使用非类型模板参数,可以实现在编译时生成特定类型或值的代码。
🚨非类型模板参数必须是以下几种类型之一:
- 整数类型,包括整数、字符和枚举类型。
- 指针类型。
- 引用类型。
⭕浮点数、类对象和字符串是不允许作为非类型模板参数的。
因为非类型模板参数在编译时需要被计算和处理,而浮点数、类对象和字符串类型的计算和处理是在运行时进行的,无法在编译时确定。
2. 定义
在定义模板时,可以使用非类型模板参数来指定一个或多个参数。例如:
template <typename T, int SIZE>
class Array {T data[SIZE];// ...
};
在这个例子中,模板参数SIZE
是一个非类型的整数参数,用于指定数组的大小。在实例化Array
模板时,需要指定一个整数常量作为SIZE
的值。
五、模板的特化
1. 概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。比如:实现了一个专门用来进行小于比较的函数模板。
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}int main()
{cout << Less(1, 2) << endl; // 可以比较,结果正确Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl; // 可以比较,结果正确Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 可以比较,结果错误return 0;
}
可以看到,Less
绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1
指向的d1
显然小于p2
指向的d2
对象,但是Less
内部并没有比较p1
和p2
指向的对象内容,而比较的是p1
和p2
指针的地址,这就无法达到预期而错误。此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
2. 函数模版特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{return *left < *right;
}
int main()
{cout << Less(1, 2) << endl;Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl;Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 调用特化之后的版本,而不走模板生成了
return 0;
}
3. 类模版特化
⭕全特化
完全特化是指对于特定的类型或参数,提供了一个完全定制的模板实现。在完全特化中,给定的特化版本针对特定类型或参数提供了特定的实现,这个特化版本将完全替代泛化模板。可以通过显式声明来完成完全特化,使用 template<>
来指示特化版本。
下面是一个完全特化的示例:
template <typename T>
struct MyClass {void doSomething() {// 泛化版本的实现}
};template <>
struct MyClass<int> {void doSomething() {// 针对 int 类型的完全特化实现}
};
在上面的示例中,我们定义了一个模板类 MyClass
,并对其进行了完全特化。在 MyClass<int>
的特化版本中,我们针对 int
类型提供了一个特定的成员函数实现。
⭕偏特化
偏特化是指对部分类型或参数进行特化,针对特定的形式或范围进行自定义处理。偏特化可以有多个参数,并对其中一个或多个参数进行特化。相对于完全特化,偏特化可以提供更灵活的定制需求。
下面是一个偏特化的示例:
template <typename T, typename U>
struct MyClass {void doSomething() {// 泛化版本的实现}
};template <typename T>
struct MyClass<T, int> {void doSomething() {// 对于第二个参数为 int 的偏特化实现}
};
在上面的示例中,我们定义了一个模板类 MyClass
,并对其进行了偏特化。在 MyClass<T, int>
的特化版本中,我们针对第二个参数为 int
的情况提供了一个特定的成员函数实现。
🚨注意,特化版本的成员函数可以是不同的,甚至可以有不同的成员变量和特定的行为。
4. 模版特化应用示例
下面是一个使用函数模板特化的示例,展示了如何实现针对特定类型的特定行为:
#include <iostream>// 泛化版本的模板函数
template <typename T>
void showType(T value) {std::cout << "Value: " << value << " is of unknown type\n";
}// 特化版本的模板函数,针对字符串类型
template <>
void showType<std::string>(std::string value) {std::cout << "Value: " << value << " is a string\n";
}// 特化版本的模板函数,针对整型类型
template <>
void showType<int>(int value) {std::cout << "Value: " << value << " is an integer\n";
}int main() {showType("Hello"); // 使用特化版本的模板函数,输出 "Value: Hello is a string"showType(123); // 使用特化版本的模板函数,输出 "Value: 123 is an integer"showType(3.14); // 使用泛化版本的模板函数,输出 "Value: 3.14 is of unknown type"return 0;
}
在上述示例中,我们定义了一个模板函数 showType
,用于根据传入的参数类型显示该值的类型信息。
通过模板特化,我们为特定类型(std::string
和int
)提供了特定的实现方式。在主函数中,我们分别调用了 showType
函数并传入不同的参数类型,从而分别调用了泛化版本和特化版本的模板函数。
这样我们可以根据不同的类型提供特定的处理方式,以满足特定需求。运行结果为:
Value: Hello is a string
Value: 123 is an integer
Value: 3.14 is of unknown type
六、模板分离编译
1. 什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
2. 模版的分离编译
模板的分离编译可以将模板的声明和实现分离到不同的文件,这样每个源文件只需要编译一次模板的实现,减少了代码冗余和编译时间。
🚨🚨注意:每次进行编译都要进行模版实例化,如果不实例化或者参数与实例化的不匹配,编译器在进行链接时会报错,所以不建议进行分离编译
下面是一个使用模板的分离编译的示例:
- 头文件
mytemplate.h
包含了模板的声明:
#ifndef MYTEMPLATE_H
#define MYTEMPLATE_Htemplate <typename T>
class MyClass {
public:void print(T value);
};#endif
- 源文件
mytemplate.cpp
包含了模板的实现:
#include <iostream>
#include "mytemplate.h"template <typename T>
void MyClass<T>::print(T value) {std::cout << "Value: " << value << std::endl;
}// 显式实例化模板,以确保编译器生成该类型的代码
template class MyClass<int>;
- 主函数所在的源文件
main.cpp
使用了模板,但没有包含实现:
#include "mytemplate.h"int main() {MyClass<int> obj;obj.print(42);return 0;
}
在上述示例中,mytemplate.h
包含了模板的声明,mytemplate.cpp
包含了模板的实现,并且通过使用 template class MyClass<int>
显式实例化了模板的 int
特化版本(必须要实例化,否则就会报错)
七、模版的优缺点
【优点】
-
通用性:模板提供了一种通用的编程方式,可以在不同的类型上进行操作和实例化,增强了代码的复用性和可扩展性。
-
静态类型检查:模板在编译时进行类型检查,可以捕获一些类型错误和逻辑错误,提前发现问题并减少运行时错误。
-
高性能:模板生成的代码在编译时会生成特定类型的实现,避免了运行时的类型转换和动态分派,提供了更高的执行效率。
-
泛化算法:模板可以用于实现各种泛化算法,无需为不同的数据类型编写不同的代码,减少了重复劳动和代码维护成本。
【缺点】
-
长编译时间:模板通常在编译时进行实例化和展开,对于复杂的模板和大规模的代码库,编译时间可能会显著增加。
-
可读性差:模板的代码通常比非模板代码更复杂,对于初学者或不熟悉模板编程的人来说,理解和维护模板代码可能更加困难。
-
编译错误信息难以理解:当模板出现编译错误时,编译器生成的错误消息可能很难理解和定位,给调试带来一定的困难。
-
扩展性受限:对于已实例化的模板,无法在运行时动态地添加新的类型支持,如果需要支持新的类型或功能,需要重新编译模板。
温馨提示
感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!
再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!