
目录摘要模版特化1函数模板特化2类模版特化①类模版特化时的成员函数参数类型②全特化③偏特化3tyename的用途摘要本文记录了 C 模板特化的学习要点函数模板特化、类模板特化含成员函数参数类型、全特化、偏特化以及 typename 在模板中的用途。模版特化概念针对于模版接收到的某些类型进行特殊处理就叫做模版特化通常情况下使用模板可以实现泛型编程但代码可能对某些类型无法进行正确处理所以对于这些类型需要特殊处理~比如我们可以实现一个模版函数去泛型比较intdouble变量的大小但是如果传递过来的是一个指针类型呢此时就会直接比较地址的大小但这不是我们期望的比较方式我们期望比较的是指针指向的内容的大小所以此时我们需要对模版参数接收到指针类型这一情况进行特殊处理模板无法处理指针类型例子#includeiostream using namespace std; // 函数模板 -- 参数匹配 templateclass T bool Less(T left, T right) { return left right; } int main() { cout Less(1, 2) endl; // 可以比较结果正确 cout Less(1.4, 2.7) endl; // 可以比较结果正确 int a 10; int b 20; int* p1 a; int* p2 b; cout Less(p1, p2) endl; // 可以比较结果错误 return 0; }解释传递的p1指针指向的内容为10p2指针指向的内容为20但是Less函数返回的0表示p1地址大于p2地址而不是比较的内容所以我们需要进行函数模版的特化处理1函数模板特化// 函数模板 -- 参数匹配 templateclass T bool Less(T left, T right) { return left right; } // 对Less函数模板进行特化 template bool Lessint*(int* left, int* right) { return *left *right; } int main() { cout Less(1, 2) endl; // 可以比较结果正确 cout Less(1.4, 2.7) endl; // 可以比较结果正确 int a 10; int b 20; int* p1 a; int* p2 b; cout Less(p1, p2) endl; // 可以比较结果错误 return 0; }解释①注意写法特化部分第一行template第二行bool Lessint*(int* left, int* right)这两行缺一不可其中的int*就是代表对原函数模版参数接收到的int*类型进行特殊处理②template是空的括号第二行bool Less后面的int*中带有特殊处理的类型③这样一来当我们传递的参数为int*时就会直接匹配到我们写的函数模版特化版本❓️为啥一定要对这个函数模版进行特化处理我直接重写一个参数为int*的Less函数不也可以吗是的当然可以如下// 函数模板 -- 参数匹配 templateclass T bool Less(T left, T right) { return left right; } // 重写一个Less函数构成重载 bool Less(int* left, int* right) { return *left *right; } int main() { cout Less(1, 2) endl; // 可以比较结果正确 cout Less(1.4, 2.7) endl; // 可以比较结果正确 int a 10; int b 20; int* p1 a; int* p2 b; cout Less(p1, p2) endl; // 可以比较结果错误 return 0; }解释①这里两个Less构成重载当传递的类型为int*时会走现成的第二个Less函数而不是调用模版函数编译器原则就是这么轻松怎么来②并且也更推荐新写一个函数构成重载的做法更优秀毕竟模版特化不是针对函数模版产生的而是类模版虽然模版特化不是针对函数模版产生的但是讲解函数模版的特化有助于我们理解另一个问题那就是模版的特化引发的权限扩大问题要学会怎么处理该问题和上面同样的例子但是我们把Less参数前加上const修饰// 函数模板 -- 参数匹配 templateclass T bool Less(const T left, const T right) { return left right; }所以我们自然而然会觉得函数模版特化应该下面这么写// 对Less函数模板进行特化 template bool Lessint*(const int* left, const int* right) { return *left *right; }但此时报错解释报错原因我们写的模版特化不是上面那个模版函数的特化因为检查到参数类型不匹配❓️我们虽然傻瓜式替换但是非常严谨的把每个T都换成int*为什么还会不匹配呢因为const T中const是修饰T这个整体的T无法被修改我们想写T为int*的特化那么也应该保证int*无法被修改也就是指针本身不能被修改而const int* 中const保证的不是int*这个指针无法被修改而是指针指向的内容不能被修改所以二者类型不同导致报错正确特化代码// 函数模板 -- 参数匹配 templateclass T bool Less(const T left, const T right) { return left right; } // 对Less函数模板进行特化 template bool Lessint*(int* const left, int* const right) { return *left *right; } int main() { cout Less(1, 2) endl; // 可以比较结果正确 cout Less(1.4, 2.7) endl; // 可以比较结果正确 int a 10; int b 20; int* p1 a; int* p2 b; cout Less(p1, p2) endl; // 可以比较结果错误 return 0; }解释①正确写法int* const此时的const才是修饰的指针本身此时特化函数的参数类型和原模版函数的参数类型才匹配才不会报错②在某些地方也称此处的错误为模版特化造成的权限放大但这句话不够完善首先const T 到const int*的确是权限放大因为前者T不可修改但是后者int*这个指针可以修改所以权限放大了但不是因为权限放大导致报错而是权限放大导致类型不同从而和原模版函数的参数类型不匹配(const T 和 const int*类型不匹配)导致编译器报错认为你写的函数模版的特化和你的函数模版不匹配这才是正解~③但需要知道const int*和int* const其实只是能够修改的东西不同类型指针本身所指对象int* const❌ 不可改✅ 可改const int*✅ 可改❌ 不可改单独讨论const int*和int* const其实不涉及到权限放大只能说const所保护的东西不同但是在模版特化这里它关心的是仅仅T的权限也就是只在乎T是否被保护起来了原模版函数的T被const修饰不可被修改那么你特化中的int*也必须不能被修改但是const int*能够修改int*所以才成为T的权限被放大了在库中一个模版函数的接口参数往往是这样写的不仅有const还有引用templateclass T bool Less(const T left, const T right) { return left right; }解释const T这种写法的初心是利用其中的方便接收具有资源的类型避免重复拷贝但单独加上导致无法接收1,10,2.5这种常量(本质是权限放大)所以再用const修饰T得到const T避免权限放大这也是库中大多数参数设计这样的原因既能接受具有资源类型也能接受常量并且不能对参数进行修改确保程序的健全性同理对于这种模版函数的int*特化也会出现上文中的问题我们依旧把const放在*的后面即可// 函数模板 -- 参数匹配 templateclass T bool Less(const T left, const T right) { return left right; } // 对Less函数模板进行特化 template bool Lessint*(int* const left, int* const right) { return *left *right; }解释正确理解int* const从右往左读C 声明阅读法符号含义这是一个引用const被引用的对象是 被const修饰的int*被引用对象的类型是int*指针合起来一个指向int*的引用且这个int*是 const 的也就是引用绑定到一个常指针上指针本身不可改。它不是const int*也不是const int*编译器分得清清楚楚不用担心被误解。2类模版特化模版特化可以说就是为了类模版而产生的函数模版的特化可以用函数重载代替但是类模版的特化无法被替代此外不管是函数模版特化还是类模板特化都是因为对模版参数接收到的某些类型无法进行正确处理在下面我们就不再举例一个无法处理某些类型的成员函数了直接进行特化①类模版特化时的成员函数参数类型我们在函数模版特化时的问题代码如下// 函数模板 -- 参数匹配 templateclass T bool Less(const T left, const T right) { return left right; } // 对Less函数模板进行特化 template bool Lessint*(const int* left, const int* right) { return *left *right; } int main() { int a 10; int b 20; int* p1 a; int* p2 b; cout Less(p1, p2); return 0; }而当我们将其变成类时如下// 模板类 templateclass T class A { public: bool Less(const T left, const T right) { return left right; } }; // 模板类特化 template class Aint* { public: bool Less(const int* left, const int* right) { return *left *right; } }; int main() { int a 10; int b 20; int* p1 a; int* p2 b; Aint* aa; cout (aa.Less(p1, p2)); return 0; }解释①我们并没有把const放在*的后面但是程序编译通过且函数返回结果正确②这是因为类模板特化这个行为的主体是类而不是其中的某个函数所以没有要求类中的成员函数的参数类型完全匹配③但是仍然强烈建议成员函数的类型也匹配因为对模板类的特化本来就应该改变成员函数的参数类型尽管它允许但我们也不该这么做应该维护代码的严谨性所以还是建议下面这么写// 模板类特化 template class Aint* { public: bool Less(int* const left, int* const right) { return *left *right; } };而类模版的特化又分为全特化和偏特化②全特化全特化即特化的时候将模板参数列表中所有的参数都确定具体的类型//模板类 templateclass T1, class T2 class Data { public: Data() { cout DataT1, T2 endl; } private: T1 _d1; T2 _d2; }; //类模板全特化 template class Dataint*, int* { public: Data() { cout Dataint*, int* endl; } private: int _d1; char _d2; }; int main() { Dataint, int d1; Dataint*, int* d2; return 0; }解释写法和函数模板的特化又有些许不同特化部分第一行仍然是template但第二行变成了class Dataint*, int*而函数模板特化第二行是bool Lessint*(int* left, int* right)区别就在于函数模板特化有参数部分而类模板全特化没有去掉参数部分一个是class Dataint*, int*一个是bool Lessint*都是类似的前者多一个int*是因为我们举例全特化当然至少要举例两个模板参数的类③偏特化偏特化a特化的时候只将模板参数列表中部分参数确定具体类型另一部分还是模版Tb特化的时候所有的模版参数都规定了类型但并不具体都和T又换a例子偏特化对class DataT1, int进行处理templateclass T1, class T2 class Data { public: Data() { cout DataT1, T2 endl; } private: T1 _d1; T2 _d2; }; // 将第二个参数特化为int template class T1 class DataT1, int { public: Data() { cout DataT1, int endl; } private: T1 _d1; int _d2; }; int main() { Dataint, int d1; Dataint, char d2; return 0; }解释①Dataint, int d1;调用的是偏特化因为第二个int正好符合偏特化的第二个参数int对于编译器来说如果走偏特化都只用传递一个模版参数了何乐而不为②Dataint, char d2;调用的是模板类本身因为第二char和偏特化的int不符合只能走模板类③所以要想走偏特化那么偏特化的所有参数类型都必须满足Dataint, int d1;能调用偏特化1是因为int符合直接偷懒2是因为另一个int也能传递给偏特化的T1Dataint, char d2;不能调用偏特化因为char不匹配直接不会走偏特化不管另一个能否偷懒b例子特殊的偏特化//基础模板类 templateclass T1, class T2 class Data { public: Data() { cout DataT1, T2 endl; } private: T1 _d1; T2 _d2; }; //两个参数偏特化为指针类型 template typename T1, typename T2 class Data T1*, T2* { public: Data() { cout DataT1*, T2* endl; } private: T1 _d1; T2 _d2; }; //两个参数偏特化为引用类型 template typename T1, typename T2 class Data T1, T2 { public: Data(const T1 d1, const T2 d2) : _d1(d1) , _d2(d2) { cout DataT1, T2 endl; } private: const T1 _d1; const T2 _d2; }; int main() { Dataint, int d1;//走模板类 Dataint, char d2;//走模板类 cout endl; Dataint*, char d3;//走模板类 Dataint, char* d4;//走模板类 cout endl; Dataint*, char* d5;//走指针偏特化 cout endl; Dataint, double d6(1, 2.0);//走引用偏特化 cout endl; return 0; }解释①这也叫偏特化不要误认为两个模版都确定为了指针/引用类型就是全特化全特化指的是确定为了int* int这种具体的类型而这里是T*/T不是具体的类型所以仍然是偏特化②d3会走模板类就是因为char类型不匹配指针偏特化的参数类型同理d4的int不匹配偏特化的参数类型而d5会走指针偏特化因为两个类型是指针类型才匹配上了指针偏特化③d6会走引用偏特化因为两个类型都是引用类型才匹配上了指针偏特化④所以如果我们Dataint, double d7;那么会匹配到模板类本身int给T1double给T2但是引用类型的值必须初始化这里会报错引用未初始化而不是类型不匹配~注意①模版参数T可以接收int类型double类型同理也可以直接接收int*类型 int类型因为这些都是类型甚至还可以接收类类型但是不能传递确切的值比如1,1.0,a这不叫类型②偏特化的参数必须同时满足要求一个都不能少。传一半参数、想蹭偏特化是不成立的因为偏特化模板不认半匹配一个位置不满足整个偏特化就不命中直接落回主模板。想要一个满足、一个不满足的特殊处理就老老实实为这种形状单独定义一个偏特化而不是投机取巧传一半去蹭别人的。要么完整满足要么别特化要么专门为你的形状写新偏特化。这才是优秀做法。❓️你在第①点中说模版参数T不能接收具体的值但是在函数模版中不是直接传递给T值吗如下代码templateclass T void test(T val) // val 是函数参数它接收值 { cout val; } int main() { test(10); // 10传给 valT 同时被推导为 int类型 }解释①看起来是将值10直接传给了本该接收int类型的T但是其实内部的转化编译器做了10传给T编译器推导出T为int并且把10赋给了val②如果这里的函数参数为T val那么我们就不能传递10了应该传递值为10的变量a此时依旧可以理解为编译器推导出了a的类型为inta的值10赋给了val所以现在我们可以理解这句话了类模版的特化无法被替代例子代码//模板类 templateclass T1, class T2 class Data { public: Data() { cout DataT1, T2 endl; } private: T1 _d1; T2 _d2; }; //类模板全特化 template class Dataint*, int* { public: Data() { cout Dataint*, int* endl; } private: int _d1; char _d2; }; int main() { Dataint, int d1; Dataint*, int* d2; return 0; }你怎么也无法做到新创建一个类来替换掉这个特化即使你创建一个非模板类new_Date类把其中的成员变量全部改成int*类型的也不行因为类模板Dataint,int、Dataint*,int*—— 只能显式指定模板参数编译器永远先看模板没法像函数那样自动匹配到一个普通类。函数模板调用时Less(2,3)、Less(p1,p2)—— 函数实参和模板参数一起传编译器自动推导所以能匹配到普通函数重载。所以这就是为什么类模版的特化无法被替代3tyename的用途以前我们说模版参数中使用class和typename的效果是一样的但在某些地方必须使用typename今天就来讲讲什么地方必须使用typename正确代码#include iostream #include vector #include list using namespace std; void test1(vectorint v) { vectorint::iterator it v.begin(); // ✅ 直接取不用 typename cout test1: *it endl; } int main() { vectorint v { 10, 20, 30 }; test1(v); // ✅ 确切类型 return 0; }解释为什么test这个函数内部能够直接访问vector类的v对象的迭代器呢因为test函数的模板参数已经确切了接收到的参数就是一个vector的对象所以函数内部允许去取类内的迭代器错误代码#include iostream #include vector #include list using namespace std; // // 情况②参数是模板参数 T → 取 T::iterator 必须加 typename // templateclass T void test2(T v) { T::iterator it v.begin(); // ❌ 不加 typename 会报错 //typename T::iterator it v.begin(); // ✅ 加 typename 才能过 cout test2: *it endl; } int main() { vectorint v { 10, 20, 30 }; test2(v); // ✅ 确切类型 return 0; }解释①对于这个模版函数来说即使main中已经给你的T传递了vector对象但是函数内部依旧不能通过T去取vector对象中的迭代器因为在编译的时候这个模版函数的参数T还未确定是什么所以编译器不知道 T::iterator意味着什么是取T类中的静态成员(具体变量)还是取T类中的某一类型所以报错②而我们要取的是迭代器类型(是一种类型)从而通过迭代器类型去创建it所以我们要在此行代码前加上typename意味着告诉编译器要取的是一个类型此处语法没问题等到模版参数T接收到确切类型后你就去取类型创建对应变量it当然讲这个例子的目的在于介绍typename的用途此处代码最简单的方式直接用auto即可#include iostream #include vector #include list using namespace std; //// //// 情况③用 auto → 最省事不用管 typename //// templateclass T void test3(T v) { auto it v.begin(); // ✅ 完全不用 typename cout test3: *it endl; } int main() { vectorint v { 10, 20, 30 }; test3(v); // ✅ 确切类型 return 0; } [ 作者 ] shylyly [ 首次发布 ] 2025.3.24❌ [ 最新修改 ] 2026.9.28 [ 声明 ] 由于笔者水平有限文中难免有疏漏或不妥之处还望读者不吝赐教