我们来详细讲解 C++ 中的模板(Templates)。

1. 模板的概念

模板是 C++ 中的一种特性,它允许你编写通用代码,这种代码可以与多种不同的数据类型一起工作,而无需为每种数据类型分别编写。

你可以把模板想象成一个代码生成器。你定义一个模板,它包含了通用的逻辑,然后当你用一个具体的数据类型(比如 intdouble 或一个自定义的 Person 结构体)去“实例化”这个模板时,编译器会自动为你生成一份针对该具体类型的代码。

2. 函数模板

函数模板是最常见的模板类型。它允许你创建一个通用的函数,用于处理各种类型的数据。

2.1 函数模板的定义

template <typename T>
T add(T a, T b) {
return a + b;
}
  • template <typename T>:这是模板声明。

    • template 关键字告诉编译器接下来是一个模板定义。
    • <typename T> 是模板参数列表。typename 是一个关键字,用于引入一个模板类型参数。T 是类型参数的名称,你可以把它看作一个占位符,它将在模板被实例化时被一个具体的类型(如 intdouble)所替代。你也可以使用 class 关键字代替 typename,它们在大多数情况下是等价的。
  • T add(T a, T b):这是函数定义。

    • 返回类型是 T
    • 参数 ab 的类型也都是 T

2.2 函数模板的使用(实例化)

当你调用一个函数模板时,编译器会根据你传入的参数类型来推断模板参数 T 的具体类型,并生成相应的函数代码。这个过程称为模板实例化

#include <iostream>
using namespace std;

template <typename T>
T add(T a, T b) {
cout << "Template add called for type: " << typeid(T).name() << endl;
return a + b;
}

int main() {
int x = 5, y = 10;
double d1 = 3.14, d2 = 2.71;
string s1 = "Hello, ", s2 = "World!";

// 编译器推断 T 为 int
int sum_int = add(x, y);
cout << "Sum (int): " << sum_int << endl;

// 编译器推断 T 为 double
double sum_double = add(d1, d2);
cout << "Sum (double): " << sum_double << endl;

// 编译器推断 T 为 string
string sum_string = add(s1, s2);
cout << "Sum (string): " << sum_string << endl;

return 0;
}

输出

Template add called for type: int
Sum (int): 15
Template add called for type: double
Sum (double): 5.85
Template add called for type: std::basic_string<char, std::char_traits<char>, std::allocator<char> >
Sum (string): Hello, World!

(注意:typeid(T).name() 的输出格式在不同编译器上可能略有不同)

在这个例子中,编译器实际上为我们生成了三个不同的 add 函数:

  • int add(int a, int b)
  • double add(double a, double b)
  • string add(string a, string b)

2.3 模板的显式实例化

有时,你可能希望在调用函数时明确指定模板参数的类型,而不是让编译器去推断。

int main() {
int x = 5;
double d = 3.14;

// 显式指定 T 为 double
double result = add<double>(x, d);
cout << "Result: " << result << endl;

return 0;
}

在这个例子中,x (int类型) 会被隐式转换为 double 类型。

3. 类模板

类模板允许你创建一个通用的类,其成员变量和成员函数的类型可以是模板参数。

3.1 类模板的定义

template <typename T>
class MyArray {
private:
T* data;
int size;

public:
MyArray(int s) : size(s) {
data = new T[size];
}

~MyArray() {
delete[] data;
}

T& operator[](int index) {
if (index < 0 || index >= size) {
throw out_of_range("Index out of bounds");
}
return data[index];
}

int getSize() const {
return size;
}
};
  • template <typename T>:同样是模板声明。
  • class MyArray:类定义。类中的 T 就是模板参数。

3.2 类模板的使用(实例化)

使用类模板时,必须显式指定模板参数。

#include <iostream>
#include <string>
using namespace std;

// ... (MyArray class template definition here) ...

int main() {
// 实例化一个存储 int 的 MyArray
MyArray<int> intArray(5);
for (int i = 0; i < intArray.getSize(); ++i) {
intArray[i] = i * 10;
}
cout << "Integer Array: ";
for (int i = 0; i < intArray.getSize(); ++i) {
cout << intArray[i] << " ";
}
cout << endl;

// 实例化一个存储 string 的 MyArray
MyArray<string> strArray(3);
strArray[0] = "Apple";
strArray[1] = "Banana";
strArray[2] = "Cherry";
cout << "\nString Array: ";
for (int i = 0; i < strArray.getSize(); ++i) {
cout << strArray[i] << " ";
}
cout << endl;

return 0;
}

输出

Integer Array: 0 10 20 30 40 

String Array: Apple Banana Cherry

编译器会为 MyArray<int>MyArray<string> 生成两个完全独立的类代码。

4. 模板参数

模板可以有多种类型的参数:

  1. 类型参数 (Type Parameters)

    • 这是最常见的,用 typenameclass 声明。
    • 例子:template <typename T, class U>
  2. 非类型参数 (Non-type Parameters)

    • 它们是编译期常量,必须是整数类型(int, long, size_t 等)、枚举或指针/引用(指向具有外部链接的对象或函数)。
    • 例子:
      template <typename T, int Size> // Size 是一个非类型参数
      class FixedSizeArray {
      private:
      T data[Size];
      public:
      // ...
      };

      FixedSizeArray<double, 10> arr; // 一个大小为 10 的 double 数组

