目录
预备知识:在C++初阶开始的部分我写了一篇模板初阶,如果不了解模板这个东西,就去看这篇博客:C++初阶-模板初阶-CSDN博客文章浏览阅读791次,点赞28次,收藏20次。class 类模板名// 类内成员定义和之前的方式一样,当然,不是因为它是类就只能用class来定义参数,也可以用typename定义参数,只是现在没学到后面,不知道二者的区别而已。// 类模版public:_size = 0;// 模版不建议声明和定义分离到两个文件.h 和.cpp会出现链接错误,具体原因后面会讲// 扩容++_size;https://blog.csdn.net/2401_86446710/article/details/147566820?spm=1011.2415.3001.10575&sharefrom=mp_manage_link
1. 非类型模板参数
在之前学习的容器stack中:
这个容器第二个模板参数为Container,但是我们在手动实现栈的时候需要用到Container的这些函数:empty、size、back、push_back、pop_back,也就是说,如果容器不满足这些函数的实现,那么就不能作为第二个模板参数,而且在模板初阶的时候当时我们说typename和class作为模板参数类型是没什么区别的,但是在模板进阶就不一样了,这个区别会在模板进阶里面进行讲解。
模板参数分为类型形参和非类型形参,类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。如,我们定义一个静态的栈:
#include<iostream>
using namespace std;
template<class T,size_t N=10>
class mystack
{
private:
T _a[N];
int _top;
};
int main()
{
//定义一个能存储10个元素的栈
mystack<int> k1;
//定义一个能存储20个元素的栈
mystack<int, 20> k2;
return 0;
}
我们可以通过控制非类型模板参数来控制内部数组的大小,但是要在定义时一定要写死(要么就加一个缺省值给非类型模板参数,否则一定要显式给值给非类型模板参数),即:必须是编译期常量:所有非类型模板参数必须在编译时确定。
C++ 标准允许以下类型的非类型模板参数:
-
整型(包括
int
,char
,short
,long
,bool
等) -
枚举类型
-
指向对象或函数的指针
-
指向对象或函数的引用
-
std::nullptr_t
-
浮点类型(C++20 起)
这个非类型模板参数的应用就是我们C++里面的array容器(静态数组):
讲到这里我就讲解一些array相关的知识,若array<int,10) a1,则a1的size为10,而且这样还不会进行初始化,相当于我们的int arr[10];最大的区别就是前者有迭代器,array增加了越界的检查,原来的arr只是进行了简单的检查(这个检查就是抽查),我们若arr[11]= 0就会报错,但是arr[12]=0则不会报错,但是array可以检查到。
此外我们若cout<<arr[11]<<endl;的话就不会报错,因为编译器只会检查越界写,无法解决越界读,写就是发生了arr[11]的修改或赋值的情况。而array都可以检查到。此外array在检查时是直接assert断言,当越界时是直接终止程序了,而且assert只在debug时起到作用,而assert在release时是不会报错的,因为它会直接终止程序。相对于vector就是用at来抛异常,所以我们在实践中常用:vector。此外,array不太好用,静态数组会占用栈帧的空间,栈是不大的,容易栈溢出,比较消耗空间,实际我们都建议使用vector。
感兴趣的可以到C++官网中,官网链接:Reference - C++ Referencehttps://legacy.cplusplus.com/reference
中搜索array了解更多。
非类型模板参数主要还是用来固定数组的大小,我们只要了解即可。
2.模板的特化
2.1概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些
错误的结果,需要特殊处理。如:
//模板的特化
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{ }
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
private:
int _year = 2025;
int _month = 1;
int _day = 1;
};
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
Date d1(2025, 7, 7);
Date d2(2025, 7, 8);
cout << Less(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl;
return 0;
}
则运行结果为:
我们比较p1和p2是通过比较二者的地址进行下去的,容易出现问题,所以我们可以进行特化处理:
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
我们是在原模板的基础上写,所以一定要有原模板函数,针对其他类型调用原模板函数,而这个类型则调用上面这个函数。
所以总结一下就是函数特化就是真的某些特定类型进行特殊化处理,模板特化中分为函数模板特化与类模板特化。
2.2函数模板特化
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同,编译器可能会报一些奇
怪的错误。
但是这样写好像感觉没必要,因为模板参数都没了,所以衍生出另外一种写法:
bool Less(Date* left, Date* right)
{
return *left < *right;
}
我们本来写模板就是为了适应多种情况的结果,所以刚刚那个完全是为了一个情况定做的,直接写成这样还更简单一些,实践中也喜欢写成这样,但这是建议!
这个函数不是函数模板特化了,因为压根不符合特化规则。
不过刚刚的这种写法:
template<class T>
bool Less(T left, T 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;
}
我们仔细观察发现这个版本const变到Date*后面了,这是因为如果const放在*前面就修饰Date*,导致Date*不可修改,而原来是让Date不可修改,所以我们要把const放到*后面,这也是一个比较重要的一步!
2.3类特化
2.3.1全特化
全特化即将模板参数列表中的所有参数都确定化,如:
//类模板的全特化
//普通类模板
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//全特化类版本
template<>
class Data<int, char>
{
public:
Data() { cout << "Data<int, char>" << endl; }
private:
int _d1;
char _d2;
};
类模板的全特化和函数模板的特化差不多,所以我们观察发现:如果我们Data<int,char>会直接调用第二个特化过的类。
此外我们可以根据之前的Less写成一个仿函数:
//类模板的全特化
//普通类模板
template<class T>
class Less
{
public:
bool operator()(const T& a1, const T& a2)
{
return a1 < a2;
}
};
//全特化类模板
template<>
class Less<Date*>
{
public:
bool operator()(Date* const& a1, Date* const& a2)
{
return *a1 < *a2;
}
};
2.3.2偏特化
偏特化有以下两种表现形式
2.3.2.1部分特化
顾名思义,就是将模板参数表中的一部分参数特化,如:
//类模板的偏特化
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//1
template<class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};
我们发现,这个template后面的<>多了一个class T1,这个代表没有特化的部分,那这样也是没有问题的:
template<class T2>
class Data<int, T2>
{
public:
Data() { cout << "Data<int, T2>" << endl; }
private:
int _d1;
T2 _d2;
};
但是不建议特化版本两个都存在,因为如果二者都存在会导致如果参数是int int时没有匹配的一个类而导致出错,要么就再加一个:class Data<int,int>的特化版本,或者就删掉一个特化版本,也就是说,我们如果对有多个模板参数的类,我们只特化第一个模板参数也是没有问题的!
2.3.2.2参数再进一步限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一
个特化版本。如:
//类模板的偏特化
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//2
//这个template后面的可以为typename也可以为class,这个是没有区别的
//两个参数偏特化为指针类型
//template <typename T1, typename T2>
template <class T1, class T2>
class Data <T1*, T2*>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//两个参数偏特化为引用类型
//template <class T1, class T2>
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1& _d1;
const T2& _d2;
};
我们之前的仿函数也可以改为:
//偏特化类模板
template<class T>
class Less<T*>
{
public:
bool operator()(T* const& a1, T* const& a2)
{
return *a1 < *a2;
}
};
这个参数再进一步限制的情况下,我们要把template后面的<>全都从原函数模板参数原封不动的copy过去。
这样也产生了一个问题,能不能把偏特化的部分特化和参数再进一步限制两者结合起来放到同一个类模板里面?
实际上是可以的,不过难度比较高,解释如下(回答由deepseek生成):
不能直接在同一个类模板中同时使用“对参数进一步限制”和“部分特化(partial specialization)”,因为它们的语法和匹配规则不同。
-
部分特化(Partial Specialization):
-
对模板参数的部分进行特化(如
template<class T1> class Data<T1, int>
)。 -
语法上必须定义一个新的模板类。
-
-
对参数进一步限制:
-
例如,用
std::enable_if
或requires
约束T
必须是整数类型。 -
通常在主模板或偏特化中用 SFINAE 或 C++20 约束 实现。
-
如果想在偏特化中同时:
-
部分特化(如
Data<T1, int>
), -
对剩余参数进一步约束(如
T1
必须是整数),
用 C++20 的 requires
最清晰:
template<class T1, class T2>
class Data { /* 默认实现 */ };
// 偏特化 + 约束:T2=int,且 T1 必须是浮点数
template<class T1> requires std::is_floating_point_v<T1>
class Data<T1, int> {
public:
Data() { std::cout << "Data<floating T1, int>\n"; }
};
不能直接在偏特化里加 enable_if
,但可以通过:
-
SFINAE 约束主模板(C++11/14),
-
requires
约束偏特化(C++20), -
完全特化解决歧义。
最佳实践:
-
如果支持 C++20,优先用
requires
。 -
否则,用 SFINAE 约束主模板 + 偏特化。
-
对
Data<int, int>
这种歧义情况,提供完全特化。
3.模板分离编译(了解)
3.1什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有
目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
通常一个程序执行分为以下阶段:
在vs系列的编译器中,生成的可执行程序是.exe等其他文件,在Liunx中泽生成后缀为.out的文件,而在最后一个结点则把函数地址等传入给每个函数调用。
如果出现了链接错误,那么就是在链接阶段时找不到函数,就是说该函数在调用那个被调用的函数时有声明但是没有定义。在普通函数中,是支持函数的声明和定义分离的,但在函数模板和类模板都不支持声明定义分离的,即模板都不可以在声明和定义时分到不同文件中,因为模板不会编译成指令,主要是因为模板在被编译时还有一个步骤:实例化,不知道把它实例化为什么,只有在定义时才知道,即只有到最后一步才知道,无法放入符号表,也就是无法完成第三步(生成符号表把具体函数的地址放到符号表里面)。
因为在开始的时候所有文件是不会交互的,这是为了加快编译速度,如果在有声明的地方知道T实例化为什么,但是有定义的地方却不知道T实例化为什么。所以我们在下面代码就是这种情况:
3.2模板的分离编译
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
实际上#include"a.h"是只包含了声明,实际调用的是定义,所以这个运行是错误的,因为找不到定义,而这个错误是:链接错误在开始的时候是Add(?)的,到最后一步找的才把?变成函数的地址,链接就是把所有文件链接起来,开始是会把所有的地址放到一个符号表里面的,到链接阶段再找,但在a.cpp中,编译器没看到对Add模板函数的实例化,也就无法生成具体的加法函数,因此才报错。
3.3解决办法
我们可以把声明和定义放到同一个文件里面,如我们把Add函数模板的定义放到main.cpp中,或者把定义放到.h文件中,这样就能解决问题;如果实在不行就在模板定义的位置显式实例化。这种方法不实用,不推荐使用。因为模板定义位置显示实例化了就不能体现该模板的功能了,如果是其他类型就还是会出现链接错误。
4.模板中class和typename的区别
4.1二者都可以使用的情况
这种情况下两种都可以。也就是说:在使用基本模板的时候二者是没有任何区别的,但是在其他的情况下,typename和class一个是类型名称,一个是类,本来就有区别了。所以我们在使用基本模板的场景下是可以随意交换使用的!
template <class T> // 正确
void foo(T t) {}
template <typename T> // 同样正确
void bar(T t) {}
4.2必须使用 typename
的场景
当模板参数是 嵌套依赖类型 时,必须用 typename
告诉编译器这是一个类型,否则会编译错误。
示例:访问嵌套类型
template <class T>
struct MyContainer {
using value_type = T; // 嵌套类型
};
template <class Container>
void print(const Container& c) {
// 必须加 typename,因为 Container::value_type 是依赖模板参数的类型
typename Container::value_type x = *c.begin();
std::cout << x;
}
-
如果不加
typename
,编译器会假定Container::value_type
是一个静态成员变量,导致语法错误。
在模板中声明返回值类型
template <class T>
typename T::value_type get_first(const T& container) { // 必须用 typename
return *container.begin();
}
4.3 必须使用 class
的场景
在 模板模板参数(template template parameter) 中,只能用 class
(C++17 前):
template <template <class> class Container> // 正确:只能用 class
class MyClass {
Container<int> data;
};
// C++17 后可以用 typename(但部分编译器可能不支持)
template <template <typename> typename Container> // C++17 允许
class MyClass2 {};
4.4总结
-
通用规则:声明模板类型参数时,优先用
typename
。 -
必须用
typename
:-
当引用 嵌套依赖类型(如
T::value_type
)。
-
-
必须用
class
:-
在 C++17 之前声明 模板模板参数(如
template <template <class> class X>
)。
-
-
其他情况:两者完全等价,按团队规范选择。
5.总结
模板这一个东西用到的地方还是比较多的,建议了解一下第二个模块:模板的特化。
模板是一个非常好用的东西,在C++进阶部分也常使用,所以需要掌握模板的特化和基本特性与写法。
好了,这就是全部C++初阶的内容,下一讲将开启C++进阶的内容了,以后的难度是比较高的,所以建议多搜索资料哦,下讲将讲解:C++进阶-继承,喜欢的可以一键三连哦,下讲再见!