本文围绕 C++ 模板的进阶用法展开,从 非类型模板参数 入手,结合 array 容器对比传统 C 数组,深入分析了 类模板的全特化与偏特化,并通过实例展示如何对不同类型进行特化。随后介绍了 模板的分离编译问题,解析了为什么模板声明与定义分离会导致链接错误,以及如何通过显式实例化或头文件内定义来解决。文章配合代码与图解,力求让读者清晰理解模板的底层机制和实际应用。
模板参数分为类类型形参和非类型形参。
例如,我们要实现一个静态数组的类,就需要用到非类型模板参数。
template<class T, size_t N> //N:非类型模板参数
class StaticArray
{
public:
size_t arraysize()
{
return N;
}
private:
T _array[N]; //利用非类型模板参数指定静态数组的大小
};
int main()
{
StaticArray<int, 10> a1; //定义一个大小为10的静态数组
cout << a1.arraysize() << endl; //10
StaticArray<int, 100> a2; //定义一个大小为100的静态数组
cout << a2.arraysize() << endl; //100
return 0;
}
特点 | 类型模板参数 (Type Template Parameter) | 非类型模板参数 (Non-type Template Parameter) |
|---|---|---|
参数形式 | 类型(如 int, double, std::string) | 编译期常量值(如整数、枚举、指针、引用、C++20起支持浮点和字面量类) |
传参方式 | Array<int> | Array<int, 10> |
常见用途 | 泛型编程、容器、算法 | 固定大小容器、编译期策略选择、哈希参数等 |
编译期要求 | 只要类型合法即可 | 必须是编译期常量 |
C++17 支持范围 | 所有类型 | 整数、枚举、指针/引用常量 |
C++20 新增支持 | - | 浮点数、字面量类类型(满足 constexpr) |
举例 | template <typename T> class Vec {} | template <typename T, int N> class Array {} |


我们可以看出arry容器也使用的非类型模板参数来管理数组的固定大小。
#include<iostream>
#include<array>
using namespace std;
int main()
{
//array容器的使用
array<int, 8> dh; //定义一个大小为8的静态数组
dh[0] = 6; //第一个数据初始化为6
dh[7] = 99; //最后一个数据初始化为99
for (auto e : dh) //输出dh数组
{
cout << e << ' ';
}
cout << endl;
return 0;
}
我们发现对未进行赋值的数据并没有对其进行初始化,反而是给出的随机值,那他和我们c语言中的传统数组有什么不同呢?又有什么优势?
#include<iostream>
#include<array>
using namespace std;
int main()
{
//array容器的使用
array<int, 8> dh; //定义一个大小为8的静态数组
dh[0] = 6; //第一个数据初始化为6
dh[7] = 99; //最后一个数据初始化为99
for (auto e : dh) //输出dh数组
{
cout << e << ' ';
}
cout << endl;
//c语言传统数组
int arr[8];
arr[0] = 6;
arr[7] = 99;
for (auto e : arr) //输出dh数组
{
cout << e << ' ';
}
cout << endl;
return 0;
}
同样的,都没有进行初始化,而且c语言中的数组也支持array中的大部分数组,其实其中的不同就是array容器对越界的检查更加的严格,可以检查出越界读和越界写。
看下面的两个错误写法:


但是c语言中也可以检查出部分越界写

