06 模板初阶

C++ 模板概述

在 C++ 中,模板(Template)是一个非常强大的特性,它可以让我们编写与特定数据类型无关的代码,最终由编译器根据实际的类型生成特定的代码。模板主要分为两类:函数模板类模板。模板的引入大大增强了 C++ 语言的灵活性和代码复用性,减少了重复代码的编写。

1. 为什么需要模板?

假设你想编写一个通用的函数来交换两个变量的值。最初,你可能会想到通过函数重载来实现,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}
……

虽然使用函数重载可以满足不同数据类型的需求,但这种方法存在一些问题:

  1. 代码重复:每增加一个新的类型(例如 floatlong),都需要为其编写一个新的 Swap 函数。这使得代码复用性差。
  2. 可维护性差:如果 Swap 函数的实现出现错误,可能需要修改每个重载版本,这会导致错误传播并增加维护的复杂度。
  3. 扩展性差:当需要处理的新类型增多时,维护这些重载函数的工作量也随之增加。

如何解决这些问题?

可以通过 函数模板 来解决这些问题。模板可以让你编写通用的函数或类,这些函数或类在编译时根据实际使用的类型生成特定的版本。

2. 函数模板

函数模板 允许你定义一个蓝图或框架,它并不依赖于某种特定的数据类型,而是通过在编译时根据传入的类型生成具体类型的函数。这样你就能够编写一个函数来处理所有类型的参数,而无需手动为每个类型编写不同的函数。

2.1 函数模板的定义

一个典型的函数模板定义如下:

1
2
3
4
5
6
7
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
  • template<typename T>:声明了一个模板,T 是一个类型参数,表示可以接受任何数据类型,T 是自己命名的,可以是 TYA 等等(同时也可以使用 template <class T>二者是等价的,这里先知道可以使用 class,至于为什么,以后再讨论)。
  • void Swap(T& left, T& right):这是模板函数的定义,T 会根据调用时传入的参数类型来具体化。

这段代码可以交换任意类型的两个变量,不需要为每种类型单独写一个函数。

2.2 模板的使用

当我们使用模板时,编译器会根据传递给模板的实际类型来生成相应的代码。

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
对于不同的类型(如 `int`、`double`),编译器会自动根据传入的类型生成不同版本的 `Swap` 函数,而不需要手动为每个类型编写不同的函数。
#include <iostream>
using namespace std;

// 泛型 Swap 函数,接受任意类型的引用参数
template <typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}

int main()
{
int a = 5, b = 10;
Swap(a, b); // 使用 int 类型生成 Swap(int& left, int& right)
cout << "a = " << a << ", b = " << b << endl;

double x = 1.1, y = 2.2;
Swap(x, y); // 使用 double 类型生成 Swap(double& left, double& right)
cout << "x = " << x << ", y = " << y << endl;

return 0;
}

多个参数的模板函数(多类型支持): 模板函数可以支持多个模板参数类型,通过模板参数列表可以定义多个不同类型的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

template<typename T1, typename T2>
void print(const T1& left, const T2& right)
{
cout << left << " " << right << endl;
}

int main()
{
print(1, 2); // 整型参数
print(1, 2.2); // 整型和浮点型参数
print("Hello", 2.2); // 字符串和浮点型参数

return 0;
}

模板函数推导类型(自动推导):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

template<class T>
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;

// 编译器自动推导类型
cout << Add(a1, a2) << endl; // 使用 int 类型
cout << Add(d1, d2) << endl; // 使用 double 类型
cout << Add(a1, (int)d1) << endl; // 混合类型时自动推导为 int 类型
cout << Add((double)a1, d1) << endl; // 混合类型时自动推导为 double 类型

return 0;
}

显式指定模板类型: 有时需要显式指定模板类型,尤其是在类型推导无法正确推导时。

1
2
3
4
// 当参数类型不一致时,必须显式指定或强制类型转换
Add(10, 20.5); // 错误:T 无法推导为两种不同类型
Add<int>(10, 20.5); // 正确:T 显式指定为 int,第二个参数隐式转换为 int
Add<double>(10, 20.5); // 正确:T 显式指定为 double,第一个参数隐式转换为 double
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}

int main()
{
int a1 = 10;
double d1 = 10.1;

// 显式转换 int 类型进行计算
cout << Add(a1, (int)d1) << endl; // 将 double 类型 d1 显式转换为 int 类型进行加法运算
// 显式转换 double 类型进行计算
cout << Add((double)a1, d1) << endl; // 将 int 类型 a1 显式转换为 double 类型进行加法运算
// 显式指定模板类型为 int,进行计算
cout << Add<int>(a1, d1) << endl; // 显式指定 Add 模板为 int 类型,但会进行隐式转换,最终结果为 int 类型

return 0;
}

