【C++模版进阶】如何理解非类型模版参数、特化与分离编译?

【C++模版进阶】如何理解非类型模版参数、特化与分离编译?

🔥艾莉丝努力练剑:个人主页

专栏传送门:《C语言》《数据结构与算法》C/C++干货分享&学习过程记录Linux操作系统编程详解笔试/面试常见算法:从基础到进阶

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬艾莉丝的简介:


🎬艾莉丝的C++专栏简介:


目录

C++的两个参考文档

1  ~>  详解非类型模版参数

1.1  分类

1.2  实践

1.3  注意

2  ~>  模板的特化

2.1  模板特化概念

2.2  函数模板特化

2.2.1  结论

2.2.2  函数模版的特化步骤

2.2.3  函数模板的特点

2.3 类模板特化

2.3.1 全特化

2.3.2 偏特化 / 半特化

2.3.3  类模板特化应用示例(一):排序特化比较器实现

2.3.4  类模板特化应用示例(二):结合优先级队列实现

2.4  偏特化的两种表现方式

2.4.1  部分特化

2.4.2  参数更进一步的限制

3  ~>  模版分离编译

3.1  分离编译的概念

3.2  模板的分离编译

3.2.1  模板分离代码简单演示

a.h:

a.cpp:

main.cpp:

3.2.2  分析运行过程

3.2.3  运行结果

3.3  解决方法

3.4  拓展阅读

4  ~>  模版总结

4.1  模版的优点

4.2  模版的缺陷

本文完整代码演示

一、模版进阶完整代码演示

priority_queue.h:

queue.h:

stack.h:

Test.cpp:

二、模版分离完整代码演示

 a.h:

a.cpp:

main.cpp:

结尾


C++的两个参考文档

老朋友(非官方文档):cplusplus

官方文档(同步更新):cppreference
stack容器文档链接:stack

queue容器文档链接:queue


1  ~>  详解非类型模版参数

1.1  分类

模板参数分类型形参与非类型形参。

类型形参,即出现在模板参数列表中,跟在class或者typename之类的参数类型名称;

非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

1.2  实践

namespace bite { // 定义一个模板类型的静态数组 template<class T, size_t N = 10> class array { public: T& operator[](size_t index) { return _array[index]; } const T& operator[](size_t index)const { return _array[index]; } size_t size()const { return _size; } bool empty()const { return 0 == _size; } private: T _array[N]; size_t _size; }; }

1.3  注意

1、浮点数、类对象以及字符串是不允许作为非类型模板参数的。

2、非类型的模板参数必须在编译期就能确认结果。

2  ~>  模板的特化

2.1  模板特化概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些
错误的结果,
需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板——

可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示
例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内
容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。

此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方
式。
模板特化中分为函数模板特化类模板特化

 

2.2  函数模板特化

2.2.1  结论

 

 

 

 

函数特化其实不如去写普通函数,但是特化版本怎么写要知道。

2.2.2  函数模版的特化步骤

函数模板的特化步骤:

1、必须要先有一个基础的函数模板;

2、关键字template后面接一对空的尖括号<>;

3、函数名后跟一对尖括号,尖括号中指定需要特化的类型;

4、函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
// 函数模板 -- 参数匹配 template<class T> bool Less(T left, T right) { return left < right; } // 对Less函数模板进行特化 template<> bool Less<Date*>(Date* left, Date* right) { return *left < *right; } int main() { cout << Less(1, 2) << endl; Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout << Less(d1, d2) << endl; Date* p1 = &d1; Date* p2 = &d2; cout << Less(p1, p2) << endl;  // 调用特化之后的版本,而不走模板生成了 return 0; }

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该
函数直接给出。

bool Less(Date* left, Date* right) { return *left < *right; } 

该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化
时特别给出,因此函数模板不建议特化,优先使用重载而不是特化来处理特定类型。

2.2.3  函数模板的特点

特点:如果说这里没有特化,就会实例化这个地方的原模版。

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; }; void TestVector() { Data<int, int> d1; Data<int, char> d2; }

或者这样的——

 

 

2.3.2 偏特化 / 半特化

偏特化:也叫半特化,即任何针对模版参数进一步进行条件限制设计的特化版本(部分模版都特化)。比如对于以下模板类——

template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; };

或者这样的——

 

 

2.3.3  类模板特化应用示例(一):排序特化比较器实现

比如专门用来按照小于比较的类模板Less:

