在这里插入图片描述

一、模板进阶开篇

前面我们已经学习了模板的基础用法,函数模板、类模板我们都已经接触过了,也能写出简单的通用代码,但是模板真正强大、真正难的地方,是它的进阶用法

如果没有学过模板初阶的小伙伴,推荐看以下文章:
【C++】模板编程入门指南:零基础掌握泛型编程核心(初阶)

模板进阶主要包含这几块内容:非类型模板参数、模板特化、模板的分离编译,这些都是面试常考、写STL必须掌握的知识点,接下来我们一个一个来学习。

二、非类型模板参数

模板参数分为两种,一种是类型模板参数,一种是非类型模板参数

类型模板参数就是我们之前写的:

template<class T>

它一般是用来传类型的,而非类型模板参数,是用常量作为模板参数,在编译期间确定值,我们来看一个例子:

template<class T, size_t N>
class array
{
private:
    T _array[N];
};

这里的size_t N就是非类型模板参数,它是一个常量,不是类型,使用起来就是这样:

array<int, 10> a1;
array<int, 100> a2;

和我们平时用模板没什么区别,只是多传了一个常量,这就叫非模板参数,是不是很简单呢,但是它的使用还是有需要注意的地方,如下:

注意事项

  1. 浮点数、类对象、字符串不能作为非类型模板参数
  2. 非类型模板参数必须在编译期就能确定结果

这一块非常简单,我们就简单过一下,接下来进入重点:模板的特化

三、模板的特化

1. 什么是特化

模板是给通用类型用的,但是某些特定类型,按照默认模板逻辑会出错,这时候我们就需要特殊处理,这就叫模板特化,我们举个例子看看:

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

这个模板对于int、double、自定义类型都没问题,但是如果传入指针呢?

Date* d1 = new Date(2025, 1, 1);
Date* d2 = new Date(2024, 1, 1);
cout << Less(d1, d2) << endl;

可能我们的原意是比较指针指向的内容,但是这里比较的是指针地址,不是指针指向的内容!结果完全错了,这时候,我们就需要对Date*类型特化

2. 函数模板特化

函数模板特化步骤:

  1. 必须先有一个基础的函数模板
  2. template<>
  3. 函数名后面加<需要特化的类型>

代码如下:

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

// 特化 Date* 版本
template<>
bool Less<Date*>(Date* left, Date* right)
{
    return *left < *right;
}

这样再调用Less(d1, d2),就会走特化版本,比较的是内容,而不是地址

小提示

函数模板特化写法有点麻烦,实际中我们直接写一个同名普通函数更简单,编译器会优先调用

3. 类模板特化(重点)

类模板特化比函数模板特化更常用,也更重要,分为两种:

  • 全特化
  • 偏特化

(1)全特化

全特化就是把所有模板参数都确定下来,函数模板的特化就类似于全特化,必须把所有模板参数都确定下来,不允许部分特化,接下来我们举一个全特化例子:

//特化类模板前,必须保证对应类模板存在
template<class T1, class T2>
class Data
{
public:
    Data() { cout << "Data<T1, T2>" << endl; }
};

// 全特化:T1=int,T2=double
template<>
class Data<int, double>
{
public:
    Data() { cout << "Data<int, double>" << endl; }
};

使用:

Data<int, int> d1;   // 通用模板
Data<int, double> d2;// 全特化

(2)偏特化

偏特化有两种表现形式:

  1. 只特化部分参数
  2. 对参数类型加限制(指针、引用)
① 部分特化
template<class T1>
class Data<T1, int>
{
public:
    Data() { cout << "Data<T1, int>" << endl; }
};
② 对类型进一步限制
// 特化为指针类型
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
    Data() { cout << "Data<T1*, T2*>" << endl; }
};

使用:

Data<int*, int*> d1; // 走指针特化
Data<char, int> d2;  // 走部分特化

类模板特化在STL里非常常见,比如vector<bool>priority_queue的比较规则,都是靠特化实现的。

四、模板的分离编译(超级大坑)

这一块是模板最容易出错、面试最爱问的地方!

1. 什么是分离编译

我们平时写代码:

  • .h 文件放声明
  • .cpp 文件放实现
  • 最后链接到一起

普通函数这样写完全没问题,但模板不行!

2. 为什么模板不能分离编译

模板的特点:只有被调用时,才会实例化出具体代码

  • 你在.h声明模板
  • .cpp写实现,但没人调用它
  • 编译器不会生成任何函数代码
  • 最后main.cpp调用时,链接找不到函数 → 报错:无法解析外部符号

相当于就是头文件有声明,知道模板需要生成什么类型,但是没有实现,生成不出来,而cpp文件有实现,但是不知道需要生成什么类型,就不会实例化,最后就找不到,所以会链接失败

3. 解决方案(最实用)

把声明和实现写在同一个文件里!

一般命名为:

  • xxx.hpp
  • 或者直接用 .h

这是最推荐的写法。

// template.hpp
template<class T>
T Add(T a, T b)
{
    return a + b;
}

直接包含.hpp文件,完美解决问题。

五、模板总结

优点

  1. 复用性强,可以快速写通用代码,实现泛型编程
  2. 模板是STL的基础,没有模板就没有STL
  3. 模板的灵活度极高,可适配各种类型,方便使用

缺点

  1. 有可能引起代码膨胀,使得编译变慢
  2. 报错信息极其混乱,很难定位错误,特别是复杂的代码,只能通过经验定位错误
  3. 分离编译会报错,新手容易踩坑

模板有优有缺,但总体来说,模板是C++最核心、最强大的特性之一,也是进阶必备,希望大家都能好好掌握

那么今天关于模板进阶就讲到这里,有什么不懂欢迎私信问我,我会及时做出解答!
下一篇文章我们开始学习继承,把C++面向对象的特性之一彻底吃透,敬请期待吧!
bye~

更多推荐