模板函数返回指针(返回类型为指针): 模板函数不仅能返回基本类型的值,还可以返回指针类型(如动态分配内存时)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

template<class T>
T* Alloc(int n)
{
return new T[n]; // 动态分配数组
}

int main()
{
// 使用模板函数分配数组
double* p1 = Alloc<double>(10); // 分配 10 个 double 类型的元素

// 可以在这里操作 p1 数组,记得使用完后 delete[] 释放内存
delete[] p1; // 释放动态分配的内存

return 0;
}

混合类型进行计算(加法操作等): 当我们进行加法操作时,如果参数类型不同,C++ 可以通过类型转换处理不同类型之间的计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}

int main()
{
int a1 = 10;
double d1 = 10.1;

// 混合类型计算
cout << Add(a1, (int)d1) << endl; // 转换为 int 类型后进行加法
cout << Add((double)a1, d1) << endl; // 转换为 double 类型后进行加法

return 0;
}

2.3 隐式实例化与显式实例化

  • 隐式实例化:编译器根据你传入的实参类型自动推导出模板参数的具体类型。例如,Swap(a, b) 会自动推导出 Tint,生成一个 Swap(int& left, int& right) 的函数。
  • 显式实例化:你也可以显式地指定模板参数类型。例如:
1
2
3
// 显式实例化的典型场景(在源文件中使用)
template void Swap<int>(int&, int&); // 生成 int 版本
template void Swap<double>(double&, double&); // 生成 double 版本

这种方式可以避免编译器自动推导模板参数的类型,而是由你明确指定。

2.4 模板的匹配与优先级

C++ 在选择函数时有一套匹配规则:

函数模板与非模板函数共存:当你既有非模板函数又有模板函数时,C++ 编译器会根据传入参数的类型优先选择非模板函数。例如:

1
2
3
void Swap(int& left, int& right);  // 非模板函数
template<typename T>
void Swap(T& left, T& right); // 模板函数

当你传入 int 类型时,编译器会优先选择 Swap(int& left, int& right)

模板函数匹配规则:如果模板能够提供更好的匹配,编译器会优先选择模板函数。模板函数通常会考虑类型转换,而非模板函数则不会进行自动类型转换。

3. 模板的优势

  • 提高代码复用性: 使用函数模板的最大好处是可以大大减少重复代码。例如,使用模板函数可以避免为每个数据类型编写多次相同的代码,只需要编写一次模板函数,编译器会根据不同的类型实例化出对应的代码。
  • 代码的可维护性: 当你需要修改 Swap 函数的实现时,只需要修改模板函数本身。所有使用了这个模板的地方,编译器会自动更新生成的代码,这大大提高了代码的可维护性。
  • 自动类型推导: 模板能够通过类型推导来决定类型,使得调用者不必显式指定类型。这不仅简化了代码,还减少了出错的可能性。
  • 避免错误: 模板的另一个好处是,可以避免因手动编写多个重载函数而导致的错误。例如,模板函数不会被错误地应用于不兼容的类型,而编译器会在编译时进行类型检查,确保类型安全。

4. 总结

C++中的模板是一种非常强大的特性,它能够实现与类型无关的通用代码,从而提高代码复用性、可维护性和扩展性。模板函数尤其适合用于处理不同类型但逻辑相同的代码,它允许编写一个蓝图或模具,编译器根据实际传入的类型生成具体的函数或类实例。通过模板,程序员可以避免冗余代码的编写,减少出错的可能,同时保持代码的简洁和高效。


类模板:提高代码复用性与灵活性

在 C++ 中,类模板是一个强大的工具,它允许我们编写通用的类,而不依赖于特定的类型。通过类模板,我们可以提高代码的复用性,减少冗余代码,并且使得代码更加灵活易于扩展。

代码重构:从重复代码到通用模板

在实际开发中,我们经常遇到需要为不同数据类型编写类似代码的情况。假设我们需要实现一个栈类,并且这个栈类要处理多种数据类型,比如 intdouble 类型。最初的做法可能是为每种数据类型写一个新的类:

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
// int 类型栈
class StackInt
{
public:
StackInt(size_t capacity = 3) {
_array = (int*)malloc(sizeof(int) * capacity);
_capacity = capacity;
_size = 0;
}

void Push(int data)
{
_array[_size] = data;
_size++;
}

~StackInt()
{
free(_array);
}

private:
int* _array;
int _capacity;
int _size;
};