#include<vector> #include<algorithm> template<class T> struct Less { bool operator()(const T& x, const T& y) const { return x < y; } }; int main() { Date d1(2022, 7, 7); Date d2(2022, 7, 6); Date d3(2022, 7, 8); vector<Date> v1; v1.push_back(d1); v1.push_back(d2); v1.push_back(d3); // 可以直接排序,结果是日期升序 sort(v1.begin(), v1.end(), Less<Date>()); vector<Date*> v2; v2.push_back(&d1); v2.push_back(&d2); v2.push_back(&d3); // 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序 // 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象 // 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期 sort(v2.begin(), v2.end(), Less<Date*>()); return 0; }

通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排
序元素是指针,结果就不一定正确。因为:sort最终按照Less模板中方式比较,所以只会比较指
针,而不是比较指针指向空间中内容,此时可以使用类版本特化来处理上述问题:

// 对Less类模板按照指针方式特化 template<> struct Less<Date*> { bool operator()(Date* x, Date* y) const { return *x < *y; } };

特化之后,再次运行上述代码,就可以得到正确的结果。

2.3.4  类模板特化应用示例(二):结合优先级队列实现

 

  

 运行一下——

2.4  偏特化的两种表现方式

 

 

2.4.1  部分特化

将模板参数类表中的一部分参数特化。
// 将第二个参数特化为int template <class T1> class Data<T1, int> { public: Data() { cout << "Data<T1, int>" << endl; } private: T1 _d1; int _d2; };

这里我们可以看看下面的代码截图——

  

2.4.2  参数更进一步的限制

偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一 个特化版本。
//两个参数偏特化为指针类型 template <typename T1, typename T2> class Data <T1*, T2*> { public: Data() { cout << "Data<T1*, 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 << "Data<T1&, T2&>" << endl; } private: const T1& _d1; const T2& _d2; }; void test2() { Data<double, int> d1;      // 调用特化的int版本 Data<int, double> d2;      // 调用基础的模板     Data<int*, int*> d3;       // 调用特化的指针版本 Data<int&, int&> d4(1, 2);  // 调用特化的指针版本 }

这里我们可以看看下面的代码截图——

 

 


3  ~>  模版分离编译

3.1  分离编译的概念

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件(.o),最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

3.2  模板的分离编译

3.2.1  模板分离代码简单演示

a.h:
#pragma once template<class T> T TAdd(const T& left, const T& right) { return left + right; } int Add(int x, int y);
a.cpp:
#define _CRT_SECURE_NO_WARNINGS 1 #include"a.h" //// 预处理:展开头文件 //template<class T> //T TAdd(const T& left, const T& right) //{ // return left + right; //} int Add(int x, int y) { return x + y; } //// 不用< > //template //double TAdd(const double& left, const double& right); template int TAdd(const int& left, const int& right);
main.cpp:
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; #include"a.h" int main() { cout << Add(1, 2) << endl; cout << TAdd(1.1, 2.2) << endl; cout << TAdd(1, 2) << endl; return 0; }

3.2.2  分析运行过程

 

 

3.2.3  运行结果

3.3  解决方法

1、将声明和定义放到一个文件“xxx.hpp"里面或者xxx.h其实也是可以的。推荐使用这种。
2、模板定义的位置显式实例化。这种方法不实用,不推荐使用。

3.4  拓展阅读

关于分离编译,这里艾莉丝给uu们推荐一篇22年前一位大佬写的博客——

为什么C++编译器不能支持对模板的分离式编译

4  ~>  模版总结

4.1  模版的优点

1、模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生;
2、增强了代码的灵活性。

4.2  模版的缺陷

1、模板会导致代码膨胀问题,也会导致编译时间变长;
2、出现模板编译错误时,错误信息非常凌乱,不易定位错误。

本文完整代码演示

一、模版进阶完整代码演示

priority_queue.h:

