云计算百科
云计算领域专业知识百科平台

【C++】模板

目录

    • 1. 泛型编程
    • 2. 函数模板
      • 2.1 概念与语法
      • 2.2 模板的实例化
      • 2.3 模板参数的匹配原则
    • 3. 类模板
      • 3.1 概念与语法
      • 3.2 补充:typename 的特殊用法
    • 4. 非类型模板参数
    • 5. 模板的特化
      • 5.1 为什么需要特化?
      • 5.2 函数模板特化 vs 普通函数重载
      • 5.3 类模板特化
        • 5.3.1 全特化
        • 5.3.2 偏特化(半特化)
    • 6. 模板的分离编译
      • 6.1 什么是分离编译?
      • 6.2 模板导致分离编译链接失败的原因
      • 6.3 解决方法
    • 7. 总结

1. 泛型编程

在日常开发中,如果需要实现一个通用的交换函数,使用函数重载虽然可以实现,但存在明显的缺陷:重载的函数仅仅是类型不同,代码复用率比较低;只要有新类型出现时,就需要用户自己增加对应的函数;并且代码的可维护性比较低,一个出错可能所有的重载均出错。

泛型编程就是为了解决这一问题而产生的。泛型编程是指编写与类型无关的通用代码,它是代码复用的一种手段。模板是泛型编程的基础。

可以把模板想象成一个模具,在 C++ 中,通过给这个模具填充不同的材料(数据类型),就能获得不同材料的铸件(即生成具体类型的代码),从而将重复的工作交给了编译器去完成。

image-20260721161410079

2. 函数模板

2.1 概念与语法

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,编译器会根据实参类型产生函数的特定类型版本。

定义格式:

template<typename T1, typename T2, ..., typename Tn>
返回值类型 函数名(参数列表){}

注意:typename 是用来定义模板参数的关键字,也可以使用 class,但切记不能使用 struct 代替。

代码示例:

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

原理:函数模板本身并不是函数,而是编译器产生特定具体类型函数的模具。在编译阶段,编译器根据传入的实参类型推演生成对应类型的函数以供调用。

image-20260721162202228

2.2 模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化,分为隐式实例化和显式实例化。

  • 隐式实例化:让编译器根据实参推演模板参数的实际类型。
  • 显式实例化:在函数名后的 <> 中指定模板参数的实际类型。如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。

template<typename T>
T Add(const T& left, const T& right)
{
cout << "T Add(const T& left, const T& right)" << endl;
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

// 当参数类型不一致时,直接调用会报错,因为编译器无法确定 T 是 int 还是 double。
// 处理方式 1:强制类型转换
cout << Add(a1, (int)d2) << endl;
cout << Add((double)a1, d2) << endl;

// 处理方式 2:显式实例化,不再让编译器去推演参数
cout << Add<int>(a1, d2) << endl;

return 0;
}

