《C++初阶之STL》【模板参数 + 模板特化 + 分离编译】
【模板参数 + 模板特化 + 分离编译】
- 前言:
- ------------模板参数------------
- C++的模板参数有哪些?
- 一、类型参数
- 二、非类型参数
- 三、模板模板参数
- ------------模板特化------------
- 1. 什么是模板特化?
- 2. 为什么要使用模板特化?
- 3. 模板特化有哪些?
- 一、函数模板特化
- 函数模板特化的步骤
- 函数模板全特化
- 函数模板偏特化
- 二、类模板特化
- 类模板全特化
- 类模板偏特化
- ------------分离编译------------
- 什么是分离编译?
- 模板的分离编译要注意什么事情?
- 怎么解决模板分离编译时带来的问题?
往期《C++初阶》回顾:
/------------ 入门基础 ------------/
【C++的前世今生】
【命名空间 + 输入&输出 + 缺省参数 + 函数重载】
【普通引用 + 常量引用 + 内联函数 + nullptr】
/------------ 类和对象 ------------/
【类 + 类域 + 访问限定符 + 对象的大小 + this指针】
【类的六大默认成员函数】
【初始化列表 + 自定义类型转换 + static成员】
【友元 + 内部类 + 匿名对象】
【经典案例:日期类】
/------------ 内存管理 ------------/
【内存分布 + operator new/delete + 定位new】
/------------ STL ------------/
【泛型编程 + STL简介】
【auto关键字 + 范围for循环 + 迭代器】
【string类:详解 + 实现】
【vector容器:详解 + 实现】
【list容器:详解 + 实现】
【stack/queue/priority_queue容器适配器:详解 + 实现】
前言:
hi~ 小伙伴们大家好呀!(≧∇≦)ノ 后天就是立秋了,夏天即将结束,但炎热的天气还没过去,可能还要持续两周左右呢~(>_<)
随着夏天的结束,《C++ 初阶》也迎来了最后一篇博客:【模板参数 + 模板特化 + 分离编译】 ✨。(๑¯∀¯๑))~这时可能会有小伙伴吐槽:看博主之前的博客,全是 “C 语言、算法、数据结构、C++” 初阶的这类内容,啥时候才能学到进阶的知识呀,哈哈~( ̄▽ ̄)ゞ 让大家久等了,但是这次关于C++的故事还并没结束,下期《C++ 进阶》敬请期待哦!(ノ◕ヮ◕)ノ:・゚✧
------------模板参数------------
C++的模板参数有哪些?
模板(Template)
:是泛型编程的核心机制,允许在编写代码时使用参数化的类型
或值
,从而实现代码的复用。模板的参数分为两大类:
类型参数
和非类型参数
,此外还有模板模板参数
(较少见)
- 注:博主可没有打错字哦~,名字真的就叫作
模板模板参数
啊!
一、类型参数
类型参数(Type Parameters)
:表示模板中使用的 数据类型
- 可以是
内置类型
(如:int
、double
)- 或者是
自定义类型
(如:类、结构体)
1. 通用类型参数
使用class或typename声明,两者含义相同(推荐用typename,更清晰)
template <class T> // 类型参数 T void swap(T& a, T& b) {T temp = a;a = b;b = temp; }template <typename U> // 等价于 class U class Vector { /* ... */ };
示例调用:
swap<int>(3, 5); // 显式指定类型为 int swap<double>(3.14, 2.71); // 显式指定类型为 double// 也可隐式推导类型:swap(3, 5);(编译器自动推导为 int)
2. 默认类型参数
可以为类型参数指定默认类型,调用时若未指定则使用默认值
template <typename T = int> // 默认类型为 int class Stack {// ... };Stack<> s; // 使用默认类型 int Stack<double> d; // 显式指定类型为 double
二、非类型参数
非类型参数(Non-Type Parameters)
:表示模板中使用的 常量值
- 通常为
整型
、枚举值
或指针/引用
- C++11 后支持
std::nullptr_t
、constexpr
变量等
1. 基本语法
template <typename T, int Size> // T 是类型参数,Size 是非类型参数(整数) class Array { private:T data[Size]; // 使用非类型参数 Size 作为数组长度public:int getSize() const { return Size; } };
2. 限制条件
非类型参数必须是编译期可确定的常量,不能是变量或运行时计算的值
Array<int, 10> arr; // 合法,10 是编译期常量int n = 10; // Array<int, n> arr; // 非法,n 是变量,非编译期常量
对于指针/引用类型的非类型参数,要求其指向的对象具有静态存储期(如:
全局变量
、static变量
)int global_var = 0;template <int* ptr> void func() { /* ... */ }func<&global_var>(); // 合法,指向全局变量
代码案例:非类型参数的使用小案例
namespace mySpace
{//任务:定义的一个“静态数组”的模板类,同时要使用“非类型参数”template<class T, size_t N = 10>class array{private:T _array[N]; //1.存储数据的静态数组 注意:在编译期确定数组大小size_t _size; //2.记录数组中有效元素数量的变量public://1.实现:“普通版本的下标运算符[]的重载函数”T& operator[](size_t index) //注意:支持对数组元素的读写操作{return _array[index];}//2.实现:“const版本的下标运算符[]的重载函数”const T& operator[](size_t index)const //注意:保证在只读场景下也能通过下标访问元素,返回的是 const 引用,确保元素不会被修改{return _array[index];}//3.实现:“获取数组中有效元素的数量的操作”size_t size()const{return _size;}//4.实现:“判断数组是否为空的操作”bool empty()const{return _size == 0;}};
}
三、模板模板参数
模板模板参数(Template Template Parameters)
:是指模板本身作为参数,用于将另一个模板传递给当前模板。(注:暂时先了解一下概念即可)
------------模板特化------------
1. 什么是模板特化?
模板特化(Template Specialization)
:是模板机制的一个重要特性,允许针对特定的模板参数类型,提供模板的定制化实现。
- 它允许针对特定
类型
或值
,定制模板的行为,解决通用模板在特殊场景下的 “水土不服” 问题。- 当模板在某些特定类型下需要不同的行为或更高效的实现时,特化可以让代码更灵活、更贴合需求。
2. 为什么要使用模板特化?
在介绍的模板特化的时候,我们说模板特化是多么的厉害,但是口说无凭,模板特化真的有那么好吗?
下面的我们就来看一看模板特化的重要性。
#include <iostream>
using namespace std;//任务1:定义一个日期类
class Date
{
public:/*--------------成员变量--------------*/int _year;int _month;int _day;/*--------------成员函数--------------*///1.实现:“默认构造函数”Date(int y, int m, int d): _year(y), _month(m), _day(d){}//2.实现:“<运算符重载函数”bool operator<(const Date& other) const //注意:用于比较两个日期的先后顺序{if (_year != other._year) return _year < other._year; // 优先比较年份if (_month != other._month) return _month < other._month; // 年份相同则比较月份return _day < other._day; // 年份和月份都相同则比较日期}};//任务2:定义比较函数模板
template<class T>
bool Less(T x, T y)
{return x < y; //注意:依赖 T 类型的 operator< 实现
}int main()
{// ---------------- 基础类型比较(正确行为)----------------// 1. 实例化 Less<int>(int, int)// 调用内置的 int 类型的 < 运算符cout << Less(1, 2) << endl; // ---------------- 对象类型比较(正确行为)----------------// 2. 实例化 Less<Date>(Date, Date)// 调用 Date 类重载的 operator<Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl; // ---------------- 指针类型比较(潜在问题)----------------// 3. 实例化 Less<Date*>(Date*, Date*)// 调用指针类型的 < 运算符(比较内存地址)Date* p1 = &d1; // p1 指向 d1 的内存地址Date* p2 = &d2; // p2 指向 d2 的内存地址cout << Less(p1, p2) << endl; // 可能输出 0 或 1(取决于内存地址的随机分配)//注意:此处本意是比较对象内容,但实际比较的是指针地址return 0;
}
可以看到,
Less
函数在大多数情况下都能正常比较,但在特殊场景中会得出错误结果。在上述示例里:
p1
指向的d1
显然小于p2
指向的d2
对象- 然而
Less
内部并没有比较p1
和p2
所指向对象的内容- 而是比较了
p1
和p2
指针的地址,这就无法达成预期,进而出现错误
(哈哈,虽然博主在自己的VS上演示的结果没有出错,但是并不代表它没有问题呦)这时,就需要对模板进行特化处理。也就是在原模板的基础上,针对特定类型,进行专门化的实现。
3. 模板特化有哪些?
模板特化的分类:
C++ 的模板特化可分为:
函数模板特化
和类模板特化
两大类。
一、函数模板特化
函数模板特化的步骤
1. 先定义基础函数模板
要特化函数模板,得先有一个通用的基础函数模板,它为各类型提供默认的泛型逻辑。
比如:我们想实现 “比较两个值大小,返回
bool
结果” 的功能,先写通用模板:template <class T>bool Less(T left, T right) {return left < right; }
这个模板能处理
int
、double
、自定义类
(若重载了<
运算符 )等类型的比较,但遇到指针类型
时会因比较地址而非内容出问题,这就需要特化。
2. 添加特化声明与实现
- 特化标识:用
template<>
表明这是一个模板特化(空尖括号表示不再推导模板参数 )- 明确特化类型:在函数名后的尖括号里,写上要专门处理的特定类型(如:
Date*
类型,就写Less<Date*>
)- 保持形参匹配:特化函数的形参列表,必须和基础函数模板的形参类型严格一致,否则编译器可能报错或匹配异常
前面我们提到,
Less
函数在比较p1
和p2
时,内部并未比较它们所指向对象的内容,而是直接比较了指针的地址,这与预期不符,会导致错误。此时,需要通过模板特化来解决这一问题。
既然已经了解了模板特化的方法,接下来我们就按照上面的步骤对代码进行特化处理吧!
#include <iostream>
using namespace std;//任务1:定义一个日期类
class Date
{
public:/*--------------成员变量--------------*/int _year;int _month;int _day;/*--------------成员函数--------------*///1.实现:“默认构造函数”Date(int y, int m, int d): _year(y), _month(m), _day(d){}//2.实现:“<运算符重载函数”bool operator<(const Date& other) const //注意:用于比较两个日期的先后顺序{if (_year != other._year) return _year < other._year; // 优先比较年份if (_month != other._month) return _month < other._month; // 年份相同则比较月份return _day < other._day; // 年份和月份都相同则比较日期}};//任务2:定义比较函数模板Less
template<class T>
bool Less(T left, T right)
{return left < right; //注意:依赖 T 类型的 operator< 实现
}//任务3:对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{return *left < *right;
}int main()
{// ---------------- 基础类型比较(正确行为)----------------// 1. 实例化 Less<int>(int, int)// 调用内置的 int 类型的 < 运算符cout << Less(1, 2) << endl; // ---------------- 对象类型比较(正确行为)----------------// 2. 实例化 Less<Date>(Date, Date)// 调用 Date 类重载的 operator<Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl; // ---------------- 指针类型比较(潜在问题)----------------// 3. 实例化 Less<Date*>(Date*, Date*)// 调用指针类型的 < 运算符(比较内存地址)Date* p1 = &d1; // p1 指向 d1 的内存地址Date* p2 = &d2; // p2 指向 d2 的内存地址cout << Less(p1, p2) << endl; //注意:调用特化之后的版本了,而不是走通用模板了return 0;
}
函数模板全特化
函数模板全特化
:为函数模板的所有参数显式指定类型,完全覆盖通用逻辑。
语法:
template <> //函数模板特化 返回类型 模板函数名<特化类型>(参数列表) { ... }
示例:通用函数模板用于比较两个值的大小,但对
const char*
(C 风格字符串),默认会比较指针地址而非内容,所以需要特化:
#include <iostream>
#include <string>
using namespace std;/*--------------------- 通用模板(求最大值)---------------------*/
template <typename T>
T max_val(T a, T b)
{return a > b ? a : b;
}/*------------------ 针对const char*类型的全特化 ------------------*/
template <> // 函数模板全特化
const char* max_val<const char*>(const char* a, const char* b) //按字符串字典序比较
{return strcmp(a, b) > 0 ? a : b; // 使用 C 风格字符串比较//注意:strcmp 返回值:a > b 则 >0,a < b 则 <0,相等则 0
}int main()
{/*----------------调用通用模板:比较int值----------------*/cout << "-----调用通用模板:比较int值-----" << endl;cout << max_val(10, 20) << endl;/*---------------------- 调用全特化版本:比较字符串的内容 ----------------------*/cout << "-----调用全特化版本:比较字符串的内容-----" << endl;const char* s1 = "apple";const char* s2 = "banana";cout << max_val(s1, s2) << endl; //注意:自动匹配特化版本return 0;
}
函数模板偏特化
注意:C++不直接支持
函数模板偏特化
(语法会报错),但可通过函数重载
模拟类似效果。
- 示例:让
max_val
对指针类型,比较指针指向的值,而非指针地址,通过重载实现:指针类型的 “偏特化”
#include <iostream>
using namespace std;// 通用函数模板:比较值
template <class T>
T max_val(T a, T b)
{return a > b ? a : b;
}// 重载版本:针对指针类型(模拟偏特化)
template <class T>
T* max_val(T* a, T* b)
{return *a > *b ? a : b;
}int main()
{int x = 10, y = 20;// 调用通用模板:比较 int 值cout << max_val(5, 3) << endl; // 调用重载的指针版本:比较 *x 和 *yint* result = max_val(&x, &y);cout << *result << endl; return 0;
}
原理:
- 重载的
max_val(T* a, T* b)
并非严格意义的 “偏特化”,但利用函数重载决议,优先匹配指针类型的调用,达到 “针对部分类型定制” 的效果。- 若直接写函数模板偏特化语法(如:
template <class T> T max_val<T*>(T* a, T* b) { ... }
),编译器会报错,因此实际开发常用重载替代。
注意:
- 虽然这种方式严格一点说并不能算作是函数模板特化,但是其实现简单、可读性高且易于书写。
- 对于参数类型复杂的函数模板,使用特化反而可能会变得繁琐且容易出错,因此,不建议对函数模板进行特化,而应优先考虑使用函数重载或类模板特化来替代。
二、类模板特化
类模板特化
:为类模板的特定参数,定制类的实现,又分为:全特化和偏特化
类模板全特化
类模板全特化
:为类模板的所有参数显式指定类型,完全替换通用类的实现。
语法:
template <> //空模板参数列表,表示特化 class 模板类名<特化类型> //类模板特化 { ... };
示例:假设有通用类模板
MyContainer
,为T=int, N=10
全特化:
#include <iostream>
using namespace std;// 通用类模板:存储 T 类型,容量为 N
template <class T, size_t N = 10>
class MyContainer
{
public:MyContainer(){cout << "通用类模板构造" << endl;}void print(){cout << "通用容器:存储类型 T,容量 " << N << endl;}
};// 类模板全特化:针对 T=int,N=10
template <>
class MyContainer<int, 10>
{
public:MyContainer(){cout << "全特化类构造(int, 10)" << endl;}void print(){cout << "特化容器:专门存储 int,容量 10(定制逻辑)" << endl;}
};int main()
{cout << "-----------调用通用类模板-----------" << endl;MyContainer<double, 5> c1;c1.print(); cout << "-----------调用全特化类-----------" << endl;MyContainer<int, 10> c2; c2.print(); return 0;
}
关键说明:
- 全特化类的实现与通用类完全独立,可自定义构造函数、成员函数等。
- 当实例化
MyContainer<int, 10>
时,编译器优先选择全特化版本。
类模板偏特化
类模板偏特化
:对模板参数的部分类型或特定条件进行特化,而非全部参数。
偏特化适用场景:
参数数量特化
:如模板有两个参数,特化其中一个。参数范围特化
:如特化指针类型、引用类型、const 类型等。
第一种场景:
针对模板参数数量的偏特化
/*------------------------通用模板(两个参数)------------------------*/template <typename T1, typename T2>
class MyClass { ... };/*----------------------偏特化为第一个参数固定为int---------------------*/template <typename T2> // 保留第二个参数 T2
class MyClass<int, T2> { ... }; // 特化第一个参数为 int
第二种场景:
针对模板参数范围的偏特化
/*----------------------------通用模板----------------------------*/
template <typename T>
class MyClass
{
public:void print(T value){cout << "通用类型: " << value << endl;}
};/*------------------------针对指针类型的偏特化-------------------------*/template <typename T> // 仍保留一个模板参数 T,表示指针指向的类型
class MyClass<T*> // 特化指针类型
{
public:void print(T* ptr) { cout << "指针地址: " << ptr << endl; }
};/*---------------------- 针对const类型的偏特化-----------------------*/template <typename T>
class MyClass<const T> // 特化 const T 类型
{// 处理 const 类型的逻辑
};/*----------------------------- 调用 -------------------------------*/
MyClass<int> obj1; // 通用类型,int
obj1.print(10); // 输出:通用类型: 10MyClass<int*> obj2; // 特化指针类型,int*
int x = 20;
obj2.print(&x); // 输出:指针地址: 0x7fff...
示例:指针类型的偏特化:让
MyContainer
对指针类型T*
,定制存储逻辑(如:打印指针地址而非值)
#include <iostream>
using namespace std;// 通用类模板:存储 T 类型,容量为 N
template <class T, size_t N = 10>
class MyContainer
{
public:
MyContainer()
{cout << "通用类模板构造" << endl;}void print()
{cout << "通用容器:存储类型 T,容量 " << N << endl;}
};// 类模板偏特化:针对 T 是指针类型(T*)
template <class T, size_t N>
class MyContainer<T*, N>
{
public:
MyContainer(T* data) : _data(data) {cout << "偏特化类构造(指针类型)" << endl;}void print()
{cout << "存储指针:地址 = " << _data << endl;}
private:T* _data;
};int main()
{
cout << "-----------调用通用类模板-----------" << endl;
MyContainer<double, 5> c1;c1.print();cout << "-----------调用指针类型的偏特化-----------" << endl;
int x = 100;MyContainer<int*> c2(&x); // 调用偏特化类(T=int*, N=默认 10)c2.print(); return 0;
}
说明:
- 偏特化后,
MyContainer<T*, N>
中的T
仍为泛型(如:int
),N
也保留默认值,但约束了T
必须是指针类型。- 实例化
MyContainer<int*>
时,自动匹配偏特化版本。
回顾与总结:
Less
函数在比较p1
和p2
(指针)时,内部没有比较它们指向对象的实际内容,而是直接比较了指针的地址,这与我们想要比较对象内容的预期不符,会引发错误。
这时,我们可以用模板特化解决问题,所以之前我们尝试过用
函数模板全特化
处理但是后来我们提到过:对于参数类型复杂的函数模板,特化过程容易变得繁琐、易错。
所以,不建议直接对函数模板特化,更推荐优先用
函数重载
或类模板特化
替代。
前面我们已经演示了用
函数重载
解决指针比较问题,接下来就用类模板特化
的方式,处理这个场景,让比较逻辑符合预期。
#include<vector>
#include<algorithm>
using namespace std;//任务1:定义一个日期类
class Date
{
public:/*--------------成员变量--------------*/int _year;int _month;int _day;/*--------------成员函数--------------*///1.实现:“默认构造函数”Date(int y, int m, int d): _year(y), _month(m), _day(d){}//2.实现:“<运算符重载函数”bool operator<(const Date& other) const //注意:用于比较两个日期的先后顺序{if (_year != other._year) return _year < other._year; // 优先比较年份if (_month != other._month) return _month < other._month; // 年份相同则比较月份return _day < other._day; // 年份和月份都相同则比较日期}};//任务2:定义比较类模板Less(仿函数)
template<class T>
struct Less
{//1.重载()运算符,使该结构体可像函数一样调用bool operator()(const T& x, const T& y) const{return x < y; //注意:依赖 T 类型的 operator< 实现}
};//任务3:比较类模板Less按照指针方式特化
template<>
struct Less<Date*>
{bool operator()(Date* x, Date* y) const{return *x < *y;}
};int main()
{// 创建三个日期对象(d2 < d1 < d3)Date d1(2022, 7, 7);Date d2(2022, 7, 6);Date d3(2022, 7, 8);// ---------------- 存储对象的 vector 排序(正确行为)----------------vector<Date> v1;v1.push_back(d1);v1.push_back(d2);v1.push_back(d3);// 使用 Less<Date> 作为比较器// 实例化 Less<Date>,调用 Date::operator< 比较对象内容sort(v1.begin(), v1.end(), Less<Date>());// ---------------- 存储指针的 vector 排序(潜在问题)----------------vector<Date*> v2;v2.push_back(&d1); v2.push_back(&d2); v2.push_back(&d3); // 使用特化模板 Less<Date*> 作为比较器// 比较的是指针指向的对象内容(*x < *y),而非指针地址sort(v2.begin(), v2.end(), Less<Date*>());return 0;
}
错误情况:若未进行类模板Less的指针方式特化处理
正确情况:若进行了类模板Less的指针方式特化处理
------------分离编译------------
什么是分离编译?
分离编译
:是一种软件开发中的编译策略,指将程序的不同部分(如:不同的源文件)分别编译为目标代码(.obj
或.o
文件),最终再通过链接器将这些目标文件
和依赖的库文件
合并成可执行程序
或库文件
的过程。
核心思想:
将大型程序拆解为多个独立编译单元(源文件),每个单元单独编译,减少重复编译的开销,提高开发效率。
关键机制:
声明与定义分离
:头文件(.h)放声明,源文件(.cpp)放实现编译单元
:每个.cpp文件及其包含的头文件构成独立编译单元符号决议
:链接器负责解决跨文件的函数/变量引用
模板的分离编译要注意什么事情?
模板的分离编译是 C++ 中一个容易引发错误的复杂问题,主要源于
模板实例化机制
与传统分离编译模型
的不兼容。
传统分离编译流程:
- 编译器独立处理每个
.cpp
文件,生成对应的.obj
文件- 链接器将所有
.obj
文件合并,解析未定义的符号(如:函数调用)
模板实例化机制:
- 模板代码(如:
template <class T> void func(T x)
)本身不是完整代码- 需要在使用时根据实参类型(如:
int
)实例化出具体代码(如:void func(int x)
)
矛盾点:
- 若模板定义(如:
func
的实现)放在.cpp
文件中,编译器编译该文件时无法得知未来会被哪些类型实例化,因此不会生成具体代码- 当其他文件(如:
main.cpp
)使用该模板时,编译器只能看到模板声明,无法找到对应实例化的定义,导致链接错误
常见错误示例:错误的分离编译结构
/*--------------------------func.h(声明模板)--------------------------*/
template <class T>
void func(T x);/*--------------------------func.cpp(定义模板)--------------------------*/
#include "func.h"template <class T>
void func(T x)
{/* ... */
}/*--------------------------main.cpp(使用模板)--------------------------*/
#include "func.h"
int main()
{func(42); // 编译时找不到 func<int> 的定义,链接错误!return 0;
}
怎么解决模板分离编译时带来的问题?
解决模板分离编译问题的方法主要有两种:
将模板定义放在头文件中
使用显式实例化
最推荐第一种方法。毕竟,解决模板分离编译问题的核心就是:让编译器在实例化模板时能同时看到声明与定义。
- 将声明和定义写在同一个头文件中,从根源上避免分离编译带来的符号解析问题,是最简单直接且兼容性最好的方案。
1. 将模板定义放在头文件中
原理:让编译器在使用模板的编译单元(如:
main.cpp
)时同时看到声明和定义,直接实例化代码。示例:
/*---------------------------func.h---------------------------*/template <class T> void func(T x) { /* 直接在头文件中定义 */ }/*---------------------------main.cpp---------------------------*/#include "func.h" int main() {func(42); // 编译器在此处实例化 func<int>return 0; }
优点:简单直接,无需额外操作。
缺点:
- 头文件包含实现细节,可能导致代码膨胀。
- 修改实现需重新编译所有包含该头文件的源文件。
2. 使用显式实例化
原理:在模板定义文件中显式指定需要实例化的类型,强制编译器生成对应代码。
示例:
/*---------------------------func.h(声明模板)---------------------------*/ template <class T> void func(T x); /*---------------------------func.cpp(定义模板并显式实例化)---------------------------*/ #include "func.h"//1.定义模板 template <class T> void func(T x) {/* ... */ }//2.显式实例化 int 类型 template void func<int>(int);/*---------------------------main.cpp(使用模板)---------------------------*/ #include "func.h" int main() {func(42); // 使用已显式实例化的 func<int>return 0; }
优点:保持分离编译结构,避免头文件包含实现。
缺点:
- 需预先知道所有会被使用的实例化类型,不灵活。
- 新增类型需修改
.cpp
文件并重新编译。