💗个人主页💗
⭐个人专栏------C++学习⭐
💫点击关注🤩一起学习C语言💯💫
目录
[1. 泛型编程](#1. 泛型编程)
[2. 模板概念](#2. 模板概念)
[3. 函数模板](#3. 函数模板)
[3.1 函数模板概念](#3.1 函数模板概念)
[3.2 函数模板定义格式](#3.2 函数模板定义格式)
[3.3 示例](#3.3 示例)
[3.4 函数模板的实例化](#3.4 函数模板的实例化)
[3.5 函数模板重载](#3.5 函数模板重载)
[3.6 模板参数的匹配原则](#3.6 模板参数的匹配原则)
[4. 类模板](#4. 类模板)
[4.1 类模板定义格式](#4.1 类模板定义格式)
[4.2 示例](#4.2 示例)
[4.3 类模板的实例化](#4.3 类模板的实例化)
[5. 模板的特化](#5. 模板的特化)
[5.1 概念](#5.1 概念)
[5.2 函数模板特化](#5.2 函数模板特化)
[5.3 类模板特化](#5.3 类模板特化)
[5.3.1 全特化](#5.3.1 全特化)
[5.3.2 偏特化](#5.3.2 偏特化)
1. 泛型编程
如何实现一个通用的交换函数呢?
cpp
#include <iostream>
using namespace std;
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;
}
int main()
{
int a = 2, b = 3;
Swap(a, b);
cout << a << ' ' << b << endl;
double c = 1.1, d = 2.2;
Swap(c, d);
cout << c << ' ' << d << endl;
char x = 'a', y = 'b';
Swap(x, y);
cout << x << ' ' << y << endl;
return 0;
}
使用函数重载虽然可以实现,但是有一下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
- 代码的可维护性比较低,一个出错可能所有的重载均出错。
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?

如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件 (即生成具体类型的代码),那将会节省许多头发。巧的是前人早已将树栽好,我们只需在此乘凉。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段,模板是泛型编程的基础。
2. 模板概念
C++模板是一种编程技术,它允许我们编写通用的代码来适应不同的数据类型。使用模板可以将类型参数化,从而实现代码的复用。
简单来说,模板是一种将代码与数据类型解耦的方法。通过模板,我们可以定义能够处理多种数据类型的函数或类,并且在编译时根据具体的数据类型生成相应的代码。
通过使用模板,我们可以避免编写重复的代码来处理不同的数据类型。只需编写一次模板代码,即可在需要时根据不同的数据类型进行实例化,生成相应的函数或类。
模板分为函数模板和类模板两种类型。

- 函数模板允许我们编写一个通用的函数,可以接受不同类型的参数。函数模板使用类型参数作为函数的参数或返回值类型,使得函数可以适用于多种数据类型。
- 类模板允许我们编写一个通用的类,可以使用不同类型的成员变量和成员函数。类模板使用类型参数来定义类的成员变量类型和函数返回值类型,从而实现类的通用性。
C++模板是一项强大的功能,它可以提高代码的重用性和灵活性。通过模板,我们可以编写更加通用和灵活的代码,使得我们的程序具有更好的可扩展性和可维护性。
3. 函数模板
3.1 函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
3.2 函数模板定义格式
函数模板的定义以关键字 template
开始,后跟类型参数列表和函数定义。类型参数可以是任意有效的C++类型。函数模板允许在函数的参数列表、返回值类型或函数体中使用类型参数。
cpp
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
{
函数体
}
3.3 示例
用模板来实现上面的不同类型交换问题:
注意:
typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class )
cpp
#include <iostream>
using namespace std;
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 2, b = 3;
Swap(a, b);
cout << a << ' ' << b << endl;
double c = 1.1, d = 2.2;
Swap(c, d);
cout << c << ' ' << d << endl;
char x = 'a', y = 'b';
Swap(x, y);
cout << x << ' ' << y << endl;
return 0;
}

3.4 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。
模板参数实例化分为:隐式实例化和显式实例化。
隐式实例化:
在函数调用时,编译器会根据参数类型自动推断出函数模板的类型参数,并生成对应的函数实例。
cpp
#include <iostream>
using namespace std;
template <typename T>
void print(T value)
{
cout << value << endl;
}
int main()
{
print(10); // 隐式实例化为 print<int>(10)
print(3.14); // 隐式实例化为 print<double>(3.14)
return 0;
}
显式实例化:
我们可以显式地指定函数模板的类型参数,从而生成指定类型的函数实例。在函数模板的定义前加上
template
关键字和具体的类型参数,然后进行函数调用。
cpp
#include <iostream>
using namespace std;
template <typename T>
void print(T value)
{
cout << value << endl;
}
int main()
{
print<int>(10); // 显式实例化为 print<int>(10)
print<double>(3.14); // 显式实例化为 print<double>(3.14)
return 0;
}
需要注意的是,对于函数模板的每个实例化都会生成独立的函数代码,因此多个不同的实例化会占用额外的存储空间。因此,如果某个函数模板只有在特定类型参数下才会被使用,可以考虑使用显式实例化的方式,以避免生成无用的函数代码。
另外,C++也提供了一种手动显式实例化所有可能的函数模板实例化的方式,即使用 template
关键字和具体的类型参数,然后用关键字 extern
声明该函数模板的实例化。
cpp
template <typename T>
void print(T value)
{
cout << value << endl;
}
extern template void print<int>(int); // 手动显式实例化为 print<int>(int)
extern template void print<double>(double); // 手动显式实例化为 print<double>(double)
函数模板的实例化是在编译时进行的,因此会带来一些额外的编译时间。但它能够提供更高效的代码,因为编译器可以针对具体类型进行优化,并减少运行时的类型检查。
3.5 函数模板重载
函数模板的重载是指在同一个作用域内,根据不同的参数类型或参数个数,定义多个函数模板的情况。
注意以下几点:
-
函数模板重载必须在同一个作用域内,可以在全局作用域或命名空间中定义。
-
参数类型或参数个数必须不同,可以通过添加或删除参数来区分。
-
函数模板重载的返回类型可以相同或不同。
-
调用函数模板时,编译器会根据实参的类型或个数来匹配最合适的函数模板。
cpp
template<typename T>
void print(T value)
{
cout << value << endl;
}
template<typename T1, typename T2>
void print(T1 value1, T2 value2)
{
cout << value1 << ", " << value2 << endl;
}
int main()
{
print(10); // 调用第一个函数模板
print(10, 20); // 调用第二个函数模板
}
函数模板的重载能够提供更灵活的参数处理和代码复用,通过适当的选取不同的函数模板,可以满足不同的使用需求。
3.6 模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
cpp
#include <iostream>
using namespace std;
// 专门处理int的加法函数
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;
}
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
cpp
#include <iostream>
using namespace std;
// 专门处理int的加法函数
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(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
return 0;
}
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
4. 类模板
4.1 类模板定义格式
C++中的类模板是一种用来生成类的蓝图或模板。类模板允许我们定义通用的类,其中的成员和成员函数的类型可以根据用户的需求进行参数化。
类模板的定义通常以template<typename T>
开头,其中T
是类型参数,可以是任何合法的C++类型。然后,在类的定义中,我们可以使用T
来表示成员变量的类型、函数的返回类型和参数类型。
cpp
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
4.2 示例
cpp
#include <iostream>
using namespace std;
template<typename T>
class Array
{
private:
T* elements;
int size;
public:
Array(int size)
{
this->size = size;
elements = new T[size];
}
~Array()
{
delete[] elements;
}
T& operator[](int index)
{
return elements[index];
}
};
int main()
{
Array<int> intArray(5); // 创建一个存储int类型元素的数组模板实例
Array<double> doubleArray(10); // 创建一个存储double类型元素的数组模板实例
return 0;
}
在上面的示例中,我们定义了一个名为
Array
的类模板,其中T
是类型参数。类模板拥有一个私有成员变量elements
,用于存储元素,以及一个size
变量表示数组的大小。我们还定义了构造函数、析构函数和[]
运算符重载函数,它们都使用了类型参数T
。通过实例化类模板,我们可以创建不同类型的数组对象,而且这些对象可以使用类模板中定义的成员函数和操作符。
需要注意的是,类模板的成员函数通常需要在类模板的定义体外进行定义,例如:
cpp
template<typename T>
void Array<T>::print()
{
for (int i = 0; i < size; i++)
{
cout << elements[i] << " ";
}
cout << endl;
}
这里的
Array<T>::print()
是一个类模板Array
的成员函数,我们需要在类模板外部使用Array<T>::
来指定成员函数所属的类模板,并且需要在函数名后面加上类型参数<T>
。
4.3 类模板的实例化
类模板的实例化是指根据模板定义创建具体的类的过程。在实例化时,需要为模板参数提供具体的类型,从而根据模板生成一个真正的类。
-
隐式实例化:当我们在代码中使用具体类型来创建类的对象时,编译器会自动进行类模板的实例化。例如,
cpp#include <iostream> using namespace std; template<typename T> class Array { // 类模板定义 }; int main() { Array<int> intArray; // 隐式实例化为Array<int> Array<double> doubleArray; // 隐式实例化为Array<double> return 0; }
-
显式实例化:除了隐式实例化,我们还可以显式地要求编译器实例化模板。通过在代码中显式实例化特定的模板类型,我们可以确保生成模板类的实例。例如,
cpptemplate<typename T> class Array { // 类模板定义 }; // 显式实例化Array<int>类 template class Array<int>; // 显式实例化Array<double>类 template class Array<double>;
5. 模板的特化
5.1 概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板
cpp
#include <iostream>
using namespace std;
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
class Date
{
public:
friend ostream& operator<<(ostream& _cout, const Date& d);
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
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指针的地址,这就无法达到预期而错误。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
5.2 函数模板特化
函数模板特化是指为模板函数的特定类型参数提供自定义的实现。与类模板特化类似,函数模板特化可以根据特定类型的需求,提供更具体的实现。
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
cpp
class Date
{
public:
friend ostream& operator<<(ostream& _cout, const Date& d);
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
// 函数模板 -- 参数匹配
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;
}
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
cpp
bool Less(Date* left, Date* right)
{
return *left < *right;
}
5.3 类模板特化
5.3.1 全特化
全特化即是将模板参数列表中所有的参数都确定化。
cpp
#include <iostream>
using namespace std;
template <typename T>
class MyTemplate
{
public:
MyTemplate(T value)
{
cout << "Generic Template: " << value << endl;
}
};
template <>
class MyTemplate<int>
{
public:
MyTemplate(int value)
{
cout << "Specialized Template for int: " << value + 1 << endl;
}
};
int main()
{
MyTemplate<float> obj1(3.14); // 使用通用模板,输出:Generic Template: 3.14
MyTemplate<int> obj2(10); // 使用特化模板,输出:Specialized Template for int: 11
return 0;
}
在上面的示例中,我们定义了一个通用的类模板MyTemplate
,它使用模板参数T
来表示数据类型,并在构造函数中打印出传入的值。
然后,我们通过特化语法,为类型int
提供了一个全特化的实现。在全特化实现中,我们将完全重新定义并实现了新的类。
在main
函数中,我们创建了两个对象obj1
和obj2
,分别使用了通用模板和全特化模板进行实例化。根据对象的类型,编译器会选择相应的模板进行实例化。
5.3.2 偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
cpp
#include <iostream>
using namespace std;
template <typename T, typename U>
class MyTemplate
{
public:
MyTemplate(T t, U u)
{
cout << "Generic Template: " << t << ", " << u << endl;
}
};
template <typename T>
class MyTemplate<T, int>
{
public:
MyTemplate(T t, int u)
{
cout << "Partial Specialization for int: " << t << ", " << u + 1 << endl;
}
};
int main()
{
MyTemplate<float, float> obj1(3.14, 2.5); // 使用通用模板,输出:Generic Template: 3.14, 2.5
MyTemplate<int, int> obj2(10, 20); // 使用通用模板,输出:Generic Template: 10, 20
MyTemplate<double, int> obj3(4.5, 5); // 使用偏特化模板,输出:Partial Specialization for int: 4.5, 6
return 0;
}
在上面的示例中,我们定义了一个通用的类模板MyTemplate
,它接受两个模板参数T
和U
,并在构造函数中打印出传入的值。
然后,我们通过偏特化语法,为类型int
提供了一个偏特化的实现。在偏特化实现中,我们将重新定义并实现了新的类,并对第二个模板参数固定为int
。这意味着只有第二个参数是int
时,才会选择偏特化模板进行实例化。
在main
函数中,我们创建了三个对象obj1
、obj2
和obj3
,分别使用了通用模板和偏特化模板进行实例化。根据对象的类型,编译器会选择相应的模板进行实例化。在obj3
的情况下,由于第二个参数是int
,因此偏特化模板会被选择。