2.3 模板参数的匹配原则

  • 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
  • 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
  • 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
  • // 专门处理 int 的普通加法函数
    int Add(int left, int right)
    {
    cout << "int Add(int left, int right)" << endl;
    return left + right;
    }

    // 通用加法函数模板
    template<class T1, class T2>
    auto Add(T1 left, T2 right)
    {
    cout << "auto Add(T1 left, T2 right)" << endl;
    return left + right;
    }

    int main()
    {
    Add(1, 2); // 优先匹配普通函数,不需要函数模板实例化
    Add(1.1, 2.2); // 模板函数可以生成更加匹配的版本,编译器生成 auto Add(double, double)
    Add(1, 2.0); // 普通函数需要类型转换,而模板能精确匹配,调用模板
    return 0;
    }

    3. 类模板

    3.1 概念与语法

    类模板的定义格式与函数模板类似,但实例化方式有所不同。类模板实例化需要在类模板名字后跟 <>,然后将实例化的类型放在其中。类模板名字不是真正的类,实例化的结果才是真正的类。

    代码示例:动态顺序栈实现

    template<typename T>
    class Stack
    {
    public:
    Stack(size_t capacity = 4)
    {
    _array = (T*)malloc(sizeof(T) * capacity);
    if (nullptr == _array)
    {
    perror("malloc申请空间失败");
    return;
    }
    _capacity = capacity;
    _size = 0;
    }

    void Push(const T& data);
    private:
    T* _array;
    size_t _capacity;
    size_t _size;
    };

    // 模板不建议声明和定义分离到 .h 和 .cpp,会出现链接错误。要分离也建议都在 .h 中。
    // 类模板中函数放在类外进行定义时,需要加模板参数列表
    template<class T>
    void Stack<T>::Push(const T& data)
    {
    _array[_size] = data;
    ++_size;
    }

    int main()
    {
    Stack<int> st1; // Stack 是类模板,Stack<int> 才是真正的类类型
    Stack<double> st2;
    return 0;
    }

    3.2 补充:typename 的特殊用法

    当在模板内部使用某些依赖于模板参数的嵌套类型时(如迭代器),编译器无法确认它是一个静态变量还是一个类型。此时需要使用 typename 明确告知编译器这是一个类型。

    template<class T>
    void PrintVector(const vector<T>& v)
    {
    // 类模板没实例化时,不去里面查细节东西,无法确认时类型还是静态变量
    // 加 typename 明确告诉是类型
    typename vector<T>::const_iterator it = v.begin();
    // 现代 C++ 也可以直接使用 auto it = v.begin();
    while (it != v.end())
    {
    cout << *it << " ";
    ++it;
    }
    cout << endl;
    }

    4. 非类型模板参数

    模板参数分为类型形参与非类型形参。

    • 类型形参:出现在模板参数列表中,跟在 class 或者 typename 之类的参数类型名称。
    • 非类型形参:用一个常量作为类(或函数)模板的一个参数,在类(或函数)模板中可将该参数当成常量来使用。

    // 静态的栈:T 为类型形参,N 为非类型形参
    template<class T, size_t N = 10>
    class Stack
    {
    public:
    void func()
    {
    // N 是常量,不能被修改,N++ 会报错
    }
    private:
    T _a[N];
    int _top;
    };

    int main()
    {
    Stack<int> st1; // 容量为 10
    Stack<int, 100> st2; // 容量为 100
    return 0;
    }

    注意:

  • 浮点数、类对象以及字符串是不允许作为非类型模板参数的(C++20 之前)。
  • 非类型的模板参数必须在编译期就能确认结果。
  • 5. 模板的特化

    通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。模板特化即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。

    5.1 为什么需要特化?

    以下面的 Date 类和 Less 模板为例,如果比较的是指针类型,模板会比较指针的地址大小,而非指针指向的内容大小,这违背了我们的本意。

    class Date
    {
    public:
    Date(int year = 1900, int month = 1, int day = 1)
    : _year(year), _month(month), _day(day) {}
    bool operator<(const Date& d)const
    { /* 日期比较逻辑 */ return true; }
    private:
    int _year, _month, _day;
    };

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

    int main()
    {
    Date d1(2022, 7, 7);
    Date d2(2022, 7, 8);
    cout << Less(d1, d2) << endl; // 比较对象,结果正确

    Date* p1 = &d1;
    Date* p2 = &d2;
    cout << Less(p1, p2) << endl; // 比较指针地址,结果错误
    return 0;
    }

    5.2 函数模板特化 vs 普通函数重载

    虽然可以对函数模板进行特化(通过 template<> 且尖括号中指定特化类型),但函数模板的特化要求形参列表必须和模板函数的基础参数类型完全相同,否则编译器可能报奇怪的错误。

    因此,一般遇到函数模板不能处理或处理有误的类型,为了实现简单通常都是直接给出一个重载的普通函数,这样代码可读性高,容易书写,不建议特化函数模板。

    // 直接重载普通函数,比函数模板特化更好
    bool Less(Date* left, Date* right)
    {
    return *left < *right;
    }

    5.3 类模板特化

    类模板特化分为全特化和偏特化。

    5.3.1 全特化

    全特化即是将模板参数列表中所有的参数都确定化。

    template<class T1, class T2>
    class Data
    {
    public:
    Data() { cout << "Data<T1, T2>-原模板" << endl; }
    };

    // 全特化:针对具体的 int 和 char
    template<>
    class Data<int, char>
    {
    public:
    Data() { cout << "Data<int, char>- 全特化" << endl; }
    };

    5.3.2 偏特化(半特化)

    偏特化是任何针对模板参数进一步进行条件限制设计的特化版本。表现为两种形式:

  • 部分特化:将模板参数类表中的一部分参数特化。
  • 参数更进一步的限制:针对模板参数更进一步的条件限制(如限定为指针或引用)。
  • // 1. 部分特化:将第二个参数特化为 int
    template <class T1>
    class Data<T1, int>
    {
    public:
    Data() { cout << "Data<T1, int>-偏特化" << endl; }
    };

    // 2. 参数进一步限制:限定模板的类型为指针
    template <typename T1, typename T2>
    class Data <T1*, T2*>
    {
    public:
    Data() { cout << "Data<T1*, T2*>-偏特化" << endl; }
    };

    // 2. 参数进一步限制:限定模板的类型为引用
    template <typename T1, typename T2>
    class Data <T1&, T2&>
    {
    public:
    Data() { cout << "Data<T1&, T2&>" << endl; }
    };

    int main()
    {
    Data<int, int> d1; // 调用基础模板
    Data<int, char> d2; // 调用全特化
    Data<double, int> d3; // 调用部分特化 (T1, int)
    Data<int*, double*> d4; // 调用指针偏特化
    Data<int&, int&> d6; // 调用引用偏特化
    return 0;
    }

    6. 模板的分离编译

    6.1 什么是分离编译?

    一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

    6.2 模板导致分离编译链接失败的原因

    如果将模板的声明放在 a.h,定义放在 a.cpp,并在 main.cpp 中调用该模板。 C/C++ 程序编译流程为:预处理 -> 编译 -> 汇编 -> 链接。 编译器对工程中的多个源文件是分离开单独编译的。在编译 a.cpp 时,编译器没有看到对模板函数的实例化,因此不会生成具体的函数代码。而在 main.obj 中调用了该函数,链接器在链接时去寻找其地址,发现找不到,从而引发无法解析的外部符号链接报错。

    //a.h
    #pragma once
    // 模板的声明
    template<class T>
    T Add(const T& left, const T& right);

    //a.cpp
    #include "a.h"
    // 模板的定义
    template<class T>
    T Add(const T& left, const T& right)
    {
    return left + right;
    }

    //main.cpp
    #include <iostream>
    #include "a.h"
    using namespace std;
    int main()
    {
    // 编译时这里会触发“无法解析的外部符号” (Unresolved external symbol) 的链接错误
    cout << Add(1, 2) << endl;
    return 0;
    }

    image-20260721164353854

    6.3 解决方法

  • 推荐做法:将声明和定义放到一个文件 "xxx.hpp" 里面,或者直接写在 .h 中。
  • 不推荐做法:在模板定义的位置进行显式实例化。这种方法缺乏灵活性,极不实用。
  • 7. 总结

    【优点】

  • 模板复用了代码,节省资源,更快的迭代开发,C++ 的标准模板库 (STL) 因此而产生。
  • 增强了代码的灵活性。
  • 【缺陷】

  • 模板会导致代码膨胀问题,也会导致编译时间变长。
  • 出现模板编译错误时,错误信息非常凌乱,不易定位错误。
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【C++】模板
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!