#pragma once #include<vector> namespace jqj { // -------------也可以用自己写的-------------- // 仿函数 template <class T> struct Less { bool operator() (const T& x, const T& y) const { return x < y; } }; template <class T> struct Greater { bool operator() (const T& x, const T& y) const { return x > y; } }; // ---------------可以用库里面的------------------ // 默认大的优先级高 template<class T, class Container = std::vector<T>, class Compare = Less<T>> class priority_queue { public: template<class InputInterator> priority_queue(InputInterator first, InputInterator last) :_con(first, last) { //建堆 —— 向下调整算法建堆(效率比向上调整建堆高) for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--) { adjust_down(i); } } // 强制编译器生成默认构造 priority_queue() = default; void adjust_up(int child) { Compare com; // com是仿函数类型的对象,可以像函数一样去使用 int parent = (child - 1) / 2; while (child > 0) { //if (_con[child] > _con[parent]) //if (_con[parent] < _con[child]) // 交换一个位置,小于是大堆 if (com(_con[parent], _con[child])) // 和上面等价,转换成调operator() { swap(_con[child], _con[parent]); // 交换双亲和孩子节点的位置 child = parent; // 把父亲的值给儿子 parent = (child - 1) / 2; } else { break; } } } void adjust_down(int parent) { Compare com; size_t child = parent * 2 + 1; while (child < _con.size()) { /*if (child + 1 < _con.size() && _con[child + 1] > _con[child])*/ //if (child + 1 < _con.size() && _con[child] < _con[child + 1]) // 先交换位置,反过来 if (child + 1 < _con.size() && com(_con[child], _con[child + 1])) { ++child; } /*if (_con[child] > _con[parent])*/ if (com(_con[parent], _con[child])) { swap(_con[child], _con[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } //// 大堆换成小堆,以前的逻辑:符号换一下 //void adjust_up(int child) //{ // int parent = (child - 1) / 2; // while (child > 0) // { // if (_con[child] < _con[parent]) // { // swap(_con[child], _con[parent]); // 交换双亲和孩子节点的位置 // child = parent; // 把父亲的值给儿子 // parent = (child - 1) / 2; // } // else // { // break; // } // } //} //void adjust_down(int parent) //{ // size_t child = parent * 2 + 1; // while (child < _con.size()) // { // if (child + 1 < _con.size() && _con[child + 1] < _con[child]) // { // ++child; // } // // if (_con[child] < _con[parent]) // { // swap(_con[child], _con[parent]); // parent = child; // child = parent * 2 + 1; // } // else // { // break; // } // } //} //// 不可能说要变成小堆了,就把代码给改一下,那肯定是不行的 // 怎么灵活地去控制大小堆?C语言是通过函数,但是C++是尽可能不去使用函数指针这种东西 // 因为函数指针、数组指针都是很恶心的东西——类型的定义很恶心 /*void (*p)() —— 函数指针*/ //void adjust_up(int child) //{ // int parent = (child - 1) / 2; // while (child > 0) // { // if (_con[child] > _con[parent]) // { // swap(_con[child], _con[parent]); // 交换双亲和孩子节点的位置 // child = parent; // 把父亲的值给儿子 // parent = (child - 1) / 2; // } // else // { // break; // } // } //} //void adjust_down(int parent) //{ // size_t child = parent * 2 + 1; // while (child < _con.size()) // { // if (child + 1 < _con.size() && _con[child + 1] > _con[child]) // { // ++child; // } // if (_con[child] > _con[parent]) // { // swap(_con[child], _con[parent]); // parent = child; // child = parent * 2 + 1; // } // else // { // break; // } // } //} void push(const T& x) { _con.push_back(x); adjust_up(_con.size() - 1); } void pop() { swap(_con[0], _con[_con.size() - 1]); // 交换首尾位置 _con.pop_back(); adjust_down(0); } const T& top() const { return _con[0]; } bool empty() const { return _con.empty(); } size_t size() const { return _con.size(); } private: Container _con; }; }

queue.h:

#pragma once #include<vector> #include<list> #include<deque> namespace bit { // 容器适配器 // deque:双端队列 template<class T, class Container = deque<T>> class queue { public: void push(const T& x) { _con.push_back(x); } void pop() { _con.pop_front(); } const T& front() { return _con.front(); } const T& back() { return _con.back(); } size_t size() const { return _con.size(); } bool empty() const { return _con.empty(); } private: Container _con; }; }

stack.h:

#pragma once #include<vector> #include<list> #include<deque> namespace bit { //template<class T> //class stack //{ // // ... //private: // T* _a; // size_t _top; // size_T _capacity; //}; // template<class T, class Container = deque<T>> class stack { public: void push(const T& x) { _con.push_back(x); } void pop() { _con.pop_back(); } const T& top() { return _con.back(); } size_t size() const { return _con.size(); } bool empty() const { return _con.empty(); } private: Container _con; }; }

Test.cpp:

