文章目录
- [1. 前言](#1. 前言)
- [2. 非类型模板参数](#2. 非类型模板参数)
-
- [1. 介绍](#1. 介绍)
- [2. 使用](#2. 使用)
- [3. array](#3. array)
-
- [1. array的介绍](#1. array的介绍)
- [2. array的使用](#2. array的使用)
- [3. 模板的特化](#3. 模板的特化)
-
- [1. 概念引入](#1. 概念引入)
- [2. 函数模板特化](#2. 函数模板特化)
-
- [1. 函数模板特化步骤](#1. 函数模板特化步骤)
- [2. Less函数模板针对日期类的特化](#2. Less函数模板针对日期类的特化)
- [3. 类模板特化](#3. 类模板特化)
-
- [1. 全特化](#1. 全特化)
- [2. 偏特化](#2. 偏特化)
- [3. 应用](#3. 应用)
- [4. 模板分离编译](#4. 模板分离编译)
-
- [1. 前言](#1. 前言)
- [2. 什么是分离编译](#2. 什么是分离编译)
- [3. 模板的分离编译](#3. 模板的分离编译)
- [4. 解决办法](#4. 解决办法)
-
- [1. 显式实例化](#1. 显式实例化)
- [2. 模板声明和定义不分离](#2. 模板声明和定义不分离)
- [5. 模板总结](#5. 模板总结)
-
- [1. 优点](#1. 优点)
- [2. 缺陷](#2. 缺陷)
1. 前言
在前面,我们已经了解了一部分stl容器,学会了模板的使用,接下来我们来讨论一些关于模板更加进阶的东西------>>>点击查看【模板初阶+STL简介】
2. 非类型模板参数
1. 介绍
模板参数分为类型形参和非类型形参
- 类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
- 非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常
量来使用。
但是我们要注意两点:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
2. 使用
cpp
#include<iostream>
using namespace std;
// 静态数组
#define N 10
int a[N];
//静态栈
template<typename T, size_t n = 10>
class stack
{
public:
private:
T _a[n];
int _capacity;
int _size;
};
int main()
{
stack<int> st1;
stack<int, 100> st2;
a[1] = 9;
cout << a[1] << endl;
return 0;
}
- 这里我们就自己实现了一个静态的栈,它的空间大小是一开始就决定好了的
- 这里其实跟我们在C语言中学习define定义一个常量来确定数组大小是有点相像的
- 在C++中有个容器使用了非类型模板参数,它就是array,下面我们来看看
3. array
1. array的介绍

- 我们可以看到array中也是使用了非类型模板参数,去定义固定大小的数据,这里就很像我们平常使用的静态数组,后面我们会举例并说一下它们的区别
- 因为它是静态的,所以是没有插入扩容之类的接口的
2. array的使用
cpp
#include<iostream>
#include<array>
using namespace std;
// 这里我们自己简单实现一个array
namespace William
{
// 定义一个模板类型的静态数组
template<class T, size_t N = 10>
class array
{
public:
typedef T* iterator;
iterator begin() { return _array; }
iterator end() { return _array + _size; }
T& operator[](size_t index) { return _array[index]; }
const T& operator[](size_t index)const { return _array[index]; }
size_t size()const { return _size; }
bool empty()const { return 0 == _size; }
private:
T _array[N];
size_t _size = N;
};
}
int main()
{
William::array<int> a1;
William::array<int, 5> a2;
/*std::array<int, 15> a1;
std::array<int, 5> a2;*/
for (int i = 0; i < a1.size(); i++) a1[i] = i;
for (auto e : a1) cout << e << " ";
return 0;
}
- 这里我们简单搓一个array,库中肯定没有这么简单,不过大体逻辑也不会差太多,这里可以方便我们更进一步理解
- array的使用其实跟我们平常使用的数组是没什么区别的,按照我们平常使用数组的方式使用array就可以了
- array的优势就是这个是我们自己封装出来的,它会有更加严格的检查机制,比如越界访问,我们就可以通过断言检查,而且array还支持迭代器
3. 模板的特化
1. 概念引入
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 2026, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
bool operator<(const Date& d) const
{
if (_year < d._year)
return true;
else if (_year == d._year)
{
if (_month < d._month)
return true;
else if (_month == d._month)
return _day < d._day;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(2, 1) << endl;
Date d1(2026, 10, 10);
Date d2(2026, 1, 1);
cout << Less(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl;
return 0;
}
- 这里我们来看一下特化的场景,如上面我们实现的Less类一样,在针对内置类型和我们实现的日期类时还是可以正常比较的,因为我们这里的模板参数都可以正确地对应上,但是针对我们第三个例子,即Date*时就对应不上了,我们希望的是Less可以比较我们指向的内容,但是它却只能比较这两个对象的地址
- 那我们接下来的处理就要用到特化了,这里是函数模板的特化
- 模板特化分为函数模板特化和类模板特化
2. 函数模板特化
1. 函数模板特化步骤
- 必须要先有一个基础的函数模板,因为特化的函数模板不能脱离基础的函数模板而独立存在
- 必须要使用关键字template并且后面加空的尖括号<>表示这是函数模板的特化
- 函数名后面跟尖括号<>,在尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
2. Less函数模板针对日期类的特化
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 2026, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
bool operator<(const Date& d) const
{
if (_year < d._year)
return true;
else if (_year == d._year)
{
if (_month < d._month)
return true;
else if (_month == d._month)
return _day < d._day;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
//template<class T>
//bool Less(T left, T right)
//{
// return left < right;
//}
//template<>
//bool Less<Date*>(Date* left, Date* right)
//{
// return left < right;
//}
//
//bool Less(Date* left, Date* right)
//{
// return left < right;
//}
template<class T>
bool Less(const T& left, const T& right)
{
return left < right;
}
template<>
bool Less<Date*>(Date* const & left, Date* const & right)
{
return *left < *right;
}
template<>
bool Less<const Date*>(const Date* const& left, const Date* const& right)
{
return *left < *right;
}
bool Less(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
cout << Less(2, 1) << endl;
Date d1(2026, 10, 10);
Date d2(2026, 1, 1);
cout << Less(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl;
const Date* p3 = &d1;
const Date* p4 = &d2;
cout << Less(p3, p4) << endl;
return 0;
}
- 这里我们来详细拆解一下这段代码,我们一开始需要看被我们注释掉的那部分的特化案例
- 当时我们在特化的概念引入时说到,我们在Less中传入日期类的指针的结果并不是我们想要的,所以我们这里就可以按照上面说的函数模板特化步骤来实现一个针对日期类的指针的特化,但是想要实现这个功能,我们在以前学习函数重载的时候就已经了解了,在这里我更推荐大家使用函数重载,因为后面我们就会知道这里的特化还是比较麻烦的
- 这里有个小细节,如果我们既实现了函数模板特化,又实现了函数重载,那么我们的代码会去走哪部分呢,答案是会去走函数重载的那部分,因为我们的编译器会优先走最匹配的部分,有现成的函数重载就没必要再走什么模板去实例化什么的了
- 为什么我们上面会说特化有些地方会很麻烦呢,这里就可以看我们没被注释掉的那部分的特化代码了,我们以前使用模板的时候,因为不知道T类型会实例化成什么,所以为了减少拷贝我们会传引用,为了不被修改我们还加了const,这时我们的函数模板特化的传参就会很复杂,就像针对我们的Date*,这里的const是要加到*之前还是加到之后呢?我们要知道,对于一般的类型,const加到类型前还是加到类型后其实没什么影响,只不过我们习惯性地加到前面,但是对于指针类型如果我们加到*之前就是修饰指向的内容,而不是指针本身,所以我们这里的const就要加到*之后,就跟我们的习惯很不一样,容易出错
- 如果我们本来就修饰了指向的内容,那么我们还要再额外实现一份const加到*之前的版本
3. 类模板特化
1. 全特化
cpp
#include<iostream>
using namespace std;
template<class T1, class T2>
class A
{
public:
A() { cout << "A(T1, T2)" << endl; }
};
template<>
class A<int, int>
{
public:
A() { cout << "A(int, int)" << endl; }
};
template<>
class A<int, char>
{
public:
A() { cout << "A(int, char)" << endl; }
};
int main()
{
A<bool, bool> a1;
A<int, int> a2;
A<int, char> a3;
return 0;
}

- 从代码中我们就可以理解全特化了,这里类模板的特化和函数模板的特化区别是不大的,如果满足特化的类型编译器就会去走更适合的特化版本去
2. 偏特化
cpp
#include<iostream>
using namespace std;
template<class T1, class T2>
class A
{
public:
A() { cout << "A(T1, T2)" << endl; }
};
// 全特化
template<>
class A<int, int>
{
public:
A() { cout << "A(int, int)" << endl; }
};
template<>
class A<int, char>
{
public:
A() { cout << "A(int, char)" << endl; }
};
// 偏特化
template<class T1>
class A<T1, char>
{
public:
A() { cout << "A(T1, char)" << endl; }
};
template<class T2>
class A<int, T2>
{
public:
A() { cout << "A(int, T2)" << endl; }
};
template<class T1, class T2>
class A<T1*, T2*>
{
public:
A() { cout << "A(T1*, T2*)" << endl; }
};
template<class T1, class T2>
class A<T1&, T2&>
{
public:
A() { cout << "A(T1&, T2&)" << endl; }
};
template<class T1, class T2>
class A<T1*, T2&>
{
public:
A() { cout << "A(T1*, T2&)" << endl; }
};
int main()
{
A<bool, bool> a1;
A<int, int> a2;
A<int, char> a3;
A<double, char> a4;
A<int, double> a5;
A<int*, char*> a6;
A<int&, char&> a7;
A<int*, char&> a8;
return 0;
}

- 这里的后面两个特化就是我们的偏特化
- 如果我们传入的类型既满足走全特化也满足走偏特化的话,我们会优先去走全特化
- 我们可以看到,后面几个偏特化我们是特化成了指针和引用,这也是属于我们的偏特化的
- 针对指针和引用的偏特化,我们的这里的T1、T2并不直接就是对应类型的指针或者引用了,还是原来的类型
3. 应用
cpp
#include<iostream>
using namespace std;
template<class T>
class Less
{
public:
bool operator()(const T& a, const T& b)
{
return a < b;
}
};
int main()
{
Less<int> cmp1;
int a = 2;
int b = 1;
cout << cmp1(a, b) << endl;
cout << cmp1(b, a) << endl;
Less<int*> cmp2;
int* p1 = &a;
int* p2 = &b;
cout << cmp2(p1, p2) << endl;
cout << cmp2(p2, p1) << endl;
return 0;
}

- 这里用到我们之前在优先级队列那里说的仿函数了,我们以这个为例来看一下类模板类型的特化
- 这里我们传int类型的时候是正常的,但是传入指针类型就不对了,这里我们就可以靠特化来解决,我们的库中没做这样的特化是怕我们本来就是想要比较指针的话就画蛇添足了
cpp
#include<iostream>
using namespace std;
template<class T>
class Less
{
public:
bool operator()(const T& a, const T& b)
{
return a < b;
}
};
template<class T>
class Less<T*>
{
public:
bool operator()(T* const& a, T* const& b)
{
return *a < *b;
}
};
int main()
{
Less<int> cmp1;
int a = 2;
int b = 1;
cout << cmp1(a, b) << endl;
cout << cmp1(b, a) << endl;
Less<int*> cmp2;
int* p1 = &a;
int* p2 = &b;
cout << cmp2(p1, p2) << endl;
cout << cmp2(p2, p1) << endl;
return 0;
}

4. 模板分离编译
1. 前言
我们在之前STL部分那里经常会说模板的声明和定义不分离,如果分离了就会发生链接错误,这个东西我们一直在说,但是没有解释过为什么会这样,这里我们就来简单了解一下
2. 什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。下面我们来简单看看代码是怎么执行的
- 预处理:在这个阶段,我们会做以下动作:头文件展开、宏替换、条件编译、去掉注释等等
- 编译:检查语法,生成汇编代码
- 汇编:汇编代码转换成二进制机器码,同时会生成一个符号表,每个函数的函数名会有对应的符号表
- 目标文件合并在一起生成可执行程序,并把需要的函数地址等链接上
- 下面我们针对模板的分离编译具体说一下
3. 模板的分离编译
cpp
// a.h
#pragma once
#include<iostream>
using namespace std;
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;
}
// test.cpp
#include "a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
- 这里就发生了链接错误
- 首先我们进行了预处理,把我们的头文件a.h在a.cpp和test.cpp中展开了,生成对应的a.i、test.i文件,这里是没有问题的
- 然后进行编译,检查语法错误,生成汇编代码,生成对应的a.s、test.s文件,这里也没有问题
- 再进行汇编,汇编代码转换成二进制机器码,生成符号表,这里就出现问题了,因为我们这里的模板没有实例化,我们只是声明了,所以在编译过程中我们是让这个Add函数通过了(确实是有这个函数的声明),但是我们没有这个函数对应的地址,所以我们符号表中也没有Add这个函数,汇编进行后会生成对应的a.o、test.o文件
- 最后链接时因为我们在符号表中没有找到对应的Add函数,所以就发生了链接错误
4. 解决办法
1. 显式实例化
cpp
#pragma once
// 不太推荐
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right);
template
int Add(const int& left, const int& right);
template
double Add(const double& left, const double& right);
2. 模板声明和定义不分离
cpp
#pragma once
// 比较推荐
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
5. 模板总结
1. 优点
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
2. 缺陷
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误