【C++】模板初阶: 解析模板原理、实例化与特化

📌 相关专栏

很高兴你点开这篇文章✨

这里会持续更新我喜欢的内容,关注我,一起慢慢变好呀

👍 点赞 ⭐ 收藏 💬 评论


文章目录

  • 前言
  • 一、函数模板
    • [1.1 为什么需要函数模板?](#1.1 为什么需要函数模板?)
  • [1.2 函数模板的语法](#1.2 函数模板的语法)
    • [1.3 模板的使用](#1.3 模板的使用)
  • 二、模板参数推导与实例化
    • [2.1 隐式实例化](#2.1 隐式实例化)
    • [2.2 参数不匹配时的处理](#2.2 参数不匹配时的处理)
    • [2.3 多类型模板参数](#2.3 多类型模板参数)
    • [2.4 显式实例化](#2.4 显式实例化)
  • 三、模板重载与匹配规则
    • [3.1 函数模板与普通函数可以重载](#3.1 函数模板与普通函数可以重载)
    • [3.2 匹配优先级](#3.2 匹配优先级)
  • 四、类模板
    • [4.1 类模板的定义](#4.1 类模板的定义)
    • [4.2 类模板成员函数的类外定义](#4.2 类模板成员函数的类外定义)
    • [4.3 类模板的使用(显式实例化)](#4.3 类模板的使用(显式实例化))
  • 五、模板的注意事项
    • [5.1 声明与定义分离的问题](#5.1 声明与定义分离的问题)
    • [5.2 模板的编译原理](#5.2 模板的编译原理)
    • [5.3 模板的缺点](#5.3 模板的缺点)
  • 六、完整示例:通用Stack类
  • 七、知识点汇总
  • 八、常见面试题

前言

在C语言中,如果我们要写一个交换两个整数的函数,再写一个交换两个浮点数的函数,我们只能写两个不同名的函数,或者用宏(但宏有很多坑)。

C++提供了模板来解决这个问题。有了模板,我们就可以写出类型相关的通用代码。

🐾 这一篇我们来学习:

  • 函数模板:如何写一个通用的Swap函数
  • 模板参数推导:编译器如何自动推断类型
  • 显式实例化:强制指定模板参数类型
  • 类模板:如何写一个通用的Stack容器

🐶 🐾 ✨ 🐾 🐶


一、函数模板

1.1 为什么需要函数模板?

看看这个例子:我们需要交换两个变量的值,但int、double、char都需要写一个版本。

cpp 复制代码
//  代码重复严重
void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

void Swap(double& left, double& right)
{
    double temp = left;
    left = right;
    right = temp;
}

void Swap(char& left, char& right)
{
    char temp = left;
    left = right;
    right = temp;
}

🐾 函数模板

cpp 复制代码
//  一个模板搞定所有类型
template<typename T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}

1.2 函数模板的语法

cpp 复制代码
// template 关键字 + <typename T> 或 <class T>
template<typename T>
void Swap(T& x, T& y)
{
    T tmp = x;
    x = y;
    y = tmp;
}

// 多个模板参数
template<typename T1, typename T2>
void func(const T1& x, const T2& y)
{
    // ...
}

注意 : typename和class在模板参数中完全等价,没有区别。

cpp 复制代码
template<typename T>  //  推荐(更语义化)
template<class T>     //  也可以(C++早期用法)

1.3 模板的使用

cpp 复制代码
int main()
{
    int i = 1, j = 2;
    double m = 1.1, n = 2.2;
    
    Swap(i, j);    // 编译器推导 T = int
    Swap(m, n);    // 编译器推导 T = double
    
    // Swap(i, n); //  错误!T被推导成int还是double?矛盾
    return 0;
}

🐶 🐾 ✨ 🐾 🐶


二、模板参数推导与实例化

2.1 隐式实例化

编译器会根据你传入的实参类型,自动推导模板参数:

cpp 复制代码
template<typename T>
T Add(const T& left, const T& right)
{
    return left + right;
}

int main()
{
    int a1 = 10, a2 = 20;
    double d1 = 10.1, d2 = 20.2;
    
    Add(a1, a2);    // 隐式实例化:T → int
    Add(d1, d2);    // 隐式实例化:T → double
    
    return 0;
}

2.2 参数不匹配时的处理

编译器根据你传入的实参类型,自动推导模板参数:

cpp 复制代码
Add(a1, d1);  //  错误:T被推导成int还是double?

🐾

解决方法1:强制类型转换

cpp 复制代码
cout << Add(a1, (int)d1) << endl;      // 都转成int
cout << Add((double)a1, d1) << endl;   // 都转成double

🐾

解决方法2 :显式实例化(推荐)

cpp 复制代码
cout << Add<int>(a1, d1) << endl;      // 明确指定 T = int
cout << Add<double>(a1, d1) << endl;   // 明确指定 T = double

2.3 多类型模板参数

cpp 复制代码
template<typename T1, typename T2>
T1 Add(const T1& left, const T2& right)
{
    return left + right;
}

int main()
{
    int a1 = 10;
    double d1 = 20.2;
    
    cout << Add(a1, d1) << endl;  // T1=int, T2=double,返回int
    return 0;
}

2.4 显式实例化

cpp 复制代码
template<typename T>
T* func1(int n)
{
    return new T[n];
}

int main()
{
    // 无法推导T,必须显式指定
    int* p1 = func1<int>(10);     // T → int
    double* p2 = func1<double>(10); // T → double
    
    delete[] p1;
    delete[] p2;
    return 0;
}

🐶 🐾 ✨ 🐾 🐶


三、模板重载与匹配规则

3.1 函数模板与普通函数可以重载

cpp 复制代码
// 函数模板
template<typename T>
T Add(const T& left, const T& right)
{
    cout << "template Add: ";
    return left + right;
}

// 普通函数(特化版本)
int Add(const int& x, const int& y)
{
    cout << "normal Add: ";
    return (x + y) * 10;
}

int main()
{
    int a1 = 10, a2 = 20;
    
    cout << Add(a1, a2) << endl;
    // 输出:normal Add: 300
    // 普通函数优先级更高
    
    cout << Add<int>(a1, a2) << endl;
    // 输出:template Add: 30
    // 显式指定<>,强制调用模板
    
    double d1 = 1.1, d2 = 2.2;
    cout << Add(d1, d2) << endl;
    // 输出:template Add: 3.3
    // 没有匹配的普通函数,调用模板
    
    return 0;
}

3.2 匹配优先级

优先级 匹配规则
1(最高) 完全匹配的普通函数
2 通过模板实例化得到匹配函数
3(最低) 通过类型转换匹配

🐶 🐾 ✨ 🐾 🐶


四、类模板

4.1 类模板的定义

cpp 复制代码
template<typename T>
class Stack
{
public:
    Stack(int n = 4)
        : _array(new T[n])
        , _size(0)
        , _capacity(n)
    {}
    
    ~Stack()
    {
        delete[] _array;
        _array = nullptr;
        _size = _capacity = 0;
    }
    
    void Push(const T& x);
    
private:
    T* _array;
    size_t _capacity;
    size_t _size;
};

4.2 类模板成员函数的类外定义

关键 : 类外定义时需要加上template<typename T>,并用类名<T>::指定作用域。

cpp 复制代码
// 类外定义成员函数
template<typename T>
void Stack<T>::Push(const T& x)
{
    if (_size == _capacity)
    {
        // 扩容逻辑
        T* tmp = new T[_capacity * 2];
        memcpy(tmp, _array, sizeof(T) * _size);
        delete[] _array;
        
        _array = tmp;
        _capacity *= 2;
    }
    _array[_size++] = x;
}

4.3 类模板的使用(显式实例化)

注意 : 类模板不支持隐式实例化,必须显式指定模板参数类型。

cpp 复制代码
int main()
{
    // 显式实例化:指定T为int
    Stack<int> st1;
    st1.Push(1);
    st1.Push(2);
    st1.Push(3);
    
    // 显式实例化:指定T为double
    Stack<double> st2;
    st2.Push(1.1);
    st2.Push(2.2);
    st2.Push(3.3);
    
    // 动态分配类模板对象
    Stack<double>* pst = new Stack<double>;
    pst->Push(10.5);
    delete pst;
    
    return 0;
}

🐶 🐾 ✨ 🐾 🐶


五、模板的注意事项

5.1 声明与定义分离的问题

模板的声明和定义通常不能分离到.h.cpp文件中。

cpp 复制代码
// Stack.h
template<typename T>
class Stack {
public:
    void Push(const T& x);
};

// Stack.cpp  错误!链接时会找不到定义
template<typename T>
void Stack<T>::Push(const T& x) { /* ... */ }

🐾 解决办法:

  • 将定义直接写在.h文件中
  • 或者在.cpp文件末尾显式实例化需要的类型
cpp 复制代码
// Stack.cpp - 显式实例化
template class Stack<int>;
template class Stack<double>;

5.2 模板的编译原理

模板在编译阶段根据使用情况生成具体代码:

  • 编译器看到模板定义时,不会生成代码
  • 编译器看到实例化(如Stack<int>)时,才会生成对应的类代码
  • 不同的实例化生成不同的类(Stack<int>和Stack<double>是不同类型)

5.3 模板的缺点

缺点 说明
编译慢 每次实例化都要重新生成代码
代码膨胀 不同类型生成多份代码
错误信息复杂 模板编译错误信息难以阅读
声明定义难分离 通常只能写在头文件

🐶 🐾 ✨ 🐾 🐶


六、完整示例:通用Stack类

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

template<typename T>
class Stack
{
public:
    Stack(int n = 4)
        : _array(new T[n])
        , _size(0)
        , _capacity(n)
    {
        cout << "Stack()" << endl;
    }
    
    ~Stack()
    {
        delete[] _array;
        _array = nullptr;
        _size = _capacity = 0;
        cout << "~Stack()" << endl;
    }
    
    void Push(const T& x)
    {
        if (_size == _capacity)
        {
            Expand();
        }
        _array[_size++] = x;
    }
    
    void Pop()
    {
        if (_size > 0)
            _size--;
    }
    
    T& Top()
    {
        return _array[_size - 1];
    }
    
    bool Empty() const
    {
        return _size == 0;
    }
    
    size_t Size() const
    {
        return _size;
    }
    
private:
    void Expand()
    {
        T* tmp = new T[_capacity * 2];
        for (size_t i = 0; i < _size; i++)
        {
            tmp[i] = _array[i];
        }
        delete[] _array;
        _array = tmp;
        _capacity *= 2;
    }
    
    T* _array;
    size_t _capacity;
    size_t _size;
};

int main()
{
    // int栈
    Stack<int> intStack;
    intStack.Push(10);
    intStack.Push(20);
    intStack.Push(30);
    
    while (!intStack.Empty())
    {
        cout << intStack.Top() << " ";
        intStack.Pop();
    }
    cout << endl;  // 30 20 10
    
    // string栈
    Stack<string> strStack;
    strStack.Push("hello");
    strStack.Push("world");
    
    cout << strStack.Top() << endl;  // world
    
    return 0;
}

🐶 🐾 ✨ 🐾 🐶


七、知识点汇总

知识点 核心要点
函数模板 template + 函数定义
模板参数 typename和class完全等价
隐式实例化 编译器自动推导参数类型
显式实例化 FuncName(a, b)强制指定
类模板 必须显式实例化,如Stack
类外定义 需template + Stack::
匹配优先级 普通函数 > 模板实例化 > 类型转换
编译特性 模板在实例化时才生成代码

🐶 🐾 ✨ 🐾 🐶


八、常见面试题

🐾 Q1:typename和class在模板中有什么区别?

  • 在模板参数中完全等价。但typename还可以用于嵌套依赖类型,例如typename T::iterator。

🐾 Q2:函数模板可以隐式实例化,类模板为什么不行?

  • 函数模板编译器可以实参推导,类模板没有推导依据(构造函数实参可以推导C++17开始支持)。C++17开始类模板也支持部分隐式推导(CTAD)。

🐾 Q3:模板声明和定义为什么不能分离?

  • 模板在实例化时才生成代码。如果定义在.cpp文件,其他文件包含.h时看不到定义,无法实例化,导致链接错误。

🐾 Q4:模板代码膨胀怎么解决?

  • 将不依赖模板参数的公共代码抽取到基类或单独的函数中。

🐾 下一篇我们来学习:

  • STL初识(vector、list、map等)
  • 迭代器的使用

🐶 🐾 ✨ 🐾 🐶


谢谢你看到这里呀

如果喜欢这篇内容,点个关注,下次更新不迷路✨

👍 点赞 ⭐ 收藏 💬 评论