5. 模板的特化 (Specialization)

有时,对于某个特定的类型,你可能希望模板有不同的行为,而不是使用通用的模板代码。这就是模板特化的用途。

5.1 函数模板特化

#include <iostream>
#include <string>
using namespace std;

template <typename T>
void print(T value) {
cout << "Generic print: " << value << endl;
}

// 对 const char* 类型进行特化
template <>
void print<const char*>(const char* value) {
cout << "Specialized print for const char*: " << value << endl;
}

int main() {
print(42); // 使用通用模板
print(3.14); // 使用通用模板
print("Hello, World!"); // 使用特化的版本

string s = "Hi there";
print(s); // 使用通用模板 (因为 s 是 string 类型,不是 const char*)

return 0;
}

输出

Generic print: 42
Generic print: 3.14
Specialized print for const char*: Hello, World!
Generic print: Hi there

5.2 类模板特化

#include <iostream>
using namespace std;

template <typename T>
class MyContainer {
public:
MyContainer(T val) : value(val) {
cout << "Generic MyContainer constructed with value: " << value << endl;
}
private:
T value;
};

// 对 bool 类型进行特化
template <>
class MyContainer<bool> {
public:
MyContainer(bool val) : flag(val) {
cout << "Specialized MyContainer<bool> constructed with flag: " << (flag ? "true" : "false") << endl;
}
private:
bool flag;
};

int main() {
MyContainer<int> intContainer(100);
MyContainer<double> doubleContainer(3.14);
MyContainer<bool> boolContainer(true);

return 0;
}

输出

Generic MyContainer constructed with value: 100
Generic MyContainer constructed with value: 3.14
Specialized MyContainer<bool> constructed with flag: true

6. 模板的优点

  1. 代码复用:编写一份通用代码,可以用于多种数据类型,极大地减少了代码冗余。
  2. 类型安全:模板在编译时进行类型检查,比使用 void* 等方式更安全。
  3. 效率:模板实例化是在编译期完成的,生成的代码是针对具体类型优化的,没有运行时开销。
  4. 灵活性和可扩展性:可以轻松地为新的数据类型扩展现有模板的功能。C++ 标准库中的许多组件(如 std::vector, std::map, std::sort)都是基于模板实现的。

7. 模板的缺点/注意事项

  1. 编译时间增加:模板实例化会增加编译时间,因为编译器需要为每个使用的类型生成新的代码。
  2. 代码膨胀:如果一个模板被多种类型实例化,生成的目标文件会包含多份相似的代码,可能导致可执行文件变大。现代编译器和链接器会进行优化(如 COMDAT folding)来缓解这个问题。
  3. 错误信息复杂:当模板代码出现错误时,编译器给出的错误信息通常非常冗长和难以理解,因为它涉及到模板实例化的过程。
  4. 调试困难:调试模板代码也比调试普通代码更具挑战性。

8.多个类型参数

1. 模板的多个类型参数

定义格式:

template <typename T, typename D>
返回类型 函数名(参数列表) {
// 函数体
}

  • TD 都是独立的类型参数,可在函数中作为类型使用。
  • 调用时,编译器会根据传入的参数自动推断 TD 的具体类型,也可以显式指定。

2. 示例:多个类型参数的函数模板

#include <iostream>
#include <string>
using namespace std;

template <typename T, typename D>
void printPair(T a, D b) {
cout << "(" << a << ", " << b << ")" << endl;
}

int main() {
printPair(10, 3.14); // T=int, D=double
printPair("Hello", 5); // T=const char*, D=int
printPair(string("Alice"), true); // T=string, D=bool
return 0;
}

输出:

(10, 3.14)
(Hello, 5)
(Alice, 1)

3. 类模板的多个类型参数

类模板也支持多个类型参数,例如:

template <typename Key, typename Value>
class Pair {
private:
Key key;
Value value;
public:
Pair(Key k, Value v) : key(k), value(v) {}

Key getKey() const { return key; }
Value getValue() const { return value; }
};

int main() {
Pair<int, string> p1(1, "Apple");
Pair<string, double> p2("PI", 3.1415);

cout << p1.getKey() << ": " << p1.getValue() << endl;
cout << p2.getKey() << ": " << p2.getValue() << endl;
return 0;
}

输出:

1: Apple
PI: 3.1415

4. 多个类型参数 + 非类型参数

模板还可以同时包含类型参数非类型参数

template <typename T, typename D, int N>
void printArray(T arr1[N], D arr2[N], int size) {
for (int i = 0; i < size; ++i) {
cout << arr1[i] << " -> " << arr2[i] << endl;
}
}

int main() {
int nums[3] = {1, 2, 3};
string strs[3] = {"A", "B", "C"};
printArray<int, string, 3>(nums, strs, 3);
return 0;
}

输出:

1 -> A
2 -> B
3 -> C

5. 显式指定模板参数

调用时,可以显式指定部分或全部类型参数:

printPair<int, double>(10, 3.14);  // 显式指定 T=int, D=double
printPair<double>(20, 5.5); // 显式指定 T=double,D由参数推断为double