我们通过一个代码的例子来理解模板的特化
#include<iostream>
using namespace std;
// 函数模板 -- 参数匹配
template<class T>
bool IsEqual(T x, T y) //判断是否相等
{
return x == y;
}
int main()
{
cout << IsEqual(1, 1) << endl; //1
cout << IsEqual(1.1, 2.2) << endl; //0
char a1[] = "2021bsdt";
char a2[] = "2021bsdt";
cout << IsEqual(a1, a2) << endl; //0
return 0;
}
判断结果是这两个字符串不相等,这很好理解,因为我们希望的是该函数能够判断两个字符串的内容是否相等,而该函数实际上判断是确实这两个字符串所存储的地址是否相同,这是两个存在于栈区的字符串,其地址显然是不同的。类似于上述实例,使用模板可以实现一些与类型无关的代码,但对于一些特殊的类型可能会得到一些错误的结果,此时就需要对模板进行特化,即在原模板的基础上,针对特殊类型进行特殊化的实现方式
函数模板的特化步骤:
我们知道当传入的类型是char时,应该依次比较各个字符的ASCII码值进而判断两个字符串是否相等,或是直接调用strcmp函数进行字符串比较,那么此时我们就可以对char类型进行特殊化的实现。
#include<iostream>
using namespace std;
//特化步骤一:基础的函数模板
template<class T>
bool IsEqual(T x, T y)
{
return x == y;
}
//对于char*类型的特化
//特化步骤二:关键字template后面接一对空的尖括号<>
template<>
//特化步骤三:函数名后跟一对尖括号,尖括号中指定需要特化的类型
//特化步骤四:函数形参表必须要和模板函数的基础参数类型完全相同,否则不同的编译器可能会报一些奇怪的错误。
bool IsEqual<char*>(char* x, char* y)
{
return strcmp(x, y) == 0;
}
int main()
{
cout << IsEqual(1, 1) << endl; //1
cout << IsEqual(1.1, 2.2) << endl; //0
char a1[] = "2021bsdt";
char a2[] = "2021bsdt";
cout << IsEqual(a1, a2) << endl; //0
return 0;
}
//特化步骤一:基础的函数模板
template<class T>
bool IsEqual(T x, T y)
{
return x == y;
}
bool IsEqual(char* x, char* y)
{
return strcmp(x, y) == 0;
}全特化是将模板参数列表中的全部参数都确定化
// 演示:类模板全特化
#include <iostream>
using namespace std;
// 通用类模板定义
template<typename T1, typename T2>
class dh
{
public:
// 构造函数:名字必须和类名相同(C++语法规定)
// 在创建对象时自动调用,用于初始化或打印提示
dh()
{
cout << "dh<T1,T2>" << endl; // 输出说明调用的是通用模板
}
private:
T1 _d; // 成员变量,类型为 T1
T2 _h; // 成员变量,类型为 T2
};
// 类模板的全特化版本
// 针对 <double,double> 这种情况,提供单独的实现
template<>
class dh<double, double>
{
public:
// 构造函数:同样名字必须与类名一致
dh()
{
cout << "dh<double,double>" << endl; // 输出说明调用的是特化版本
}
private:
double _d;
double _h;
};
int main()
{
// 使用通用类模板实例化对象
dh<int, int> a;
// 调用 dh<int,int> 的构造函数
// 输出:dh<T1,T2>
// 使用特化类模板实例化对象
dh<double, double> a1;
// 调用 dh<double,double> 的构造函数
// 输出:dh<double,double>
return 0;
}当我们实例化一个对象时,编译器会自动调用其默认构造函数,我们若是在构造函数当中打印适当的提示信息,那么当我们实例化对象后,通过观察控制台上打印的结果,即可确定实例化该对象时调用的是不是我们自己特化的类模板了。

