阅读须知:每个部分中的示例代码相连

一、模板的基本概念

模板是 C++泛型编程的核心工具,允许编写与类型无关的代码,提高代码复用性。模板分为函数模板类模板,编译器会根据调用时的具体类型,生成对应的 “实例化代码”。

二、函数模板

1. 定义与语法

用 template<typename T> 或 template<class T>typename 和 class 在此处等价)声明模板参数列表,随后定义函数逻辑。

示例:

template<class T>
void Swap(T& left, T& right) {
    T temp = left;
    left = right;
    right = temp;
}

2. 模板实例化

编译器根据调用时的实参类型,自动生成对应类型的函数(即 “实例化”)。分为两种方式:

(1)隐式实例化

编译器根据实参自动推演模板参数 T 的类型,生成函数实例。

示例:

int x = 0, y = 1;
Swap(x, y); // 推演T为int,生成 void Swap(int&, int&)

double d1 = 1.1, d2 = 2.2;
Swap(d1, d2); // 推演T为double,生成 void Swap(double&, double&)
(2)显式实例化

手动指定模板参数,格式为 函数模板名<具体类型>(实参)

示例:

template<class T>
T Add(const T& left, const T& right) {
    return left + right;
}

// 显式指定T为int,将double隐式转换为int后相加
cout << Add<int>(10, 20.5) << endl; 

// 显式指定T为double,将int隐式转换为double后相加
cout << Add<double>(10, 20.5) << endl; 

3. 函数模板与普通函数的重载匹配

  • 规则:编译器优先匹配普通函数(有 “现成” 实现时);若普通函数不匹配,再考虑函数模板的实例化

示例:

// 普通函数
int Add(int left, int right) {
    return left + right;
}

// 函数模板
template<class T>
T Add(T left, T right) {
    return left + right;
}

int main() {
    Add(1, 2); // 优先调用普通函数 int Add(int, int)
    Add<int>(1, 2); // 显式指定模板,调用函数模板实例化的 int Add(int, int)
    Add(1.1, 2); // 普通函数不匹配(无double+int的普通Add),调用模板(2隐式转为double)
}

三、类模板

1. 定义与语法

用 template<class T1, class T2, ...> 声明模板参数,随后定义类结构。

示例(栈的类模板(栈实际用的是容器适配器,此处栈实现更像是array实现)):

template<class T, size_t N = 10> // N为“非类型模板参数”,默认值10
class Stack {
private:
    T _a[N];    // 用模板参数指定数组大小
    size_t _top;
public:
    Stack() : _top(0) {}
    void push(const T& val) { /* 入栈逻辑 */ }
    void pop() { /* 出栈逻辑 */ }
    // 其他成员函数...
};

2. 类模板的使用

类模板不能隐式推演类型,必须显式指定模板参数

示例:

Stack<int, 20> s1; // T为int,N为20(覆盖默认值)
Stack<double> s2;  // T为double,N使用默认值10

3. 成员函数的实例化

类模板的成员函数在调用时才会实例化(未被调用的成员函数,不会生成对应的二进制代码)。

成员函数可在类内定义,也可在类外定义(类外定义需显式指定模板参数):

template<class T, size_t N>
void Stack<T, N>::push(const T& val) {
    if (_top < N) {
        _a[_top++] = val;
    }
}

进阶部分:

四、模板特化(Specialization)

为模板的特定类型提供 “定制化” 实现,解决通用模板在某些类型下的行为不符合预期的问题(如指针比较时,默认比较 “指针值” 而非 “指向对象的内容”)。

1. 函数模板特化

步骤:
  1. 先声明通用函数模板
  2. 再定义特化版本,格式为 template<> 返回值 函数名<特化类型>(参数列表)
示例(指针比较:比较指向对象的内容,而非指针本身):
// 通用模板:比较两个对象的大小
template<class T>
bool Less(const T& left, const T& right) {
    return left < right;
}

// 特化版本:针对T为Date*的情况
template<>
bool Less<Date*>(Date* const& left, Date* const& right) {
    return *left < *right; // 比较指针指向的Date对象内容
}
注意:

特化时,函数参数类型要与通用模板推演后的类型完全匹配(如示例中用 Date* const& 匹配 “引用传递的指针”)。

2. 类模板特化

分为全特化偏特化(部分特化和限制)。

(1)全特化

对模板参数的所有可能都指定具体类型,格式为 template<> class 类名<具体类型1, 具体类型2>

示例:

// 通用类模板
template<class T1, class T2>
class Data {
public:
    Data() { cout << "Data<T1, T2>" << endl; }
};

// 全特化:T1为int,T2为char
template<>
class Data<int, char> {
public:
    Data() { cout << "Data<int, char>" << endl; }
};
(2)偏特化(部分特化和限制)

对模板参数的部分进行限制,或对参数的特性(如指针、引用、常量等)进行限制。

  • 示例 1:限制 “部分模板参数类型”(如第二个参数为char):

    template<class T1>
    class Data<T1, char> {
    public:
        Data() { cout << "Data<T1, char>" << endl; }
    };
    
  • 示例 2:限制 “模板参数为指针类型”:

    template<class T1, class T2>
    class Data<T1*, T2*> {
    public:
        Data() { cout << "Data<T1*, T2*>" << endl; }
    };
    
  • 示例 3:限制 “模板参数为引用类型”:

    template<class T1, class T2>
    class Data<T1&, T2&> {
    public:
        Data() { cout << "Data<T1&, T2&>" << endl; }
    };
    
  • 示例 4:混合限制(如第一个为指针,第二个为引用):

    template<class T1, class T2>
    class Data<T1*, T2&> {
    public:
        Data() { cout << "Data<T1*, T2&>" << endl; }
    };
    

