模板是 C++ 泛型编程 的基础,属于编译期多态,核心思想是「类型参数化」------ 让同一份代码适配不同数据类型,所有逻辑在编译期生成对应类型的具体代码,运行时零额外开销。以下覆盖六大核心考点,兼顾原理、语法与面试高频点。
一、函数模板
1. 核心本质
函数模板是「生成函数的模具」:用模板参数把函数的参数、返回值类型抽象化,编译器在编译期根据调用时的实参类型自动推导模板参数,并实例化出对应类型的独立函数代码。
- 函数模板本身不是函数,不生成二进制代码;只有被具体类型实例化后,才会产生真实的函数。
- 每一种不同的类型,都会生成一份独立的函数二进制(比如
int和double各一份),彼此互不影响。
2. 关键特性
- 自动类型推导:调用时可以不显式写类型,编译器根据实参自动推导;
- 严格类型匹配 :模板推导不会做隐式类型转换(比如
int和long不能自动匹配到同一个T),类型不匹配时需要显式指定类型; - 编译期生成:所有实例化都在编译期完成,运行时性能和普通函数完全一致。
3. 代码示例
cpp
运行
// 函数模板定义
template<typename T>
T maxVal(T a, T b) {
return a > b ? a : b;
}
int main() {
maxVal(10, 20); // 自动推导 T=int,实例化 int 版本函数
maxVal(3.14, 2.71); // 自动推导 T=double,实例化 double 版本函数
maxVal<int>(5, 8L); // 显式指定 T=int,强制按 int 版本处理
return 0;
}
二、类模板
1. 核心本质
将类的成员变量、成员函数的类型参数化,生成通用的类模具。和函数模板不同,C++17 之前类模板无法自动推导类型 ,使用时必须显式指定模板参数,例如 std::vector<int>。
2. 核心特性:成员函数按需实例化(面试高频)
类模板的成员函数不会随类一起全部实例化,而是只有被调用、被使用的成员函数才会被实例化;未使用的成员函数仅做基础语法检查,不会生成真实代码。
这也是为什么 vector 可以存放不支持 < 比较的类型 ------ 只要不调用排序相关方法,就不会编译报错,因为对应的成员函数根本没有被实例化。
3. 代码示例
cpp
运行
template<typename T>
class Box {
private:
T value;
public:
Box(T v) : value(v) {}
T getValue() const { return value; }
void print() const {
value.print(); // 调用 T 的 print 方法
}
};
int main() {
Box<int> b(10); // 显式指定 T=int,实例化 int 版本的 Box 类
b.getValue(); // 只有被调用的成员函数才会被实例化
// b.print(); // 打开才会实例化 print;int 没有 print 方法,此时才会编译报错
return 0;
}
补充:C++17 类模板实参推导(CTAD)
C++17 之后支持根据构造函数实参自动推导类模板参数,简化写法:
cpp
运行
std::vector v{1,2,3}; // 自动推导为 vector<int>,无需显式写 <int>
Box b(3.14); // 自动推导为 Box<double>
三、模板特化
模板特化是针对特定类型或某一类类型,重写一份定制化的实现,覆盖通用模板的默认逻辑。分为「全特化」和「偏特化」两类。
1. 全特化
对模板的所有参数都指定具体类型,完全重写实现,优先级高于通用模板。
- 函数模板和类模板都支持全特化。
类模板全特化示例
cpp
运行
// 通用主模板
template<typename T>
class TypeTraits {
public:
static bool isInteger() { return false; }
};
// 针对 int 的全特化版本
template<>
class TypeTraits<int> {
public:
static bool isInteger() { return true; }
};
函数模板全特化示例
cpp
运行
// 通用主模板
template<typename T>
bool isEqual(T a, T b) { return a == b; }
// 针对 const char* 的全特化,改为字符串比较
template<>
bool isEqual<const char*>(const char* a, const char* b) {
return strcmp(a, b) == 0;
}
2. 偏特化(部分特化)
只指定部分模板参数,或者针对某一类类型做特化(比如所有指针类型、所有引用类型)。
- ❗ 重要规则:只有类模板支持偏特化,函数模板不支持偏特化;函数模板想实现类似效果,优先用函数重载。
常见场景:针对所有指针类型 T* 偏特化
cpp
运行
// 通用主模板
template<typename T>
class PointerWrapper {
public:
void print() { std::cout << "普通类型" << std::endl; }
};
// 偏特化:针对所有指针类型 T*
template<typename T>
class PointerWrapper<T*> {
public:
void print() { std::cout << "指针类型,解引用访问" << std::endl; }
};
3. 匹配优先级
全特化 > 偏特化 > 通用主模板;编译器会优先匹配最具体、最匹配的版本。
四、非类型模板参数
1. 核心概念
模板参数不仅可以是「类型」,也可以是编译期常量值,称为非类型模板参数。它是模板类型的一部分,在编译期就确定取值,用于定义编译期固定大小、常量配置等场景。
2. 允许的参数类型
- 整型、枚举类型(
int、size_t、bool等,最常用) - 指针类型、引用类型
- C++20 起支持浮点类型、字面量类类型
3. 最典型应用:std::array
cpp
运行
// std::array 的本质就是类模板 + 非类型参数
template<typename T, size_t N>
class array {
T data[N]; // 编译期确定数组大小,栈上分配连续内存
};
// 使用:第二个参数是编译期常量
std::array<int, 10> arr; // 10 就是非类型模板参数
4. 注意事项
- 非类型参数必须是编译期常量,不能用运行时变量传参;
- 不同的非类型参数值,属于完全不同的类型(例如
array<int,5>和array<int,10>是两个无关的类)。
五、模板编译的两阶段名称查找
模板的编译分为两个独立阶段,这是很多诡异编译错误的根源,也是面试高频考点。 核心规则:不依赖模板参数的名字,在模板定义时查找;依赖模板参数的名字,在实例化时查找。
第一阶段:模板定义点(模板本身被编译时)
编译器此时还不知道具体的 T 是什么,只做两件事:
- 检查模板本身的语法错误;
- 查找非依赖名称 :不依赖模板参数
T的变量、函数、类型,直接在模板定义处的上下文查找绑定。
第二阶段:模板实例化点(用具体类型实例化时)
编译器此时知道了具体的 T,做一件事:
- 查找依赖名称 :依赖模板参数
T的变量、函数、类型,在实例化点的上下文查找,同时会触发 ADL(参数依赖查找)------ 根据参数类型去对应命名空间查找函数。
代码示例演示差异
cpp
运行
void foo(int) { std::cout << "全局 foo(int)" << std::endl; }
template<typename T>
void test(T x) {
foo(10); // 非依赖名:和 T 无关,第一阶段就绑定全局 foo
foo(x); // 依赖名:和 T 有关,第二阶段实例化时查找
}
namespace MyNS {
struct MyType {};
void foo(MyType) { std::cout << "MyNS::foo" << std::endl; }
}
int main() {
test(100);
// foo(10) 调用全局 foo(int)
// foo(x) 也调用全局 foo(int)
MyNS::MyType obj;
test(obj);
// foo(10) 还是调用全局 foo(int)(第一阶段已确定)
// foo(x) 触发 ADL,找到 MyNS::foo(MyType)(第二阶段查找)
return 0;
}
六、typename vs class 关键字
1. 声明模板参数时:完全等价
在 template<typename T> 和 template<class T> 中,两个关键字没有任何区别,功能完全一致,可以互换。
class是早期 C++ 的写法,typename是后来为了语义更清晰引入的。
2. 嵌套依赖类型:必须用 typename(核心考点)
当模板内部引用一个依赖于模板参数 T 的嵌套类型 时,必须加 typename 前缀,作用是告诉编译器:「这是一个类型名,不是静态成员变量」。
错误写法 vs 正确写法
cpp
运行
template<typename T>
void func() {
// ❌ 错误:编译器默认把 T::value_type 当成静态成员变量,不是类型
T::value_type val;
// ✅ 正确:typename 明确告诉编译器这是一个嵌套类型
typename T::value_type val;
}
为什么会有歧义?
因为 T::xxx 语法既可以指代嵌套类型,也可以指代静态成员变量;编译器默认会优先识别为变量,只有加了 typename 才会识别为类型。
例外场景
在类的基类列表 、成员初始化列表的基类标识 中,嵌套依赖的基类类型不需要加 typename,编译器会自动识别为类型:
cpp
运行
template<typename T>
class Derived : public T::BaseType { // 基类列表中,无需 typename
public:
Derived() : T::BaseType() {} // 初始化列表的基类构造,无需 typename
};