C++ 模板

一、引言

C++ 通过函数重载可以实现对不同数据类型完成同一功能,但是这样写重复的代码就显得很臃肿,且代码的复用率较低,每当出现一个 新类型,就需要增加对应的函数。

由此,引入了模板。所谓模板,顾名思义就是一个通用的描述。也就是使用泛型来定义函数,就是编写与类型无关的代码,其中泛型可通过具体的类型来(如 int 或 double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的的函数。

二、函数模板

2.1 函数模板格式

函数模板的基本格式如下:

1
2
3
4
5
6
7
template<typename T1, typename T2, ... , typename Tn>
return_type function_name(argument-list)
{

}
// 在标准 C++98 添加关键字 typename 之前,C++ 使用关键字 class 来创建模板,也就是可以写做 template<class T1>
// 这里用 typename 和 class 是等价的,但不能使用 struct

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename anyType>
void Swap(anyType &a, anyType &b)
{
anyType temp;
temp = a;
a = b;
b = temp;
}

int main()
{
int i = 10, j = 20;
Swap(i, j);
cout << "i:" << i << " j:" << j << endl;

double a = 3.14, b = 2.718;
Swap(a, b);
cout << "a:" << a << " b:" << b << endl;

return 0;
}

// i:20 j:10
// a:2.718 b:3.14

模板并不创建任何函数,而只是告诉编译器如何定义函数。需要交换 int 的函数时,编译器将按模板模式创建这样的函数,并用 int 代替 anyType。

这就是相当于创建了两个函数 void Swap(int &a, int &b)void Swap(double &a, double &b)

2.2 实例化

2.2.1 隐式实例化

隐式实例化(Implicit Instantiation)就是让编译器根据实参推演模板参数的实际类型。就像上面的例子那样,只管调用 Swap(i, j) 即可,具体的 i 和 j 的类型由编译器自行推导。

但若是这样调用:Swap(i, a) 编译就会报错,因为编译器无法辨认这里的 ansType 到底是 int 类型还是 double 类型。一个方法就是我们自己来强制转换,另一个方法就是下面要提到的显示实例化。

2.2.2 显式实例化

显示实例化(Explicit Instantiation)就是在函数名后的 <>中指定模板参数的实际类型,例如:

1
2
Swap<int>(i, j);
Swap<double>(a, b);

或者直接声明:

1
2
template void Swap(int a, int b);
template void Swap(double a, double b);

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
T add(T x, T y) {
return x + y;
}

template int add(int, int);
template double add(double, double);

int main() {
cout << add(1, 2) << endl; // 或直接用add<int>(1, 2)
cout << add(1.0, 2.0) << endl; // 或直接用add<double>(1, 2)
return 0;
}

这里会将 x 强制转换为 int 类型。那如果对 Swap 做类似的处理呢?

这里如果调用 Swap<int>(i, a) 则会报错。注意,这里的形参类型为 double &,所以不能指向 int 类型的变量 i。

2.3 特化

特化是指为模板提供一个或多个针对特定类型或特定类型组合的“特殊版本”的实现。当编译器需要实例化模板时,它会优先查找是否有更匹配的特化版本。如果有,就使用特化版本;如果没有,就使用通用模板。

特化是为了解决一个问题:通用模板的实现可能不适用于所有类型,或者对于某些特定类型,我们可以提供更优化、更高效、或具有特定行为的实现。

2.3.1 全特化

全特化(Full Specialization / Complete Specialization)是指为模板的所有模板参数都指定了具体的类型或值。它实际上是一个完全独立于通用模板的、同名的类或函数。

语法规则:

  • template <>:表示这是一个完全特化的版本,不再接受任何模板参数。
  • 后面紧跟模板名所有具体的类型参数。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 通用类模板
template <typename T, typename U>
class MyPair {
public:
T first;
U second;
MyPair(T f, U s) : first(f), second(s) {
std::cout << "通用 MyPair 构造" << std::endl;
}
};

// MyPair<int, double> 的全特化版本
template <>
class MyPair<int, double> {
public:
int first;
double second;
MyPair(int f, double s) : first(f), second(s) {
std::cout << "MyPair<int, double> 全特化构造" << std::endl;
}

// 可以有完全不同的成员和行为
void specialMethod() {
std::cout << "<int, double> 全特化处理!" << std::endl;
}
};

// 通用函数模板
template <typename T>
void printType(T arg) {
std::cout << "通用版本: " << typeid(arg).name() << std::endl;
}

// printType<const char*> 的全特化版本 (处理 C 风格字符串)
template <>
void printType<const char*>(const char* arg) {
std::cout << "C 字符串特化版本: " << arg << std::endl;
}

int main() {
MyPair<char, bool> p1('A', true); // 使用通用类模板
MyPair<int, double> p2(1, 3.14); // 使用 MyPair<int, double> 的全特化版本
p2.specialMethod();

printType(10); // 使用通用函数模板 (T=int)
printType(3.14f); // 使用通用函数模板 (T=float)
printType("Hello C++"); // 使用 printType<const char*> 的全特化版本

return 0;
}

/* output
通用 MyPair 构造
MyPair<int, double> 全特化构造
<int, double> 全特化处理!
通用版本: i
通用版本: f
C 字符串特化版本: Hello C++
*/

全特化版本不再是模板,它是一个具体的类或函数。它拥有与通用模板相同的名称,但在模板参数列表处指定了具体的类型。

注意:编译器在实例化时,如果找到一个完全匹配的全特化版本,会优先选择它。

2.3.2 偏特化

偏特化(Partial Specialization)是指为模板的部分模板参数指定了具体的类型,或者对模板参数进行了限制。它本身仍然是一个模板,只是比通用模板更具体。

注意:偏特化只能应用于类模板,不能应用于函数模板。 函数模板可以通过函数重载来实现类似偏特化的效果。

语法规则:

  • template <...>:仍然接受模板参数,但参数列表会比通用模板的更少或更受限制。
  • 后面紧跟模板名部分具体的类型参数,或者带有约束的类型参数。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 通用类模板
template <typename T, typename U>
class MyPair {
public:
MyPair(T f, U s) {
std::cout << "通用 MyPair 构造 (T, U)" << std::endl;
}
};

MyPair<T, int> 的偏特化版本:第二个模板参数固定为 int
template <typename T>
class MyPair<T, int> {
public:
MyPair(T f, int s) {
std::cout << "MyPair<T, int> 偏特化构造" << std::endl;
}
void processInt() {
std::cout << "对第二个参数为 int 的特殊处理." << std::endl;
}
};

// MyPair<T*, U> 的偏特化版本:第一个模板参数是指针类型
template <typename T, typename U>
class MyPair<T*, U> {
public:
MyPair(T* f, U s) {
std::cout << "MyPair<T*, U> 偏特化构造" << std::endl;
}
void processPointer() {
std::cout << "对第一个参数为指针的特殊处理." << std::endl;
}
};

int main() {
MyPair<char, double> p1('A', 3.14); // 使用通用模板
MyPair<float, int> p2(1.23f, 42); // 使用 MyPair<T, int> 偏特化
p2.processInt();
MyPair<double*, char> p3(nullptr, 'X'); // 使用 MyPair<T*, U> 偏特化
p3.processPointer();

return 0;
}

/* output
通用 MyPair 构造 (T, U)
MyPair<T, int> 偏特化构造
对第二个参数为 int 的特殊处理.
MyPair<T*, U> 偏特化构造
对第一个参数为指针的特殊处理.
*/

C++ 标准有明确的“偏序规则”来决定哪个特化更具体,见下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
// 注意:MyPair<int*, int> 会选择哪个?
// MyPair<T, int> 匹配 T=int*
// MyPair<T*, U> 匹配 T=int, U=int
// 编译器会选择 "更具体" 的那个。
// 在这里,MyPair<T*, U> 更具体,因为它同时包含了指针和另一个类型U
// 而MyPair<T, int> 只是固定了第二个参数。
MyPair<int*, int> p5(new int(10), 20); // 应该使用 MyPair<T*, U> 特化
// delete p5.first; // 如果是MyPair<T*,U>,需要手动清理
p5.processPointer(); // 验证是 MyPair<T*, U> 偏特化

return 0;
}

