📌 相关专栏
- 【Linux专栏】
- 【C语言专栏】
- 【测试专栏】
- 【MySQL专栏】
- 【C++ 专栏】
📌 相关文章推荐
- 【C++】 深入容器适配器:从零实现stack与queue
- 【C++】 String 核心用法:构造、遍历、容量与优化技巧
- 【C++】手写双向链表:list容器模拟实现
- 【C++】C++继承入门(上):继承语法与基本特性详解
很高兴你点开这篇文章✨
这里会持续更新更多有用的内容,关注我,一起慢慢变好呀
👍 点赞 ⭐ 收藏 💬 评论
文章目录
- 前言
- 1. 非类型模板参数
-
- 1.1 基本概念
- 1.2 使用限制
- 1.3 非类型参数 vs 宏定义
- 2. array:非类型参数的典型应用
-
- 2.1 array 与原生数组的对比
- 2.2 传参时的区别
- 2.3 越界检查对比
- 3. 模板特化概述
-
- 3.1 为什么需要特化?
- 3.2 特化的概念
- 4. 函数模板特化
-
- 4.1 特化的语法
- 4.2 特化的步骤
- 4.2 特化的陷阱
- 4.3 函数模板特化的建议
- 5. 类模板全特化
-
- 5.1 全特化的语法
- 5.2 优先级规则
- 6. 类模板偏特化
-
- 6.1 部分参数特化
- 6.2 匹配规则
- 7. 指针与引用的偏特化
-
- 7.1 指针类型的偏特化
- 7.2 引用类型的偏特化
- 7.3 指针偏特化的价值
- 8. 完整测试用例
-
- 8.1 非类型参数测试
- 8.2 函数模板特化测试
- 8.3 类模板特化测试
- 9. 总结
- 本文代码链接
前言
在基础模板学习中,我们常用的模板参数是 类型形参(如 template)。但实际开发中,有时还需要给模板传递编译期常量(如固定数组大小、指定缓存容量),这时 非类型模板参数 就派上用场了。
- 此外,当通用模板逻辑对某些特殊类型(如指针)不适用时,就需要 模板特化 来为这些类型编写专属逻辑。本文将从这两个方向深入讲解 C++ 模板的进阶特性。
1. 非类型模板参数
1.1 基本概念
非类型模板参数是用编译期可确定的常量作为模板参数,在模板内部可以直接当常量使用。
// 固定数组大小的 Stack 类
template<class T, size_t N = 0> //:error C7592: “double”类型的非类型模板参数至少需要“std:c++20”
//:必须是整型(int、size_t等)
class Stack
{
private:
T _arr[N]; // N 是编译期常量,作为数组大小
};
1.2 使用限制
| 允许的类型 | 整型(int、size_t、char、bool 等) |
| C++20 前 | 不支持 double、float 等浮点类型 |
| C++20 起 | 支持 double(需指定 /std:c++20) |
template<class T, double N> //:error C7592: “double”类型的非类型模板参数至少需要“std:c++20”,这里是C++17
//:必须是整型(int、size_t等)
class Stack {};
1.3 非类型参数 vs 宏定义
- 宏定义:所有数组大小都是 N,不能按需使用
- 非类型参数:每个实例化可指定不同大小
#define N 10 // 宏定义:所有数组大小都是 10,不能按需使用
template<class T, size_t N = 0> // 非类型参数:每个实例化可指定不同大小
class Stack {};
Stack<int> st0; // N = 0
Stack<int, 5> st1; // N = 5
Stack<int, 10> st2; // N = 10(相当于实例化出不同的类)
优势 :非类型模板参数更加灵活,可以为不同实例指定不同大小,而宏定义是全局统一的。
2. array:非类型参数的典型应用
2.1 array 与原生数组的对比
std::array 是 C++11 引入的静态数组容器,使用非类型模板参数固定大小:
| 内存位置 | 栈/全局 | 栈 |
| 大小信息 | 传参时退化丢失 | 保留大小信息 |
| 范围 for | 传参后不能用 | 始终可用 |
| 越界检查 | 仅警告(读不检查) | operator[] 断言检查 |
| 容器接口 | 无 | 有迭代器等 |
2.2 传参时的区别
- 原生数组传参:退化为指针,丢失大小信息
- 不能使用范围 for(不知道大小)
- array 传参:保留完整类型信息
- 可以使用范围 for
// 原生数组传参:退化为指针,丢失大小信息
void func(int* a)
{
// ❌ 不能使用范围 for(不知道大小)
// for (auto e : a) { }
}
// array 传参:保留完整类型信息
void func(array<int, 10>& a)
{
// ✅ 可以使用范围 for
for (auto e : a)
{
cout << e << " ";
}
}
2.3 越界检查对比
- 普通数组:不检查越界读,输出随机垃圾值
- 越界读,只报警告
- array:重载 operator[ ],会进行断言检查
- 越界断言(debug 模式)
void test1()
{
int a1[10];
array<int, 10> a2;
// 普通数组:不检查越界读,输出随机垃圾值
cout << a1[10] << endl; // 越界读,只会报警告
// array:重载 operator[],会进行断言检查
// cout << a2[10] << endl; // 越界断言(debug 模式)
}
3. 模板特化概述
3.1 为什么需要特化?
模板的核心是 通用,但遇到特殊类型时,通用逻辑可能失效:
- 比如用模板比较指针时,默认会比较“地址”而非指针“指向的内容” —— 这时候就需要 “模板特化”,为特殊类型写专属逻辑,即特化是用来解决“特殊类型”适配问题的
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 问题:比较指针时,比较的是地址而非指向的值
int* p1 = &i1;
int* p2 = &i2;
cout << Less(p1, p2) << endl; // 比较地址,不是 11 < 10
3.2 特化的概念
特化为特殊类型编写专属逻辑,分为两类
- 特化 :所有模板参数都被指定为具体类型
- 偏特化(半特化):部分模板参数被特化,或添加指针/引用等限制
4. 函数模板特化
4.1 特化的语法
// 1. 基础函数模板
template<class T>
bool Less(const T& left, const T& right)
{
return left < right;
}
// 2. 全特化语法(不推荐,问题较多)
template<>
bool Less<int*>(int* const& left, int* const& right)
{
return *left < *right;
}
4.2 特化的步骤
必须要先有一个基础的函数模板
关键字 template 后面要接一对空的尖括号 :<>
函数名后面跟一对尖括号,尖括号中指定需要特化的类型
函数形参表:必须要和模板函数的"基础参数类型"完全相同(后面会进行详细讲解),如果不同编译器可能会报一些奇怪的错误。
4.2 特化的陷阱
函数模板特化容易出现问题,推荐使用普通函数重载:
// ❌ 错误:const 修饰的歧义
// bool Less<int*>(const int*& left, const int*& right) // 与模板不匹配
// ✅ 正确:普通函数重载(推荐)
bool Less(int* left, int* right)
{
return *left < *right;
}
// 处理 const int* 类型
bool Less(const int* left, const int* right)
{
return *left < *right;
}
4.3 函数模板特化的建议
核心规则 :当普通函数和模板函数都能匹配调用时,编译器会优先选择普通函数。因此,直接编写普通函数重载即可,无需使用函数模板特化。
5. 类模板全特化
5.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; }
void func() {} // 特化版本可以新增方法
};
5.2 优先级规则
当同时存在全特化和偏特化时,编译器优先选择最匹配的版本:
void test3()
{
Data<int, int> d1; // 通用版本(无匹配特化)
Data<int, double> d2; // 全特化版本
Data<char, double> d3; // 偏特化版本(后面会讲)
}
6. 类模板偏特化
6.1 部分参数特化
偏特化可以只特化部分模板参数:
// 通用版本
template<class T1, class T2>
class Data {};
// 偏特化:只固定第二个参数为 double
template<class T1>
class Data<T1, double>
{
public:
Data() { cout << "Data<T1, double> 偏特化" << endl; }
void func() { cout << typeid(T1).name() << endl; }
};
6.2 匹配规则
Data<int, int> d1; // 通用版本(第二个参数不是 double)
Data<char, double> d3; // 偏特化版本(第二个参数是 double)
7. 指针与引用的偏特化
7.1 指针类型的偏特化
// 两个参数偏特化为指针类型
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
Data()
{
cout << "Data<T1*, T2*> 偏特化–参数更进一步限制" << endl;
// 这里可以获取到原类型 T1、T2(而非指针类型)
T1 i = 0; // T1 是原类型
T1* pi = &i;
}
void func()
{
cout << typeid(T1).name() << endl; // 打印原类型名
cout << typeid(T2).name() << endl;
}
};
7.2 引用类型的偏特化
// 两个参数偏特化为引用类型
template<class T1, class T2>
class Data<T1&, T2&>
{
public:
Data()
{
cout << "Data<T1&, T2&> 偏特化–参数更进一步限制" << endl;
}
void func()
{
cout << typeid(T1).name() << endl; // 打印引用绑定的类型
cout << typeid(T2).name() << endl;
}
};
7.3 指针偏特化的价值
价值 :可以同时获取指针类型(T*)和原类型(T)
void test4()
{
// 匹配指针偏特化版本
Data<char*, double*> d4;
d4.func(); // 打印:char double(原类型,不是 char* double*)
// 匹配引用偏特化版本
Data<char&, double&> d5;
d5.func(); // 打印:char double
}
8. 完整测试用例
8.1 非类型参数测试
void test1()
{
Stack<int> st0; // N = 0(缺省值)
Stack<int, 5> st1; // N = 5
Stack<int, 10> st2; // N = 10(三个不同的类)
int a1[10];
array<int, 10> a2;
func(a1); // 传参退化,不能用范围 for
func(a2); // 保留类型,可以用范围 for
}
8.2 函数模板特化测试
void test2()
{
int i1 = 11, i2 = 10;
cout << Less(i1, i2) << endl; // 0(11 < 10?false)
int* p1 = &i1, * p2 = &i2;
cout << Less(p1, p2) << endl; // 0(比较指向的值)
const int* p3 = &i1, * p4 = &i2;
cout << Less(p3, p4) << endl; // 0(const 版本)
}
8.3 类模板特化测试
void test5()
{
Data2<int, int> d1; // 通用版本
Data2<char*, double*> d4; // 指针偏特化版本
d4.func(); // 打印:char double
Data2<char*, double> d6; // 部分偏特化(第二个参数 double)
d6.func(); // 打印:char
}
9. 总结
C++ 模板的进阶特性:
| 非类型模板参数 | 编译期常量作为模板参数,用于固定数组大小等 |
| array 容器 | 非类型参数的典型应用,比原生数组更安全、更易用 |
| 函数模板特化 | 语法复杂,推荐使用普通函数重载替代 |
| 类模板全特化 | 所有参数指定具体类型 |
| 类模板偏特化 | 部分参数特化,或添加指针/引用限制 |
特化优先级 :普通函数(非模板) > 全特化 > 偏特化 > 通用模板
| 固定大小容器 | 使用非类型模板参数 |
| 函数针对特殊类型处理 | 使用普通函数重载 |
| 类针对特殊类型处理 | 使用偏特化(更灵活) |
| 需要同时获取指针和原类型 | 使用指针偏特化 |
本文代码链接
https://gitee.com/ayidyy/cyvyan11/commit/03f32164959d87fa54e627b99cad2d246daed3a5
谢谢你看到这里呀
如果喜欢这篇内容,点个关注,下次更新不迷路✨
👍 点赞 ⭐ 收藏 💬 评论




