C++函数模板

模板是 C++ 泛型编程 的基础,属于编译期多态,核心思想是「类型参数化」------ 让同一份代码适配不同数据类型,所有逻辑在编译期生成对应类型的具体代码,运行时零额外开销。以下覆盖六大核心考点,兼顾原理、语法与面试高频点。


一、函数模板

1. 核心本质

函数模板是「生成函数的模具」:用模板参数把函数的参数、返回值类型抽象化,编译器在编译期根据调用时的实参类型自动推导模板参数,并实例化出对应类型的独立函数代码。

  • 函数模板本身不是函数,不生成二进制代码;只有被具体类型实例化后,才会产生真实的函数。
  • 每一种不同的类型,都会生成一份独立的函数二进制(比如 intdouble 各一份),彼此互不影响。

2. 关键特性

  1. 自动类型推导:调用时可以不显式写类型,编译器根据实参自动推导;
  2. 严格类型匹配 :模板推导不会做隐式类型转换(比如 intlong 不能自动匹配到同一个 T),类型不匹配时需要显式指定类型;
  3. 编译期生成:所有实例化都在编译期完成,运行时性能和普通函数完全一致。

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. 允许的参数类型

  • 整型、枚举类型(intsize_tbool 等,最常用)
  • 指针类型、引用类型
  • 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 是什么,只做两件事:

  1. 检查模板本身的语法错误;
  2. 查找非依赖名称 :不依赖模板参数 T 的变量、函数、类型,直接在模板定义处的上下文查找绑定。

第二阶段:模板实例化点(用具体类型实例化时)

编译器此时知道了具体的 T,做一件事:

  1. 查找依赖名称 :依赖模板参数 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
};
相关推荐
用户298698530141 小时前
Word 转 PDF 的 3 种自动化实现:从桌面操作到后端服务集成
java·人工智能·后端
我是苏苏1 小时前
WPF开发01:基础控件及常用模板篇
开发语言·c#·html·wpf
wuyk5551 小时前
89.嵌入式内存管理的“稳定利器”:内存池原理与C语言实现
c语言·开发语言·stm32·单片机·嵌入式硬件
Zane19941 小时前
线程池进阶:如何科学设置线程数(结合实际业务场景复盘)
java·后端
SeaTunnel2 小时前
Apache SeaTunnel 提交一个任务都经过了什么?
java·大数据·服务器·apache·etl·seatunnel
mifengxing2 小时前
Java TreeSet
java·开发语言
编程风暴2 小时前
零基础数据分析项目模板+5条避坑红线
java·数据挖掘·数据分析
IKUN家族2 小时前
常见的依懒
java·服务器·数据库
m0_519196402 小时前
【设计模式】java的习题
开发语言·python