目录
C++ 模板是泛型编程的核心机制,它允许开发者编写与具体类型无关的代码,通过 “参数化类型” 实现代码的复用。模板不是直接编译执行的代码,而是编译器生成具体代码的 “蓝图”,编译器会根据实际传入的类型参数自动生成对应版本的函数或类。
下面从其底层原理、分类、特性以及高级用法来进行讲解:
💫一、模板的底层本质:编译期代码生成
模板的核心是 “类型参数化”,也就是把代码中具体的类型(比如int、double)抽象为一个 “类型变量”(通常用T表示)。当代码中使用模板时,编译器会执行以下操作:
- 检查模板语法是否合法(比如括号匹配、关键字正确);
- 根据传入的类型参数(如
int、std::string),生成一份针对该类型的具体代码(称为 “模板实例化”); - 对生成的具体代码进行编译,会和普通代码一样参与链接。
示例:
#include <iostream>
template <typename T>
T add(T a, T b) { return a + b; }
int main() {
auto n = add(1, 2); // 编译器生成 add<int>(int, int)
auto m = add(1.5, 2.5); // 编译器生成 add<double>(double, double)
std::cout << "n:" << n << std::endl; // 输出:3
std::cout << "m:" << m << std::endl; // 输出:4
}
template关键字开始模板声明<typename T>或<class T>定义模板参数,T是类型参数(一般还是推荐使用 typename)T可以在函数中像普通类型一样使用
上述代码中,add函数模板会被实例化为两个函数:add<int>和add<double>,分别处理int和double类型的加法。
💫二、核心分类:函数模板和类模板
✨1、函数模板:通用函数的实现
- 目标:定义可以处理多种类型的通用函数,避免因为不同类型而重复编写逻辑相同的函数,提高代码的复用性。
- 特点:通过参数类型推导自动生成具体函数版本,可以支持重载和非类型参数。
1️⃣基本语法
template <typename T> // 模板参数声明(typename可替换为class)
返回值类型 函数名(参数列表) {
// 函数体(使用T作为类型)
}
2️⃣类型推导
🔥①自动推导类型:调用函数模板时,编译器会根据实参类型自动推断T的类型(无需显式指定)
template <typename T>
void print(T value) {
std::cout << typeid(T).name() << std::endl;
std::cout << value << std::endl;
}
print(100); // 推导T=int,生成print<int>
print("hello"); // 推导T=const char*,生成print<const char*>
❓❓❓为什么“hello”会被推导为const char * 而不是std::string呢?
第一点:模板类型推导遵循精确匹配原则。字符串字面量 "hello" 在 C++ 中的类型是 const char[6](包含结尾的 \0),在函数参数传递时会退化为 const char*。
第二点:模板类型推导期间不会进行用户定义的隐式转换。即使 const char* 可以隐式转换为 std::string,但是这发生在函数调用时,而不是模板推导时。
🔥②显式指定类型:当推导失败(比如参数类型不匹配)时,需显式指定T
template <typename T>
void printSize(T value) {
std::cout << "Size: " << sizeof(value) << std::endl;
}
int main() {
int x = 42;
printSize(x); // T = int, size = 4
printSize<double>(x); // 显式指定:T = double, size = 8
// 强制进行 int -> double 转换
}
3️⃣模板参数和非类型参数
- 类型参数:即
typename T中的T,代表一种数据类型(如int、double,或者自定义类)。 - 非类型参数:编译期已知的常量(如整数、指针、引用),用于指定固定属性(如数组大小)。
// 非类型参数N:指定数组大小(编译期常量)
template <typename T, int N>
void printArray(T (&arr)[N]) {
for (int i = 0; i < N; ++i) {
std::cout << arr[i] << std::endl;
}
}
int main() {
int arr1[3] = {1, 2, 3};
printArray(arr1); // 推导T=int,N=3,生成printArray<int, 3>
double arr2[2] = {1.1, 2.2};
printArray(arr2); // 推导T=double,N=2,生成printArray<double, 2>
}
非类型参数必须是编译期常量(如字面量、constexpr变量),不能是运行时变量。
4️⃣函数模板的重载(也就是函数模板的全特化)
函数模板可以与普通函数、其他模板重载,编译器按 “最佳匹配” 原则选择调用版本:
- 全特化优先级高于模板(前提是精确匹配时);
- 模板间按 “更特例化” 原则选择(通俗的讲,就是如果模板 A 的适用范围是模板 B 适用场景的 “子集”,也就是A的生效范围小于B的,则 A 比 B 更特例化,优先被选择。比如
T与const T,后者针对性更强,更特例化)。
// 函数模板:处理通用类型
template <typename T>
void swap(T& a, T& b) {
std::cout << "模板交换" << std::endl;
T temp = a;
a = b;
b = temp;
}
// 函数模板的全特化:处理int类型
template <>
void swap(int& a, int& b) {
std::cout << "普通int交换" << std::endl;
std::swap(a, b);
}
int main() {
int x = 1, y = 2;
swap(x, y); // 调用全特化版本(精确匹配)
double a = 1.1, b = 2.2;
swap(a, b); // 调用模板(推导T=double)
}
✨2、类模板:通用类的实现
类模板允许定义通用的类,通过类型参数化,实现不同类型的对象。
1️⃣基本语法:
template <typename T> // 模板参数声明
class ClassName {
private:
T member; // 用T定义成员变量
public:
ClassName(T val) : member(val) {} // 构造函数
T get() const { return member; } // 成员函数
};
// 类外定义成员函数(需重复模板参数)
template <typename T>
void 类名<T>::set(T val) {
member = val;
}
2️⃣实例化:
使用类模板时必须显式指定类型参数(因为编译器无法推导)
// 实例化:生成Class<int>类
ClassName<int> intObj(10);
std::cout << intObj.get() << std::endl; // 输出10
// 实例化:生成Class<std::string>类
ClassName<std::string> strObj("hello");
std::cout << strObj.get() << std::endl; // 输出hello
要点:
- 类模板可以有多个类型参数。
- 模板参数可以被用于成员变量和成员函数中。
- 类模板实例化时指定具体类型。
3️⃣类模板的特化
当通用模板对某些类特殊类型不适用时,可通过 “特化” 提供自定义实现(分为全特化和偏特化)。
全特化:
为特定类型完全定制模板实现。它提供了模板的一个特定版本,当模板参数完全匹配特化类型时,编译器将优先使用该特化版本。
// 通用模板
template <typename T>
class Printer {
public:
void print(T val) {
std::cout << "通用打印:" << val << std::endl;
}
};
// 对bool类型的全特化
template <> //!全特化无模板参数
class Printer<bool> {
public:
void print(bool val) {
std::cout << "bool特化打印:" << (val ? "true" : "false") << std::endl;
}
};
int main() {
Printer<int> intP;
intP.print(123); // 通用打印:123
Printer<bool> boolP;
boolP.print(true); // bool特化打印:true
}
解析
- 通用模板适用于所有类型,在
print函数中以通用方式输出值。 - 全特化模板针对
bool类型进行了专门化,实现了不同的print函数。 - 当实例化
Printer<bool>时,编译器选择全特化版本而非通用模板。
偏特化:
偏特化允许模板对部分参数进行特定类型的处理,同时又保持其他参数的通用性。
对于类模板而言,可以针对模板参数的某些特性进行偏特化;对于函数模板,则仅支持全特化,不支持偏特化。
// 多参数通用模板
template <typename T1, typename T2>
class Pair {
public:
T1 first;
T2 second;
Pair(T1 f, T2 s) : first(f), second(s) {}
};
// 偏特化:第一个参数为int
template <typename T2> // 仅特化T1=int,T2仍为模板参数
class Pair<int, T2> {
public:
int first;
T2 second;
Pair(int f, T2 s) : first(f), second(s) {
std::cout << "int特化Pair:" << f << ", " << s << std::endl;
}
};
int main() {
Pair<double, std::string> p1(1.5, "hello"); // 通用模板
Pair<int, double> p2(10, 3.14); // 偏特化模板(T1=int)
}
解析
- 偏特化模板处理第一个类型为int的情况.
- 使用偏特化提升了模板的灵活性,使其能够根据部分参数类型进行不同处理。
💫三、一些模板的高级特性
1️⃣模板的递归与编译期计算
模板可通过递归实现编译期计算(利用模板实例化的递归展开),是constexpr的前身。
示例:编译期计算阶乘
template <int N>
struct Factorial {
static constexpr int value = N * Factorial<N-1>::value; // 递归
};
// 递归终止条件(全特化)
template <>
struct Factorial<0> {
static constexpr int value = 1;
};
int main() {
constexpr int f5 = Factorial<5>::value; // 编译期计算:5! = 120
std::cout << f5 << std::endl; // 输出120
}
2️⃣模板参数的默认值
模板参数(类型参数或非类型参数)可指定默认值,使用时若不传入则采用默认值。
// 类型参数默认值:默认T为int
template <typename T = int>
class Stack {
private:
std::vector<T> v;
public:
void push(T elem) { v.push_back(elem); }
};
// 非类型参数默认值:默认Size为10
template <typename T, int Size = 10>
class Array {
private:
T data[Size];
};
int main() {
Stack<> intStack; // 采用默认T=int
Stack<std::string> strStack; // 显式指定T=std::string
Array<int> arr1; // 采用默认Size=10
Array<double, 5> arr2; // 显式指定Size=5
}
3️⃣可变参数模板(C++11及之后的版本)
支持任意数量的模板参数(类型参数或非类型参数),用于实现灵活的函数或类(如std::tuple、printf风格函数)。
示例:可变参数模板函数
// 递归终止函数(无参数)
void print() {
std::cout << std::endl;
}
// 可变参数模板:展开第一个参数,递归处理剩余参数
template <typename T, typename... Args> // Args为参数包
void print(T first, Args... rest) { // 展开第一个参数first
std::cout << first << std::endl;
print(rest...); // 递归处理剩余参数
}
int main() {
print(1, 2.5, "hello", true); // 输出:1 2.5 hello 1
}
💫四、模板的优缺点
1️⃣优点:
- 代码复用性强:一套模板适配多种类型,避免重复编写相似代码(如
std::vector支持所有类型)。 - 类型安全:编译期检查类型兼容性,比
void*等 C 风格泛型更安全。 - 零运行时开销:模板实例化在编译期完成,生成的代码与手写具体类型代码效率一致(无动态多态的虚函数调用开销)。
2️⃣缺点:
- 编译时间长:模板实例化会生成大量代码,增加编译时间。
- 错误信息复杂:模板错误提示冗长(如 “无法推导模板参数”),调试难度大。
5423

被折叠的 条评论
为什么被折叠?