#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<stack> #include<queue> #include<algorithm> using namespace std; #include"stack.h" #include"queue.h" #include<deque> #include"priority_queue.h" //#include<queue> //// 用宏来定义常量、再用宏来决定数据大小的这种方式是不好的(像下面这里浪费了99%的空间) //// 无法修改,没办法灵活地控制 ////#define N 10 //#define N 1000 // 被迫开了1000个,对于st1来说,浪费了990个 //template<class T> //class Stack //{ //private: // T _a[N]; // int _top; //}; // //int main() //{ // Stack<int> st1; // 10 // Stack<int> st2; // 1000 // // return 0; //} //// 模版进阶 //template<class T,size_t N> //class Stack //{ //private: // T _a[N]; // int _top; //}; //template<double N> // 默认到C++20才支持int之外的类型作为常量的类型 //class AA //{}; //// “std::string”:不是非类型模板参数“str”的有效类型 //template<string str> //class BB //{}; // ------------------目前C++20就支持整型、浮点型等等---------------------- // 指针是支持的 // 基本类型、内置类型是支持的,但是自定义类型是不支持的 //template<double N,int* ptr> // C++20开始支持 //class AA //{}; template<int N,int* ptr> class AA {}; // 现代C++会提到自定义类型、类类型可以定义成常量 // 可以给缺省参数 template<class T, size_t N = 10> class Stack { private: T _a[N]; int _top; }; // ------------------------C++标准库里面有两个类型用了非类型模版参数------------------------------- #include<array> // 用非类型模版参数,一个就是array // C++里面定义的一个静态数组 // array支持迭代器,就支持范围for // 3、再一个就是传参,传参是个大坑——不允许传递数组,因此会退化成指针 // 因为在C语言里面,数组的效率是很低的,所有的数组传递都是传指针 void func(int* a,int n) // 数组大小 { //// 不能使用访问for //for (auto e : a) //{ // cout << e << " "; //} //cout << endl; } void func(const array<int,10>& a) { // 能使用访问for for (auto e : a) { cout << e << " "; } cout << endl; } //int main() //{ // // 下面两个类型虽然都是int,相当于实例化出了两个类,这样一来我们就可以灵活地控制栈的大小 // Stack<int,10> st1; // 10 // Stack<int,1000> st2; // 1000 // // // array不支持尾/头插、尾/头删——因为数据已经全开出来了,可以用[]直接访问任意位置 // // 内置类型做参数,默认不会初始化 // // 除了初始化的其他都是随机值 // array<int, 10> a1; // a1.fill(0); // 另一个成员函数,fill:填充,这里就是全部初始化为0 // a1[3] = 3; // a1[9] = 9; // // 用访问for遍历一下 // for (auto e : a1) // { // cout << e << " "; // } // cout << endl; //// 输出:-858993460 -858993460 -858993460 3 -858993460 -858993460 -858993460 -858993460 -858993460 9 // // array的好处:一大好处就是能够更好地用这种类型 // // 1、假设二维数组,第二维不想要vector动态开辟了 // // 因为它空间直接开在自己身上,而动态开辟还是会有一定的消耗,编译时就开辟,更快 // cout << sizeof(a1) << endl; // // // 数组和容器array的区别是什么呢?封装? // int a2[10]; // a2[3] = 3; // a2[9] = 9; // // 用访问for遍历一下 // for (auto e : a2) // { // cout << e << " "; // } // cout << endl; // // 数组也支持 //// 输出:-858993460 -858993460 -858993460 3 -858993460 -858993460 -858993460 -858993460 -858993460 9 // // // 2、假设链表里面每个节点存一个数组,下面的数组就做不到了 // // 再去做其他容器,或者传参,array都有普通数组达不到的优势 // list<array<int, 10>> lt; // func(a1); // 不传数组的大小过来,就不知道是多少数据 // func(a2, 10); // // array肯定能够sort // // 普通数组能不能sort // //sort(a2, a2 + 10); // 普通数组也能sort,所以这个是两者真正的差异 // //// 核心原因:指向数组的指针是天然的迭代器,符合迭代器行为,int* a,指针解引用,++就能到下一个位置 // // // //// 数组越界只能检查写,并且是抽查 // ////a2[10] = 1; // 可以查出来:对数组结束的临近位置访问通常是能够检查出来的 // //a2[15] = 1; // (远一点)查不出来 // //cout << a2[10] << endl; // 越界读是一点办法没有 // // // 上面对array都不是问题,array都可以查出来,因为它是运算符重载调用,内存严格检查 // a1[15] = 1; // cout << a1[10] << endl; // // 检查很严格,如果你要用静态数组,那么推荐用array // // ----------------------整体还是用vector比较多,array用得比较少----------------------------- // // // 除了array,还有一种非连续模板参数是bitset,在哈希表章节会介绍 —— 位图/位集合,位的集合 // // 管理比特位的容器 // // 等到哈希章节再来看 // // return 0; //} // ------------------------------模版的特化------------------------------------ // =======================函数模版的特化======================= // 特化,即特殊化处理 // 日期类:比较大小 class Date { public: Date(int year = 1900, int month = 1, int day = 1) : _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); } friend ostream& operator<<(ostream& _cout, const Date& d); // 不是成员函数,而是友元函数 // 变成内联函数 // 内联函数不能声明和定义分离;显示加inline也可以 private: int _year; int _month; int _day; }; // operator<<的实现 ostream& operator<<(ostream& _cout, const Date& d) { _cout << d._year << "-" << d._month << "-" << d._day; return _cout; } // 函数模板 —— 参数匹配 // 特点:如果说这里没有特化,就会实例化这个地方类模版 //template<class T> //bool Less(T left, T right) //{ // return left < right; //} //// 对上述函数模版实现一个特化版本 //// 特化:针对某些类型进行特殊化处理 //template<> //bool Less<Date*>(Date* left, Date* right) //{ // return left < right; // 没法比较,倾向于指针指向的内容进行比较 // //输出: // //1 // //0 //} //// 如果是有匹配类型的,就会调用对应的特化版本 //template<> // 给个空格 //bool Less<Date*>(Date* left, Date* right) // 在这里函数名中间给个模板参数 //{ // return *left < *right; // 没法比较,倾向于指针指向的内容进行比较 // // 输出: // // 1 // // 1 //} // ----想针对某些类型进行特殊化处理,就专门写一个特化版本出来---- //int main() //{ // cout << Less(1, 2) << endl; // 可以比较,结果正确 —— 用指针指向的内容比较,有意义 // //Date* p1 = &d1; // 再写得具体一点 // Date* p1 = new Date(2025, 1, 1); // /*Date* p2 = &d2;*/ // Date* p2 = new Date(2025, 1, 3); // cout << Less(p1, p2) << endl; // 可以比较,结果错误 —— 用指针进行比较,结果随机,比较没意义 // // return 0; //} // 上面只是一个简单的特化的例子,特化很恶心,下面举一个复杂的例子 // 特点:如果说这里没有特化,就会实例化这个地方类模版 //template<class T> //bool Less(const T& left, const T& right) // C++里面传参如果不改变,就把const加上,传值传参代价太大 //{ // // 这里的const相当于修饰引用本身 // // 引用修饰T,下面T实例化成Date* // // 特化的版本整个的类型结构得跟原版本保持一致,所以人家的引用用const修饰,你也得用const修饰 // // 这就是报错报的“不是函数的专用化” // return left < right; //} //template<> //bool Less<Date*>(Date* left, Date* right) //{ // return *left < *right; //} //const T* p1 // const修饰*p1(指向的内容) //T* const p2 // const修饰引用本身 // 对上述函数模版实现一个特化版本 // 特化:针对某些类型进行特殊化处理 //template<> //bool Less<Date*>(const Date*& left, const Date*& right) // 直接把T替换成Date*,错了,编译不过 //{ // // 这里类型是const Date*,const在左边,修饰的是Date*,形参对不上 —— 参数类型不匹配,类型匹配不上 // // const Date*& left,这里相当于&left,指针的引用,相当于特化成指针的引用 // // 指针特化就会有这样的坑 —— 问题出在这里的const没有修饰到引用,因为这里跟指针连在一起 // // “不是函数模版的专用化” // return *left < *right; //} //template<class T> //bool Less(const T& left, const T& right) //{ // return left < right; //} ////const T* p1 // 保护指向的内容 ////T* const p2 // 保护指针本身(少用,所以非常容易出错) // //// 真正的麻烦的地方不在特化,而在于这个规则 //// 核心:特化版本形参结构跟原模板必须保持一致,比如说原模板是const的形参,特化版本也必须是 //// 对上述函数模版实现一个特化版本 //// 特化:针对某些类型进行特殊化处理 //template<> //bool Less<Date*>(Date* const& left, Date* const& right) // 修饰引用本身 //{ // // 只要是函数模版,对指针特化就会有这样的坑 —— 问题出在这里的const没有修饰到引用,因为这里跟指针连在一起 // // “不是函数的专用化” // // 特化:函数模板最大的坑 // return *left < *right; //} // //int main() //{ // cout << Less(1, 2) << endl; // 可以比较,结果正确 —— 用指针指向的内容比较,有意义 // // Date* p1 = new Date(2025, 1, 1); // Date* p2 = new Date(2025, 1, 3); // cout << Less(p1, p2) << endl; // 可以比较,结果错误 —— 用指针进行比较,结果随机,比较没意义 // // return 0; //} //// -----------------函数特化不如去写普通函数,但是特化版本怎么写要知道---------------------- // //// 参数匹配时,函数调用有个原则——“有现成的能吃的就吃现成的,有更好吃的就吃更好吃的,没有更好吃的凑活一下也能吃” //// 参数匹配原则核心:“有现成的能吃的就吃现成的”,不会再吃半成品(模版) //// “自热米饭、方便面”:处理一下才能吃 //template<class T> //bool Less(const T& left, const T& right) //{ // return left < right; //} // //// 还有个方式,可以不计较刚才的问题 //// “外卖”:马上就能吃 //bool Less(Date* left, Date* right) // 这里还是得写成Date* //{ // return *left < *right; //} // ////bool Less(const Date* left, const Date* right) // 权限的缩小,但它会去匹配原模版 ////{ //// return *left < *right; ////} // //// -----------函数模版和普通函数是可以同时存在的------------- //// 编译器的原则:普通函数是做好的,函数模板是半成品 // //int main() //{ // cout << Less(1, 2) << endl; // 可以比较,结果正确 —— 用指针指向的内容比较,有意义 // // // 存在类型转换:Date*转换成const Date*,权限的缩小,但它会去匹配原模版 // Date* p1 = new Date(2025, 1, 1); // Date* p2 = new Date(2025, 1, 3); // cout << Less(p1, p2) << endl; // 可以比较,结果错误 —— 用指针进行比较,结果随机,比较没意义 // // return 0; //} // =======================类模版的特化======================= // 函数模板是通过参数推演的,类模版就没有这么麻烦 template<class T1,class T2> class Data { public: Data() { cout << "Data<T1,T2>" << endl; } // 原模版 private: T1 _d1; T2 _d2; }; //// 特化版本相当于定义一个全新的类 //// 类模板特化,并不要求成员类型一致(即对内部成员没有要求)—— //// 也就是说原模版定义的,特化版本可以不定义,也可以新增 //template<> //class Data <int, double> // 之前是写在函数名后面,现在是写在类名后面 //{ //public: // Data() { cout << "Data<int,double> 特化" << endl; } // 构造这里再改一改 // void func() {}; //}; //// 优先会走特化版本,因为特化版本可以被认为是处理好了,不需要实例化 //// 模版的特点就是让编译器帮我们写代码 // //// ----------------------vector<bool>--------------------- //// specialization:特化,专业化 //// vector<bool>:特化 —— 核心点:优化空间,这是个经典的特化的例子 //// —— 用bool值的时候还用一个真正的bool值去存储就太浪费了,所以用一个比特位去存储 //// “在不在”改成位(比特位)去标记,目的是space——节省空间 //// 学了哈希表那一个章节的位图才能理解位存储的一些玩法,比如这里的用一个位来存储 //// filp:开关 / 翻转,把值都翻一遍 // ==================全特化 / 半特化============================= // 所有模版参数全部特化就叫全特化,部分模板参数特化就叫半特化或者偏特化 //// 全特化 //template<> //class Data <int, double> //{ //public: // Data() { cout << "Data<int,double> 特化" << endl; } // void func() {}; //}; //// 半特化/偏特化 //template<class T1> //class Data <T1, char> //{ //public: // Data() { cout << "Data<T1,char> 特化" << endl; } // void func() {}; //}; // 都有double,匹配实例化参数更多的那个,所以这里匹配的是全特化 // 如果两个模板参数都是double,即<double,double>,匹配半特化——选择最接近于成品的 // 全特化 template<> class Data <int, double> { public: Data() { cout << "Data<int,double> 特化" << endl; } void func() {}; }; // 半特化/偏特化 template<class T1> class Data <T1, double> { public: Data() { cout << "Data<T1,double> 特化" << endl; } void func() {}; }; // --------------------半特化的另一种用法,对参数类型进行限制------------------------- // 限制实例化参数是指针 —— 什么类型的指针都可以,只要是指针都行,具体什么类型不重要 template<class T1,class T2> class Data <T1*, T2*> { public: Data() { cout << "Data<T1*, T2*> 特化" << endl; } // 最NB在这里,用法很灵活 // 相当于是指针的时候可以匹配,但是还是可以拿到原类型 void func() { // typeid可以用来打类型 cout << typeid(T1).name() << endl; cout << typeid(T2).name() << endl; // 打印的还是原本的类型 } }; // 除了是指针,还可以是引用 —— 模板参数还可以传引用 template<class T1, class T2> class Data <T1&, T2&> { public: Data() { cout << "Data<T1&, T2&> 特化" << endl; } void func() { // typeid可以用来打类型 cout << typeid(T1).name() << endl; cout << typeid(T2).name() << endl; // 打印的还是原本的类型 } }; // 也可以混在一起,一个传指针,一个传引用 template<class T1, class T2> class Data <T1*, T2&> { public: Data() { cout << "Data<T1*, T2&> 特化" << endl; } void func() { // typeid可以用来打类型 cout << typeid(T1).name() << endl; cout << typeid(T2).name() << endl; // 打印的还是原本的类型 } }; // 甚至于说第一个是指针,第二是整型int template<class T1> class Data <T1*, int> { public: Data() { cout << "Data<T1*, int> 特化" << endl; } void func() { // typeid可以用来打类型 cout << typeid(T1).name() << endl; // 打印的还是原本的类型 } }; //int main() //{ // // 类模版更加清晰,直接是调的原模版 // Data<int, int> d1; // //d1.func(); // 红色波浪线,d1没有func // // Data<int, double> d2; // d2.func(); // // //Data<int, char> d3; // 匹配到了半特化 // //d3.func(); // // // 匹配结果:Data<T1,double> 特化 // // //Data<int, double> d3; // 优先匹配全特化 // //d3.func(); // //// 匹配结果:Data<int,double> 特化 // // Data<double, double> d3; // 优先匹配全特化 // d3.func(); // // 匹配结果:Data<T1,double> 特化 // // Data<char*, double*> d4; // d4.func(); // 打印的还是原本的类型 // // Data<char&, double&> d5; // d5.func(); // // Data<char*, int> d6; // d6.func(); // 打印的还是原本的类型 // // return 0; //} // -------------------有了特化的用法,就可以把前面的用法改造一下---------------------- // 这里改造一下前面的优先级队列 namespace jqj { //// 特化版本 //template <Date*> //struct less //{ // // Date*传过去会有类型转换,Date* -> const Date*是一个权限的缩小,可以传过去,但是会产生一个新的问题 // // 这个新的问题就是会产生临时变量,而临时变量具有常性,而下面的const Date*& x只是个普通的引用 // // 临时变量 -> (普通引用)传不过去 // bool operator() (const Date*& x, const Date*& y) const // 左边的const修饰的是指针 // { // return x < y; // } //}; //template <Date*> //struct Greater //{ // bool operator() (const Date*& x, const Date*& y) const { return x < y; } //}; // --------------------特化版本---------------------- //// 解决方法1(类模板:只要能传过来就行了) //template<> //struct Less<Date*> //{ // bool operator() (const Date* const& x, const Date* const& y) const // 变成常引用 // { // return *x < *y; // } //}; //template<> //struct Greater<Date*> //{ // bool operator() (const Date* const& x, const Date* const& y) const // 变成常引用 // { // return *x > *y; // } //}; //// 解决方法2:可以不要&,只要能传参传过来就行了 //template<> //struct Less<Date*> //{ // bool operator() (const Date* x, const Date* y) const // 变成常引用 // { // return *x < *y; // } //}; //template<> //struct Greater<Date*> //{ // bool operator() (const Date* x, const Date* y) const // 变成常引用 // { // return *x > *y; // } //}; // ---------------偏特化版本,所有的指针都按指针指向的内容去比较---------------- // 让所有的指针(这种做法不一定是你的需求,只有当你的需求真是这个样子的时候才这么做)都不按指针去比较了 // 都按指针指向的内容去比较 template<class T> struct Less<T*> { bool operator() (const T* x, const T* y) const // 变成常引用 { return *x < *y; } }; template<> struct Greater<Date*> { bool operator() (const Date* x, const Date* y) const // 变成常引用 { return *x > *y; } }; } int main() { //// 可以不传了 jqj::priority_queue<Date*> pq; //jqj::priority_queue<Date*,vector<Date*>,jqj::Greater<Date*>> pq; pq.push(new Date(2025, 10, 18)); pq.push(new Date(2025, 10, 19)); pq.push(new Date(2025, 10, 17)); while (!pq.empty()) { cout << *pq.top() << " "; pq.pop(); } cout << endl; return 0; } // ===========模版的分离编译见另外一个项目============