我在测试的时候会报错:类"MyPair<int *, int>" 没有成员 "processPointer",由此可见这个的规则匹配的是 class MyPair<T, int>

偏特化版本仍然是模板,它有自己的模板参数列表。它通过指定部分参数、参数类型(如指针、引用、数组)、或参数数量来比通用模板更具体。编译器在实例化时,会根据一个称为“模板参数推导和重载解析”的复杂规则,选择“最具体”的模板版本。匹配优先级为:全特化版本 > 任何偏特化版本 > 通用模板。

2.3 模板参数的匹配规则

对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)。大致过程如下。

  1. 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数
  2. 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用 float 参数的函数调用可以将该参数转换为 double,从而与 double 形参匹配,而模板可以为 float 生成一个实例
  3. 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错

对类模板而言,优先级排列为:显式实例化 > 全特化 > 偏特化 > 隐式实例化

对于函数模板,匹配规则为:普通函数 > 全特化 > 显式实例化 > 隐式实例化

参数匹配过程中,从最佳到最差的顺序如下所述:

  1. 完全匹配,但常规函数优先于模板
  2. 提升转换(例如,char 和 short 自动转换为int,float 自动转换为 double)
  3. 标准转换(例如,int 转换为 char,long 转换为 double)
  4. 用户定义的转换,如类声明中定义的转换

其中完全匹配允许的无关紧要转换:

从实参 到形参
Type Type &
Type & Type
Type [] * Type
Type (argument-list) Type (*) (argument-list)
Type const Type
Type volatile Type
Type * cosnt Type
Type * volatile Type *

三、类模板

类模板是对成员数据类型不同的类的抽象,它说明了类的定义规则,一个类模板可以生成多种具体的类。与函数模板的定义形式类似, 类模板也是使用template关键字和尖括号“<>”中的模板形参进行说明,类的定义形式与普通类相同。

1
2
3
4
5
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类成员定义
};

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
template<class T1>
class Stack
{
public:
// 构造函数
Stack(int capacity = 4)
:_a(new T1[capacity])
,_capacity(capacity)
,_size(0)
{}

void Push(T1 data)
{
_a[_size] = data;
_size++;
}
// ...其他方法
// 析构函数
~Stack()
{
delete[]_a;
_a = nullptr;
_capacity = _size = 0;
}
private:
T1* _a;
int _capacity;
int _size;
};

int main()
{
Stack<int> s1;
Stack<double> s2;
return 0;
}

C++ 模板
http://example.com/2026/04/14/C++-模板/
作者
Yu xin
发布于
2026年4月14日
许可协议