14 模板
泛型编程
泛型编程(Generic Programming)是一种编程范式,核心思想是编写与具体数据类型无关的代码,让算法和数据结构能够自动适配多种类型,而不需要为每种类型重复写同样的逻辑。
C++ 中的实现:模板(Template)
泛型编程是一种编译时多态性(静态多态)。
实例化(Instantiation):由编译器将通用模板代码转换为不同的实例代码的过程。
函数模板
语法格式:
template <typename 模板形参1, typename 模板形参2> // 声明模板
返回类型 函数名(参数列表) {
// 函数体
}
函数模板的模板形参不一定要出现在形参列表里,但如果某个模板参数在调用时不能通过函数参数推导出来,就必须由调用者显式指定或给出默认参数。
- 如果模板形参出现在函数形参类型中,编译器会尝试从实参中推导它们(常见情况)。
- 如果某形参只出现在返回类型或函数体中,则不能从调用处推导,调用时须显式指定或该形参必须有默认值。
- 非类型模板参数同样需要能被推导,否则须显式提供。
示例:
- 不能被推导(调用时需显式指定):
template<typename T, typename U> void f(T);//U未出现在参数列表,不能推导 →f<int,double>(42); - 有默认值:
template<typename T, typename U = int> void f(T);// 可以写f(42);,U默认为int - 只靠返回值无法推导:
template<typename T> T g();// 必须g<int>()
经典示例(交换函数):
template <typename T>
void mySwap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
调用方式
- 自动类型推导:编译器根据传入的实参自动推断出
T的类型。int a = 10, b = 20; mySwap(a, b); // 编译器自动推导出 T 为 int - 显式指定类型:在函数名后的尖括号中明确指定类型。
double x = 1.1, y = 2.2; mySwap<double>(x, y); // 明确指定 T 为 double
注意:此过程中不进行常规的隐式类型转换。
(不能把一个int和一个double传给mySwap)
模板(全)特化
模板特化(Template Specialization)是指为特定的类型参数组合,提供一份定制的模板实现,替代编译器自动生成的通用版本。
全特化:为所有模板参数提供具体类型的定制实现:
// 通用模板
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}
// 全特化:针对 const char* 的定制版本
template<> // 空参数列表
const char* max(const char* a, const char* b) {
return strcmp(a, b) > 0 ? a : b; // 比较字符串内容
}
模板重载
对函数模板进行重载:
- 定义名字相同而函数形参表不同的函数模板
- 或者定义与函数模板同名的非模板函数(正常函数),在其函数体中完成不同的行为
函数调用的静态绑定规则(重载协议):
- 如果某一同名非模板函数(指正常的函数)的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数。否则,进入第2步
- 如果能从同名的函数模板实例化一个函数实例,而该函数实例的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数模板的实例函数。否则,进入第3步
> 在步骤2中:首先匹配函数模板的特化,再匹配非指定特殊的函数模板 - 对函数调用的实参进行隐式类型转换后与非模板函数再次进行匹配,若能找到匹配的函数则调用该函数。否则,进入第4步
- 提示编译错误
类模板
语法格式:
template <模板形参表>
class 类模板名
{
类成员声明
...
}
其中模板形参表的形式为:template <typename 类型参数1, typename 类型参数2, ...>
(注:模板形参每项是非类型形参、类型形参、模板形参之一。)
示例
// 1. 定义类模板
template <typename T>
class Stack {
private:
T* arr;
int topIndex;
public:
void push(T value) { /* 入栈逻辑 */ }
T pop() { /* 出栈逻辑 */ }
};
// 2. 显式实例化并创建对象
Stack<int> intStack;
Stack<double> doubleStack;
在类模板外定义成员函数
语法格式
template <模板形参表>
返回值类型 类模板名<模板形参名列表>::函数名(函数形参表)
{
函数实现
...
}
示例
template <typename T>
void MyClass<T>::show(T val) { }
类模板的文件组织形式
类模板(以及函数模板)在实例化时,编译器必须能看到完整的源代码(头文件中的模板定义)。
普通类在编译时,编译器只需要看到类的声明(通常放在头文件中)即可,而成员函数的具体实现可以放在源文件(.cpp)中。但类模板不同,编译器只有在遇到具体的类型(如 MyClass<int>)进行实例化时,才会生成真正的机器代码。
解决方案:
一、包含编译模式
- 核心要求 在模板(函数模板/类模板成员函数)被调用实例化的位置,编译器必须能看到该模板的完整实现代码,才能根据传入的类型生成对应的实体代码。
- 两种实现手段
- 方案1:把类模板的声明、成员函数实现全部直接写在头文件(
.h/.hpp)里,包含头文件就能看到全部代码; - 方案2:头文件只写类模板声明,在头文件末尾用
#include "模板实现文件.tpp"引入存放成员实现的文件,调用处包含头文件时就会连带引入实现代码。
- 方案1:把类模板的声明、成员函数实现全部直接写在头文件(
- 特点:所有主流C++编译器都支持,工程里最普遍使用。
.tpp 文件通常用于将 C++ 模板类或模板函数的实现与声明分离,同时避免链接错误。
二、分离编译模式
- 核心要求
像普通类一样把声明放头文件、实现放单独
.cpp文件,需要在实现文件用export关键字修饰模板,让编译器跨编译单元保留模板定义信息,后续实例化时能找到实现。 - 局限
C++标准虽定义了该模式,但绝大多数主流编译器(如GCC、Clang早期版本、MSVC)都不完整支持
export关键字,实际开发几乎不会采用这种方式。
非类型模板形参
- 相当于模板内部的常量,形式上类似于普通的函数形参
- 对模板进行实例化时,非类型形参由相应模板实参的值代替
- 对应的模板实参必须是编译时常量表达式
- C++20以前,非类型模板形参只允许是整型、枚举、指针、引用或指向成员类型的指针。不能是 float、double、string 等。
template<typename T, int N> // T 是类型参数,N 是非类型参数
class Array {
T data[N]; // N 是编译期确定的常量
};
Array<int, 100> arr; // N = 100,编译期分配 100 个 int
数组的引用
数组的引用就是给整个数组起别名,而不是指向数组首元素的指针。
int arr[5] = {1, 2, 3, 4, 5};
int (&ref)[5] = arr;
// ↑ 括号不能省!int& ref[5] 会变成"引用数组"(非法)
数组指针 int* |
数组引用 int (&)[5] |
|
|---|---|---|
| 类型 | 指向 int 的指针 |
绑定到"5 个 int 的数组" |
sizeof |
sizeof(int*)(4/8 字节) |
sizeof(int[5])(20 字节) |
| 能否重新绑定 | 可以指向其他地址 | 不能,引用一旦绑定不可改 |
| 退化行为 | 数组名会退化为指针 | 保持数组身份,不退化 |
| 元素个数信息 | 丢失 | 保留 |
数组引用是模板推导数组大小的经典技巧:
template<typename T, size_t N>
size_t array_size(T (&arr)[N]) { // 推导 N
return N;
}
int a[10];
array_size(a); // 编译器推导 T=int, N=10,不需要传入N
这比 sizeof(arr)/sizeof(arr[0]) 更安全,因为传入指针会编译错误(无法匹配数组引用)。
数组引用 T (&)[N] 是整个数组的别名,它阻止了数组到指针的退化,保留了元素个数信息。 它是模板元编程中推导数组大小的利器,也是函数参数传递数组时避免信息丢失的精确方式。