// double 类型栈
class StackDouble
{
public:
StackDouble(size_t capacity = 3)
{
_array = (double*)malloc(sizeof(double) * capacity);
_capacity = capacity;
_size = 0;
}

void Push(double data)
{
_array[_size] = data;
_size++;
}

~StackDouble()
{
free(_array);
}

private:
double* _array;
int _capacity;
int _size;
};

问题分析:

虽然 StackIntStackDouble 做的工作基本相同,唯一的区别是它们处理的类型不同(intdouble)。但是,这种做法导致了冗余的代码。每增加一个新的数据类型,我们都需要为其编写一个类似的类,这样代码就会变得越来越冗长,维护起来也变得更为困难。


类模板的引入:

为了避免这种冗余代码,并提高代码的复用性,我们可以使用 C++ 的类模板来解决这个问题。类模板允许我们为不同的数据类型生成相同功能的类,而无需重复编写相似的代码。

通过类模板,我们可以将 StackIntStackDouble 合并为一个通用的栈类 Stack<T>,其中 T 代表数据类型。通过实例化类模板,编译器会根据我们提供的类型自动生成不同版本的栈类。

下面是通过类模板重构后的栈类代码:

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 T>  // 定义一个类模板,T 是类型参数
class Stack
{
public:
Stack(size_t capacity = 3) : _capacity(capacity), _size(0)
{
_array = new T[capacity]; // 使用 T 类型动态分配内存
}

void Push(const T& data)
{
_array[_size++] = data; // 将数据压入栈
}

~Stack()
{
delete[] _array; // 释放内存
}

private:
T* _array; // 存储数据的数组,类型由模板参数决定
int _capacity; // 栈的最大容量
int _size; // 栈当前的元素个数
};

通过类模板,我们可以在 `main` 函数中实例化栈类,指定具体的类型:
int main()
{
Stack<int> s1; // 创建一个存储 int 类型的栈
Stack<double> s2; // 创建一个存储 double 类型的栈

s1.Push(10); // 向 int 类型栈中压入整数
s2.Push(3.14); // 向 double 类型栈中压入浮点数

return 0;
}

代码解析:

  1. 模板定义template<class T> 定义了一个类模板,其中 T 是类型参数,表示栈中元素的类型。在实例化时,T 会被替换为具体的类型。
  2. 通用数据类型:类模板中的 _array 被定义为 T*,这使得我们可以使用任意类型的数据。无论是 intdouble 还是其他自定义类型,类模板都会根据我们指定的类型生成相应的栈类。
  3. 内存分配:在构造函数中,我们使用 new T[capacity] 来动态分配内存,T 会根据实际类型决定内存的大小。例如,当 Tint 时,会分配一个 int 类型的数组;当 Tdouble 时,会分配一个 double 类型的数组。
  4. 入栈操作Push 方法接受一个 const T& 类型的参数,允许我们将任何类型的数据压入栈中。
  5. 析构函数:使用 delete[] 来释放栈的内存,避免内存泄漏。

在上面的代码中,我们通过指定不同的模板参数(intdouble)来创建不同类型的栈实例。每个栈都具有相同的功能,但能够处理不同的数据类型。


类模板的优势

  • 提高代码复用性:类模板允许我们编写一次通用代码,而不需要为每种数据类型编写独立的类。只需定义一个模板,编译器会根据实际使用的类型自动生成不同类型的类。这使得代码更加简洁并减少了冗余。
  • 更加灵活:类模板的一个显著优点是它的灵活性。你可以根据需要创建不同类型的栈(或其他数据结构)。例如,可以创建 Stack<int> 来存储整数,创建 Stack<double> 来存储浮点数,甚至可以用自定义类作为模板参数来存储自定义对象。
  • 易于维护:使用类模板时,修改模板代码会自动影响所有实例化的类。这意味着当我们需要支持新的数据类型时,只需修改模板定义,而无需修改现有的类定义,从而减少了重复工作。
  • 避免冗余代码:每种数据类型都写一个独立的类会导致代码冗长,且容易出错。类模板提供了一个统一的代码框架,减少了冗余代码,提高了代码质量。

总结

类模板是 C++ 的一项强大特性,它允许你编写通用的类,并且能够处理不同类型的数据。通过类模板,我们可以避免冗余代码,提高代码复用性和可维护性,并使代码更加灵活。类模板在 C++ 标准库(如 STL)中广泛应用,特别是在实现容器类和算法时。掌握类模板的使用,能够帮助开发者编写更加简洁、灵活、易于扩展的代码。