二、模版分离完整代码演示

 a.h:

#pragma once template<class T> T TAdd(const T& left, const T& right) { return left + right; } int Add(int x, int y);

a.cpp:

#define _CRT_SECURE_NO_WARNINGS 1 #include"a.h" //// 预处理:展开头文件 //template<class T> //T TAdd(const T& left, const T& right) //{ // return left + right; //} int Add(int x, int y) { return x + y; } //// 不用< > //template //double TAdd(const double& left, const double& right); template int TAdd(const int& left, const int& right);

main.cpp:

#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; #include"a.h" int main() { cout << Add(1, 2) << endl; cout << TAdd(1.1, 2.2) << endl; cout << TAdd(1, 2) << endl; return 0; }

结尾

往期回顾:

【C++STL :stack && queue (三) 】优先级队列的使用以及底层实现

 结语:都看到这里啦!那请大佬不要忘记给博主来个“一键四连”哦! 

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡

૮₍ ˶ ˊ ᴥ ˋ˶₎ა


Read more

Git国内极速下载与安装全攻略:无需翻墙的完整解决方案

Git国内极速下载与安装全攻略:无需翻墙的完整解决方案

在国内使用Git时,由于网络限制,直接从官方源下载安装包或克隆仓库往往速度缓慢甚至失败。本文将提供一套完整的国内镜像解决方案,涵盖从Git软件安装到日常使用加速的全流程,帮助开发者无需翻墙即可高效完成Git相关操作。 一、国内镜像源安装Git 1.1 选择国内镜像源下载安装包 国内多所高校和企业提供了Git安装包的镜像服务,下载速度远超国际源: * 中科大镜像源 :https://mirrors.ustc.edu.cn/git/ * 清华大学镜像源 :https://mirrors.tuna.tsinghua.edu.cn/git/ * 阿里云镜像源 :https://registry.npmmirror.com/binary.html?path=git-for-windows/ * 码云(Gitee)镜像 :https://gitee.com/mirrors/git-for-windows 推荐优先使用阿里云或中科大镜像,更新频率高且下载稳定 1.2 各系统安装步骤