五、非类型模板参数

模板参数可以是常量表达式(而非仅 “类型”),这类参数称为 “非类型模板参数”。

1. 规则与限制

  • 非类型模板参数的类型只能是:整数类型intsize_t等)、枚举类型指针(对象 / 函数指针)、引用
  • 浮点数(如double)、类对象(如std::string不能作为非类型模板参数(C++20 前不支持;C++20 开始部分支持浮点数,但兼容性需注意)。

2. 示例

用非类型模板参数指定数组大小:

template<class T, size_t N = 10> // size_t是无符号整数类型,N为非类型参数,默认值10
class Stack {
private:
    T _a[N]; // 数组大小由非类型参数N决定
    size_t _top;
public:
    Stack() : _top(0) {}
    // ...
};

// 使用示例
Stack<int, 20> s1; // 显式指定N为20
Stack<double> s2;  // 使用默认N=10

六、模板的编译机制

  • 模板是 “蓝图 / 配方”,不是真正的可执行代码,只有在实例化时才会生成具体的函数 / 类代码。
  • 模板的声明和定义通常放在同一个头文件中(若分离到.cpp,其他文件调用时可能因看不到模板定义而无法实例化,导致链接错误)。

当 func.h 只有模板声明func.cpp 只有模板定义时,会直接导致链接错误,原因是模板实例化需要 “可见的完整定义”,而分离后编译阶段无法生成所需的实例化代码。

模板声明和定义分离(使得链接报错)的详细分析(结合编译流程):

1. test.cpp 的编译过程
// test.cpp
#include "func.h"
int main() {
    vector<int> v = {1,2,3};
    Print(v); // 调用 Print<vector<int>>
    return 0;
}
  • func.h 中只有 Print 模板的声明(无定义)。
  • 编译 test.cpp 时,编译器看到 Print(v),需要实例化 Print<vector<int>>,但缺少模板的完整定义,因此无法生成 Print<vector<int>> 的具体代码,只能记录 “需要调用 Print<vector<int>>,但函数未定义”。
2. func.cpp 的编译过程
// func.cpp
#include "func.h"
// 模板定义(但 func.cpp 中没有调用 Print 的代码)
template<class Container>
void Print(const Container& con) {
    // 遍历容器的逻辑...
}
  • func.cpp 中有 Print 模板的完整定义,但 func.cpp 自身没有调用 Print 的代码无法用具体类型触发模板实例化)。
  • 编译 func.cpp 时,编译器不会主动生成 “所有可能的 Print 实例化代码”(模板实例化是 “按需生成” 的),因此 func.cpp 编译后,不会包含 Print<vector<int>> 的代码
3. 链接阶段的错误
  • test.cpp 生成的目标文件(test.o):包含 main 函数,且记录了 “需要调用 Print<vector<int>>,但该函数未定义”。
  • func.cpp 生成的目标文件(func.o):包含 Print 模板的通用定义,但没有 Print<vector<int>> 的具体实例化代码。
  • 链接时,链接器需要把 test.o 和 func.o 合并,但找不到 Print<vector<int>> 的实际代码,因此报 “未定义的引用” 错误(如 undefined reference to Print<std::vector<int, std::allocator<int>>>)。
极端情况下可以使用模板特化来显示实例化(可行不可取,用起来不现实),如下

(因为特化版本会在编译阶段直接生成具体的代码,链接

七、标准库中的模板应用

标准库大量使用模板实现泛型:

  • 容器(如vectorlistmap)是类模板,支持自定义存储类型。
  • 算法(如std::swapstd::sort)是函数模板,支持不同类型的元素操作。

示例(std::swap的核心逻辑简化):

template<class T>
void swap(T& a, T& b) {
    T temp(a);
    a = b;
    b = temp;
}

(实际标准库的swap结合了移动语义等优化,但核心是模板实现泛型交换)

补充重点:

模板中 typename 的独有应用场景

template<class Container>
void Print(const Container& con)
{
	//Container::const_iterator前必须要加typename ,因为编译器并不清楚Container::const_iterator是类型还是静态成员变量\函数
	typename Container::const_iterator it = con.begin();
	while (it != con.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl; 
}

在 C++ 模板中,当使用 Container::const_iterator 这类 “嵌套依赖名称” 时,必须在前面加 typename,核心原因是编译器在模板定义阶段无法区分其是 “类型” 还是 “静态成员(函数\变量)”。

(1)核心问题:模板的 “延迟绑定” 特性

模板在定义时只是 “蓝图”,实际的类型(如 Container 具体是什么容器)要到实例化时才确定。对于 Container::const_iterator编译器在模板定义阶段,不知道 Container 是类还是静态成员,因此无法判断 const_iterator 是:

  • 情况 1:类型(比如 vector<int>::const_iterator 是迭代器类型);
  • 情况 2:静态成员(比如 class Container { static int const_iterator; };,此时 Container::const_iterator 是静态变量)。
(2)typename 的作用

typename 关键字的作用是显式声明 “后续的嵌套名称是一个类”,消除编译器的歧义。如果不加 typename,编译器会 “保守假设” 其是 “静态成员变量 / 函数”,从而导致编译错误(用 “非类型” 定义变量是非法的)。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