偏特化:任何针对模板参数进行进一步的条件限制设计的特化版本。那么以下面的类模板为基础类模板进行设计
//偏特化
template<typename T1,typename T2>
class dh
{
public:
//构造函数
dh()
{
cout << "dh<T1,T2>" << endl;
}
private:
T1 _d;
T2 _h;
};争对以上的模板,我们可以偏特化成两种模式,如下。
将模板参数列表中的部分参数进行特化
//部分偏特化
template<typename T2>
class dh<int,T2>
{
public:
dh()
{
cout << "dh<int,T2>" << endl;
}
private:
int _d;
T2 _h;
};可以针对模板参数更进一步进行条件限制设计出来的特化版本,同时我们进行设计的特化的类模板中的成员没有必要追求和原类模板的成员一样,应该具体根据实际的应用场景去进行设计成员
//针对指针的进一步限制
template<typename T1,typename T2>
class dh<T1*, T2*>
{
public:
dh()
{
cout << "dh<T1*,T2*>" << endl;
}
private:
T1 _d;
T2 _h;
};
//针对引用的进一步限制
template<typename T1, typename T2>
class dh<T1&, T2&>
{
public:
dh()
{
cout << "dh<T1&,T2&>" << endl;
}
private:
T1 _d;
T2 _h;
};//部分偏特化
template<typename T1, typename T2>
class dh
{
public:
dh()
{
cout << "dh<T1,T2>" << endl;
}
private:
T1 _d;
T2 _h;
};
template<typename T2>
class dh<int,T2>
{
public:
dh()
{
cout << "dh<int,T2>" << endl;
}
private:
int _d;
T2 _h;
};
//针对指针的进一步限制
template<typename T1,typename T2>
class dh<T1*, T2*>
{
public:
dh()
{
cout << "dh<T1*,T2*>" << endl;
}
private:
T1 _d;
T2 _h;
};
//针对引用的进一步限制
template<typename T1, typename T2>
class dh<T1&, T2&>
{
public:
dh()
{
cout << "dh<T1&,T2&>" << endl;
}
private:
T1 _d;
T2 _h;
};
int main()
{
//dh<int, int> a;
//dh<double, double> a1;
dh<double, int> a;
dh<int, double> a1;
dh<int*, int*> a2;
dh<int&, int&> a3;
return 0;
}
什么是分离编译: 一个程序(项目)由多个源文件共同实现,而每个源文件单独编译形成目标文件,最后将所有的目标文件链接起来形成单一的可执行文件的过程称为分离编译模式
//stack.h
#pragma once
#include <deque>
namespace dh
{
template<typename T, typename Container = deque<T>>
class stack
{
public:
void push(const T& val);
T& top()
{
return _con.back();
}
private:
Container _con;
};
}//stack.c
using namespace std;
#include "Stack.h"
namespace dh
{
template<typename T, typename Container>
void stack<T, Container>::push(const T& val)
{
_con.push_back(val);
}
}//test.c
#include <iostream>
using namespace std;
#include "Stack.h"
int main()
{
dh::stack<int> st;
st.push(1);
st.top();
return 0;
}
在 C++ 中,模板的实例化依赖于具体的类型参数。
.cpp 文件中,那么在别的 .cpp 中使用这个模板时,编译器只看到了声明,却找不到定义,导致无法实例化出具体函数的实现。test.cpp 里,stack<int> 确定了类型,所以编译器尝试实例化。像 top 这种在头文件里有定义的函数就能成功实例化;但 push 只有声明,没有定义,实例化失败,缺少地址。Stack.cpp 里虽然有 push 的定义,但因为没有遇到 stack<int> 这种具体类型,编译器不会生成对应的实例化代码,导致符号表里也没有函数实体。push 找不到对应的实现,就会报链接错误。

👉 所以模板代码一般要写在头文件里,保证在需要实例化的地方编译器能同时看到声明和定义;如果非要把定义放在 .cpp 里,就需要显示实例化(template class stack<int>;)来告诉编译器生成对应的函数实现。
声明和定义分离编译之后将声明和定义放在同一个源文件中
//stack.h
#pragma once
#include <deque>
namespace dh
{
template<typename T, typename Container = deque<T>>
class stack
{
public:
void push(const T& val);
T& top()
{
return _con.back();
}
private:
Container _con;
};
template<typename T, typename Container>
void stack<T, Container>::push(const T& val)
{
_con.push_back(val);
}
}//test.c
#include <iostream>
#include "Stack.h"
using namespace std;
int main()
{
dh::stack<int> st;
st.push(1);
st.top();
return 0;
}
当模板的定义写在头文件里时,包含头文件的 .cpp 文件在编译阶段就能同时看到声明和定义。
test.cpp 中使用 stack<int> 时,模板参数 T 被确定为 int,编译器会立即实例化 stack<int>。push 和 top 都在头文件中有完整的定义,所以这两个函数也会被实例化,生成对应的函数实体。test.o 的符号表里就包含了 push 和 top 的地址。push 和 top。通过本文的学习,我们可以系统掌握 C++ 模板的关键知识点:
👉 模板的核心价值在于 泛型编程 + 编译期计算,它不仅提升了代码复用性,也让程序在保持灵活性的同时具备更高性能。理解这些机制,能帮助我们在写库、写框架时更好地驾驭 C++ 的高级特性。