By Ne0inhk
保姆级教程:Windows Git 安装全流程,手把手带你从 0 到 1 (2025版)

保姆级教程:Windows Git 安装全流程,手把手带你从 0 到 1 (2025版)

Git 是程序员的必备工具。对于 Windows 用户来说,安装过程中的几十个英文选项往往让人头大。本教程将手把手带您走完安装流程,确保您的环境配置最优化、最符合现代开发标准。 第一步:下载安装包 1. 下载地址 * 官方网站:git-scm.com/download/win * 下载方式:推荐直接点击页面上的 "Click here to download" 或者 "Git for Windows/x64 Setup" 下载独立的 .exe 安装程序。 * 注:虽然可以用 Winget 命令行下载,但传统安装包更适合初次配置。 2. 版本选择 (x64 vs ARM64) * 绝大多数电脑(Intel/AMD

By Ne0inhk

上传本地文件(夹)代码到GitHub 超详细讲解最全命令集合(配图 适用全部)

下面我用最稳妥、最常用的方式,手把手教你把本地代码文件夹上传到 GitHub。不管你是第一次用 GitHub,还是之前总出错,按这个来基本不会翻车。 【注意】:https://github.com/beiyang366/LYVCSHOP  为作者的GitHub 仓库地址  一、准备工作(只需一次) 1️⃣ 注册 / 登录 GitHub 👉 https://github.com 登录即可(你应该已经有了) 2️⃣ 安装 Git(如果没装) 📥 下载地址(Windows / macOS / Linux): 👉 https://git-scm.com/ 安装完成后,打开 命令行 / Git Bash,输入: git --version 能看到版本号说明安装成功 ✅ 二、在

By Ne0inhk

DeepSeek-Coder-V2开源:128K上下文的AI编程利器

导语:深度求索(DeepSeek)正式开源新一代代码大模型DeepSeek-Coder-V2,以128K超长上下文、338种编程语言支持和比肩GPT-4 Turbo的性能,为开发者带来全新的AI编程体验。 【免费下载链接】DeepSeek-Coder-V2-Base开源代码智能利器DeepSeek-Coder-V2,性能比肩GPT4-Turbo,支持338种编程语言,128K代码上下文,助力编程如虎添翼。 项目地址: https://ai.gitcode.com/hf_mirrors/deepseek-ai/DeepSeek-Coder-V2-Base 行业现状:代码大模型进入"性能与开放"双轨竞争时代 随着生成式AI技术的快速发展,代码大模型已成为提升软件开发效率的关键工具。近年来,从GitHub Copilot到Cursor等基于大模型的编程辅助工具不断涌现,推动着开发范式的变革。据行业研究显示,配备AI辅助工具的开发者完成相同任务的效率平均提升30%以上,尤其在代码生成、调试和文档撰写方面表现突出。 当前市场呈现出"闭源商业模型"与"开源社区模型"并行发展的格局。闭源模

By Ne0inhk