做会计应关注什么网站,wordpress注册邮箱必选,大连网站模板建站,购买服务器#x1f3d6;️作者#xff1a;malloc不出对象 ⛺专栏#xff1a;C的学习之路 #x1f466;个人简介#xff1a;一名双非本科院校大二在读的科班编程菜鸟#xff0c;努力编程只为赶上各位大佬的步伐#x1f648;#x1f648; 目录 前言一、非类型模板参数二、模板的特… ️作者malloc不出对象 ⛺专栏C的学习之路 个人简介一名双非本科院校大二在读的科班编程菜鸟努力编程只为赶上各位大佬的步伐 目录 前言一、非类型模板参数二、模板的特化2.1 函数模板特化2.2 类模板的特化2.2.1 类模板的全特化2.2.2 类模板的偏特化 三、模板分离编译3.1、为什么模板不支持分离编译 四、模板总结 前言
本篇文章我们需要进一步了解模板的使用以及讲解为什么使用模板不能声明与定义分离的原因。
一、非类型模板参数
模板参数分为类型形参与非类型形参。 类型形参出现在模板参数列表中跟在class或者typename之类的参数类型名称。 非类型形参就是用一个常量作为类(函数)模板的一个参数在类(函数)模板中可将该参数当成常量来使用。 下面我们先来看看下面这个例子
#include iostream
using namespace std;#define N 10
templateclass T
class Array
{
public:private:T _a[N];
};int main()
{Arrayint a1;Arraydouble a2;return 0;
}使用#define宏替换我们可以创建一个长度为10任意类型的静态数组但是如果我想让a1数组创建一个长度为10的静态数组而a2为一个长度为100的静态数组使用#define是完不成任务的。这时候就需要使用我们的非类型模板参数了我们可以显式的去实例化任意长度的静态数组下面我们一起来看看非类型模板参数的使用
#include iostream
using namespace std;templateclass T, size_t N // 非类型模板参数
class Array
{
public:private:T _a[N];
};int main()
{Arrayint, 10 a1;Arraydouble, 20 a2;return 0;
}下面我们继续来探究一下非类型模板参数的性质
#includeiostream
using namespace std;templateclass T, size_t N 10
void func(const T a)
{int arr[N] { 0 };for (size_t i 0; i N; i){arr[i] a;cout arr[i] ;}cout endl;
}int main()
{func(1);return 0;
}非类型模板参数除了可以用于类模板当然也是可以用于函数模板的。
第一个性质非类型参数的值是一个整型常量它是一个固定大小的不能被修改的。 第二个性质非类型模板参数的类型一定是整型的这是语法规定。 注整型可以是整型家族的任意一种(long long、char、long、short、bool…)。 讲到非类型模板参数这里我们顺便提及C11中提出的一个容器array它采用的就是非类型模板参数。 我们可以看到它的功能与vector很多都是类似的也就是array能做到的vector也是一定可以做到的那么C11为何要提出这个容器呢它有什么优势
它主要其实不是与vector做对比它对比的是静态数组它在数组越界上是全面进行检查的而静态数组是抽查的在有些情况下是检查不到越界问题的下面我们就来一起看看这个问题 我们可以看到静态数组越界访问进行修改竟然也不会报错这就很离谱了下面我们来看看使用模板类array的好处 一旦数组越界访问了我们立即能够检查出来assert断言使程序崩溃这是由于重载了运算符 []在函数内部使用assert检查数组是否越界这就是相较于静态数组的优点所在但是我们既然有了vector我们其实没有理由去使用arrayvector能实现一切array能实现的功能所以array其实是没有很大意义的我们也很少去使用它
二、模板的特化 概念通常情况下使用模板可以实现一些与类型无关的代码但对于一些特殊类型的可能会得到一些错误的结果此时就需要特殊处理。 模板特化分为函数模板特化与类模板特化。
2.1 函数模板特化 函数模板的特化步骤 1.必须要先有一个基础的函数模板!! 2. 关键字template后面接一对空的尖括号 3. 函数名后跟一对尖括号尖括号中指定需要特化的类型 4. 函数形参表: 必须要和模板函数的基础参数类型完全相同如果不同编译器可能会报一些奇怪的错误。 下面我们来看一个简单的例子
#include iostream
using namespace std;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){_cout d._year - d._month - d._day;return _cout;}
private:int _year;int _month;int _day;
};templateclass T // 基础函数模板
bool Less(T left, T 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;
}别看着有这么多代码其实我们这里主要的目的就是使用函数模板来比较日期的大小罢了。我们来看看结果 可以看到Less绝对多数情况下都可以正常比较但是在特殊场景下就得到错误的结果。上述示例三中p1指向的d1显然小于p2指向的d2对象但是Less内部并没有比较p1和p2指向的对象内容而比较的是p1和p2指针的地址这就无法达到预期而错误。
此时就需要对模板进行特化。即在原模板类的基础上 针对特殊类型所进行特殊化的实现方式。 当然了这里的处理方式肯定是不局限于模板特化这一种方式的这里我们只是特定的针对模板这一块的问题提出解决方法。实际上函数模板特化一般情况下我们其实是不推荐使用的遇到不能处理或者处理有误的类型我们都是将该函数直接给出。
如上述问题我们直接在类外定义一个函数就完事了
bool Less(Date* left, Date* right)
{return *left *right;
}该种实现简单明了代码的可读性高容易书写因为对于一些参数类型复杂的函数模板特化时特别给出因此函数模板不建议特化而类模板的意义更大。 总结一般情况下如果函数模板遇到不能处理或者处理有误的类型为了实现简单通常都是将该函数直接给出。 2.2 类模板的特化
我们先来看一个例子
namespace curry
{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){_cout d._year - d._month - d._day;return _cout;}private:int _year;int _month;int _day;};// 利用仿函数变小堆templateclass Tstruct less{// 仿函数bool operator()(const T x, const T y){return x y;}};templateclass Tstruct greater{bool operator()(const T x, const T y){return x y;}};// 特化类模板templatestruct lessDate* {bool operator()(const Date* x, const Date* y){return *x *y;}};templatestruct greaterDate*{bool operator()(const Date* x, const Date* y){return *x *y;}};templateclass T, class Container vectorT, class Compare lessTclass priority_queue // 优先级队列{public:// 建大堆// 向上调整建堆void ajust_up(int child){Compare com; // com对象去调用仿函数int parent (child - 1) / 2;while (child 0){//if (_con[parent] _con[child]) // 大堆升序if (com(_con[parent], _con[child])) // 比小的{swap(_con[parent], _con[child]);child parent;parent (child - 1) / 2;}else{break;}}}// 向下调整建堆void ajust_down(int parent){size_t child 2 * parent 1;while (child _con.size()){Compare com;//if (child 1 _con.size() _con[child] _con[child 1])if (child 1 _con.size() com(_con[child], _con[child 1])){child;}//if (_con[parent] _con[child])if (com(_con[parent], _con[child])){swap(_con[parent], _con[child]);parent child;child 2 * parent 1;}else{break;}}}void push(const T x){_con.push_back(x); // 插入元素向上调整建堆ajust_up(_con.size() - 1);}void pop(){swap(_con[0], _con[_con.size() - 1]); // 先交换栈顶元素删除最后一个元素再向下调整建堆_con.pop_back();ajust_down(0); // 向下调整堆从根节点开始}const T top(){return _con[0];}size_t size(){return _con.size();}bool empty(){return _con.empty();}private:Container _con;};void test(){priority_queueDate, vectorDate q1; // 大堆q1.push(Date(2018, 10, 29));q1.push(Date(2018, 10, 28));q1.push(Date(2018, 10, 30));cout q1.top() endl;priority_queueDate, vectorDate, greaterDate q2;q2.push(Date(2018, 10, 29));q2.push(Date(2018, 10, 28));q2.push(Date(2018, 10, 30));cout q2.top() endl;cout ------------------------ endl;priority_queueDate*, vectorDate* q3; // 大堆q3.push(new Date(2018, 10, 29));q3.push(new Date(2018, 10, 28));q3.push(new Date(2018, 10, 30));cout *(q3.top()) endl;priority_queueDate*, vectorDate*, greaterDate* q4; // 小堆q4.push(new Date(2018, 10, 29));q4.push(new Date(2018, 10, 28));q4.push(new Date(2018, 10, 30));cout *(q4.top()) endl;}
}2.2.1 类模板的全特化
全特化即是将模板参数列表中所有的参数都确定化上述例子我们讲的就是全特化下面我们继续来看看例子
#include iostream
using namespace std;templateclass T1, class T2 // 基础类模板
class Data
{
public:Data() { cout DataT1, T2 endl; }
private:T1 _d1;T2 _d2;
};template // 全特化
class Dataint, char
{
public:Data() { cout Dataint, char endl; }
private:int _d1;char _d2;
};int main()
{Dataint, char d2;Dataint, int d1;Datadouble, int d4;Datashort, int d5;Datadouble, double d6;return 0;
}从上图我们发现类模板全特化只能显式实例化出一种参数类型而原类模板可以实例化所有参数类型。
2.2.2 类模板的偏特化
偏特化任何针对模版参数进一步进行条件限制设计的特化版本。
偏特化有以下两种表现方式
部分特化
部分特化 将模板参数类表中的一部分参数特化。
#include iostream
using namespace std;templateclass T1, class T2
class Data
{
public:Data() { cout DataT1, T2 endl; }
private:T1 _d1;T2 _d2;
};// 全特化
template
class Dataint, char
{
public:Data() { cout Dataint, char endl; }
private:int _d1;char _d2;
};// 将第二个参数特化为int
template class T1
class DataT1, int
{
public:Data() { cout DataT1, int endl; }
private:T1 _d1;int _d2;
};int main()
{Dataint, char d2;Dataint, int d1;Dataint*, int d3;Datadouble, int d4;Datashort, int d5;Datadouble, double d6;return 0;
}参数更进一步的限制
偏特化并不仅仅是指特化部分参数而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
我们来看看下面这个例子
#include iostream
using namespace std;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){_cout d._year - d._month - d._day;return _cout;}
private:int _year;int _month;int _day;
};// 原类模板
templateclass T
struct Less
{bool operator()(T x, T y){return x y;}
};// 类模板全特化只能特化出一份具体指针类型
template
struct LessDate*
{bool operator()(Date* x, Date* y){return *x *y;}
};// 偏特化 -- 进一步的限制针对指针这个泛类
templateclass T
struct LessT*
{bool operator()(T* x, T* y){return *x *y; }
};int main()
{Date d1(2023, 3, 26);Date d2(2023, 3, 27);cout LessDate()(d1, d2) endl;Date* p1 d1;Date* p2 d2;cout LessDate*()(p1, p2) endl;int* p3 new int(1);int* p4 new int(2);cout Lessint*()(p3, p4) endl;return 0;
}对于上述全特化类模板来说它只能实例化出Date*这一种类模板如果我们需要其他指针类型就得全特化多份类模板了因此它是极其不方便的。偏特化类模板就解决了这一问题它针对的是所有指针类型的日期类对象的比较所以说偏特化还是非常具有实际意义的。
下面我们再继续简单的看几个偏特化类模板的例子
templateclass T1, class T2
class Data
{
public:Data() { cout DataT1, T2 endl; }
private:T1 _d1;T2 _d2;
};//两个参数全特化为int*, char*类型
template
class Dataint*, char*
{
public:Data() { cout Dataint, char endl; }
private:int _d1;char _d2;
};//两个参数偏特化为指针类型
templateclass T1, class T2
class Data T1*, T2*
{
public:Data() { cout DataT1*, T2* endl; }
private:T1 _d1;T2 _d2;
};//两个参数偏特化为引用类型
template class T1, class 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*, char* d1; // 调用全特化的int*, char*版本Dataint, double d2; // 调用基础的模板Dataint*, int* d3; // 调用偏特化的指针版本Dataint, int d4(1, 2); // 调用偏特化的引用版本return 0;
}关于基础、偏特化以及全特化类(函数)模板的调用顺序其实很简单有全特化 类(函数)模板就用全特化类(函数)模板它就相当于一份现成的代码针对的是个体没有对应的全特化类(函数)模板就使用偏特化类(函数)模板它就相当于一份半产品针对的是一类没有对应的全特化和偏特化类(函数)模板就使用基础类(函数)模板它针对的是所有类型。
我们可以用下图来表示它们之间的关系 三、模板分离编译
Q什么是分离编译 一个程序项目由若干个源文件共同实现而每个源文件单独编译生成目标文件最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。 3.1、为什么模板不支持分离编译
在探究这个问题之前首先我们来看看模板分离编译产生的现象
// test.h
templateclass T
T Add(const T left, const T right);void func();// test.cpp
#include test.htemplateclass T
T Add(const T left, const T right)
{return left right;
}void func()
{cout void func() endl;
}// test_03_25.cpp
#include iostream
using namespace std;
#include test.hint main()
{Add(1, 2);Add(1.1, 2.2);func();return 0;
}下面我们来分析一下在test_03_25.cpp这个源文件中为什么找不到模板函数Add的地址而找得到func函数的地址。
首先我们在C语言阶段就知道要生成可执行程序(.exe)需要经过预处理、编译、汇编、链接四个阶段那么对于多个源文件来说我们生成了几个可执行程序(.exe)和目标文件(.obj) 我首先给出结论多个源文件会生成多个目标文件(.obj)而经过链接过程之后只会生成一个可执行程序(.exe)详细过程可以看看我的这篇博客。 那么在链接之前每个源文件都要经过预处理、编译、汇编这三个阶段最后生成各自的目标文件(.obj)也就是说在链接之前每个源文件是独立进行的并未产生交互的行为。那么在test.cpp中对于Add函数模板来说它知道要实例化出什么类型的函数吗因此Add函数根本没有进行实例化没有实例化那么函数的地址自然是不知道的而func函数是定义了的所以test.obj中是没有Add函数地址但是有func函数的地址对于test_03_25.cpp来说它引用了test.h这个头文件它会将test.h中的内容展开到本源文件中在本源文件中我们声明了Add模板函数以及func函数那么它们其实会生成一个无效的地址加入到符号表中的这个仅仅是为了通过预处理、编译、汇编阶段而不会产生报错的行为实际上在最后链接的过程我们还会对符号进行重定位操作这个过程是选出有效的函数地址最后才能进行符号表合并。在本源文件中确实找不到Add函数的实际地址因此就发生了链接错误 那么我们找到了问题所在本质原因就是因为Add函数没有在test.cpp这个源文件中进行实例化那么我们就应该想办法让它实例化
解决方案一显式实例化 我们在test.cpp中显式声明了Add函数模板的类型那么Add函数模板自然就能实例化出对应的函数那么就有了相应的函数地址由此就解决了问题。但是这个解决方法是不常用、不好用的我们只能显式实例化一种类型如果我还想使用多份类型不一致的参数类型那我还是要显式声明多份函数这样也就失去了模板泛型编程的意义了那我们还不如直接定义多份函数呢所以这种解决方案我们是极其不推荐的。
第二种解决方案将函数模板/类模板声明与定义全部放在一个头文件中源文件直接引用这个头文件。
// test.h
#include iostream
using namespace std;templateclass T
T Add(const T left, const T right);void func();templateclass T
T Add(const T left, const T right)
{return left right;
}void func()
{cout void func() endl;
}// test.03_25.cpp
#include test.hint main()
{cout Add(1, 2) endl;cout Add(1.1, 2.2) endl;func();return 0;
}对于上述声明与定义全部放在头文件中有些读者可能会问有时候我们直接在类中定义函数不就好了吗为什么还要声明呢 在类和对象中我提到过这个问题成员函数如果在类中定义编译器可能会将其当成内联函数处理那么对于代码量多的成员函数当做内联函数展开的话那么是不是会造成代码膨胀啊所以我们一般都是代码量小的成员函数在类中直接定义而代码量大的成员函数先在类中声明在类外实现 四、模板总结 优点 1.模板复用了代码节省资源更快的迭代开发C的标准模板库(STL)因此而产生 2.增强了代码的灵活性 缺点 1.模板会导致代码膨胀问题也会导致编译时间变长 2.出现模板编译错误时错误信息非常凌乱不易定位错误(个人认为这个是现阶段对于我来说造成的最大问题模板导致代码膨胀是由于内联函数的展开而内联函数其实是一种理想的状态)。 关于模板其实还有挺多内容没讲到的现阶段我们只需要大概了解它的一些基本使用场景就可以了。最后本篇文章的讲解就到这里了如果有任何错处或者疑问欢迎大家评论区交流哦~~