先进网站建设有哪些,中国建设银行个人信息网站,模版网站搭建,水溶花边外发加工网文章目录 一、泛型编程二、函数模板2.1 函数模板的原理2.2 函数模板的实例化2.3 模板参数的匹配原则 三、类模板四、非类型模板参数五、模板的特化5.1 函数模板特化5.2 类模板特化 六、模板分离编译七、模板总结好书推荐#x1f381;彩蛋 一、泛型编程
#x1f4d6;实现一个… 文章目录 一、泛型编程二、函数模板2.1 函数模板的原理2.2 函数模板的实例化2.3 模板参数的匹配原则 三、类模板四、非类型模板参数五、模板的特化5.1 函数模板特化5.2 类模板特化 六、模板分离编译七、模板总结好书推荐彩蛋 一、泛型编程
实现一个通用的交换函数
void Swap(int left, int right)
{int temp left;left right;right temp;
}
void Swap(double left, double right)
{double temp left;left right;right temp;
}
void Swap(char left, char right)
{char temp left;left right;right temp;
}想要实现一个通用的交换函数不难借助函数重载就可以。函数重载小伙伴们还记得嘛忘了的小伙伴可以走传送门回去复习一下。如上面代码所示我们借助函数重载实现了三份Swap函数分别用来交换两个整型变量、两个双精度浮点型变量、两个字符型变量。
小Tips函数重载的通用性体现在函数调用的时候。当我们想交换两个变量的时候不管变量是什么类型都是直接使用Swap函数编译器会根据用户传递的实参数据类型自动去匹配调用对应的交换函数在用户看来仿佛就只有一份Swap函数实现了所有类型数据的交换。
函数重载的缺陷 通过上面的分析我们可以看出函数重载的最大缺陷就是当有一个新类型出现时需要自己增加对应的重载交换函数代码的复用率比较低。其次因为所有的重载函数都是我们自己写的过程繁琐并且难免会出现差错。
模板的引入 函数重载的主要问题就出在需要我们自己去写那能否告诉编译器一个模子让编译器代替我们根据不同的类型利用该模子来生成代码呢 如果在C中也能够存在这样一个模板通过给这个模板涂上不同的颜色类型来获得不同颜色的五角星生成具体的代码那将会方便不少我们的头发也能少掉一点。巧的是我们的先辈已经将树栽好我们只需在此乘凉。
什么是泛型编程 编写与类型无关的通用代码是代码复用的一种手段。模板是泛型编程的基础其中模板分为函数模板和类模板。 二、函数模板
概念 函数模板代表了一个函数家族该函数模板与类型无关在使用时被参数化根据实参类型产生函数的特定类型版本。
格式
template typename T1, typename T2,.......,typename Tn
返回值类型 函数名(参数列表){函数体}小Tipstypename是用来定义模板参数的关键字也可以用class替换。T1、T2…是模板参数表示类型。
//一个交换函数的函数模板
templatetypename T
void Swap( T left, T right)
{T temp left;left right;right temp;
}2.1 函数模板的原理 函数模板是一个蓝图它本身并不是函数是编译器用来产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的事情交给了编译器。
//一个交换函数模板
templatetypename T
void Swap(T left, T right)
{T temp left;left right;right temp;
}int main()
{int i1 10;int i2 20;Swap(i1, i2);double d1 1.1;double d2 2.2;Swap(d1, d2);char c1 a;char c2 b;Swap(c1, c2);
}在编译器编译阶段对于函数模板的使用编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。例如当用double类型的数据去调用Swap函数编译器通过对实参类型的推演将T确定为double类型然后生成一份专门处理double类型的代码对于int类型和char类型的数据也是这样处理的。所以在使用者看来无论什么类型都是调用Swap函数但本质上不同的类型会去调用不同的Swap函数。 小Tips这里为了给大家展示函数模板的原理专门写了一个交换函数Swap的模板实际中当大家需要交换两个数据的时候不需要自己写因为库中已经帮我们实现好了可以用库中的swaps小写实现对两个同类型数据的交换。
2.2 函数模板的实例化
用不同类型的参数使用函数模板时称为函数模板的实例化。模板参数实例化分为隐式实例化和显式实例化。
隐式实例化 隐式实例化就是让编译器根据实参自动推演模板参数的实际类型。
templateclass T
T Add(const T left, const T right)
{return left right;
}
int main()
{int a1 10, a2 20;double d1 10.0, d2 20.0;Add(a1, a2);Add(d1, d2);//Add(a1, d1);//该语句编译不通过Add(a, (int)d);//强制类型转换将两个参数设置成同类型return 0;
}注意Add(a1, d1)会导致编译失败因为在编译期间当编译器看到该函数调用的时候会去自动推演模板参数的类型首先通过实参a1将T推演为int通过实参d1将T推演为double但是模板参数列表中只有一个T编译器无法确定此处到底应该将T确定为int或者double类型而报错。此时这里有三种处理方法第一种方法在函数模板的参数列表中再增加一个模板参数第二种方法用户自己来强制类型转换对一个参数进行强制类型转换使得两个参数的类型相同第三种方法使用接下来介绍的显式实例化。
小Tips模板参数也可以作为函数模板的返回值类型。
显式实例化 显式实例化是在函数调用阶段在函数名后跟一个在里面指定模板参数的实际类型。
templateclass T
T Add(const T left, const T right)
{return left right;
}int main(void)
{int a 10;double b 20.0;// 显式实例化Addint(a, b);return 0;
}注意如果传递的实参和实例化出的函数形参类型不匹配编译器会尝试进行隐式类型转换如果无法转换成功编译器会报错。
显式实例化的实际使用场景
templatetypename T//模板参数T
T* Alloc(int n)//函数模板的形参没有使用模板参数
{return new T[n];
}int main()
{double* p Allocdouble(10);return 0;
}如上面的代码所示函数模板Alloc并没有模板参数T类型的形参因此编译器就无法去根据用户传递的实参类型推导出模板参数T的具体类型。因此当用户想要调用Alloc函数时必须进行显式实例化。从这里也可以看出编译器支持隐式实例化的前提是模板函数使用了模板参数类型的形参。
小Tips编译器不会根据函数的返回值去推导模板参数T的类型就像上面的Alloc函数模板虽然返回值的类型是模板参数T但是不管用编译器不会根据这里去推演T的实际类型也推演不出来。
2.3 模板参数的匹配原则
一个非模板函数可以和一个同名的函数模板同时存在而且该函数模板还可以被实例化为这个非模板函数。
// 专门处理int的加法函数
int Add(int left, int right)
{return left right;
}
// 通用加法函数
templateclass T
T Add(T left, T right)
{return left right;
}
void Test()
{Add(1, 2); // 与非模板函数匹配编译器不需要特化Addint(1, 2); // 调用编译器特化的Add版本
}对于非模板函数和同名的函数模板如果其他条件都相同在调用时会优先调用非模板函数而不会从函数模板产生一个实例。如果函数模板可以产生一个具有更好匹配的函数那么将选择函数模板。
// 专门处理int的加法函数
int Add(int left, int right)
{return left right;
}
// 通用加法函数
templateclass T1, class T2
T1 Add(T1 left, T2 right)
{return left right;
}
void Test()
{Add(1, 2); // 与非函数模板类型完全匹配不需要函数模板实例化Add(1, 2.0); // 模板函数可以生成更加匹配的版本编译器根据实参生成更加匹配的Add函数
}三、类模板
类模板的定义格式
templateclass T1, class T2, ..., class Tn//模板参数列表
class 类模板名
{// 类内成员定义
}; // 动态顺序表
// 注意Vector不是具体的类是编译器根据被实例化的类型生成具体类的模具
templateclass T
class Vector
{
public :Vector(size_t capacity 10): _pData(new T[capacity]), _size(0), _capacity(capacity){}// 使用析构函数演示在类中声明在类外定义。
~Vector();void PushBack(const T data);
void PopBack();
// ...size_t Size() {return _size;}private:T* _pData;size_t _size;size_t _capacity;
};
// 注意类模板中函数放在类外进行定义时需要加模板参数列表
template class T
VectorT::~Vector()
{if(_pData)delete[] _pData;_size _capacity 0;
}注意Vector不是具体的类是编译器根据被实例化的类型生成具体类的模具。其次类模板中的成员函数放在类外面进行定义时需要加模板参数列表因为此时单独的Vector已经不再表示类型了编译器可能会根据Vector这个模板同时实例化出多个类此时VectorT表示一个具体的类型。建议类模板中的成员函数声明和定义不要分离到两个文件中。
类模板的实例化 类模板的实例化与函数模板的实例化不同类模板的实例化需要在类模板的名字后面跟然后将实例化的类型放在中即可类模板的名字不是真正的类而实例化的结果才是真正的类。
// Vector类名Vectorint才是类型
Vectorint s1;
Vectordouble s2;类模板的优势 类模板的优势和函数模板一样把本来需要我们自己干的事情交给了编译器去干。以上面的动态顺序表为例假如没有类模板我需要在一个程序中同时定义一个存储int型数据的顺序表和存储double型数据的顺序表因为没有类模板的话动态顺序表的成员变量_pData的类型就只能时固定的int或double此时我们就只能写两个动态顺序表的类将其中一个类的成员变量_pData设置成int类型用来存储int型数据将另一个类的成员变量_pData设置成double类型用来存储double型数据。当程序中还需要一个存储其他类型数据的顺序表时我们还得自己再增加类这工作量可见一斑长时间下去不管你能不能忍受你的头发必定受不了。而类模板的出现就极大的缓解了我们头发的压力我们只需要写一份动态顺序表的模板代码出来当要用动态顺序表存储某类型的数据时我们只需要把该类型告诉编译器让编译器根据我们写的模板去实例化一个对应的动态顺序表类出来存储该类型的数据。
四、非类型模板参数
前文提到的模板参数准确的说应该叫做模板的类型形参出现在模板参数列表中跟在class、typename之后用来表示一种类型除此之外模板还有另一种参数非类型形参就是用一个常量作为类函数模板的一个参数在类函数模板中可将该参数当成常量来使用。
非类型模板参数的使用场景
//静态栈
//#define M 100
templateclass T, size_t N 10
class Stack
{
public://一些成员函数
private:T _arr[N];//T _arr[M];int _top;
};
int main()
{Stackint, 10 s1;//实例化一个栈可以存储10个整型数据Stackdouble, 100 s2;//实例化出一个栈可以存储100个double型数据return 0;
}假设我们这里要实现一个静态的栈即存储的数据量一经确认是无法扩容的所以成员变量我们声明了一个数组_arr在没有非类型模板参数的时候设计静态的栈一般是通过#define来定义一个标识符常量我们给这个常量一个初始值就像上面代码中的M最终设计出来的栈就能存储M个数据但是一个程序中M只能被定义一次当我们需要存储的多组数据并且每组数据的数据量大不相同就像上面的s1需要存储10个int型数据s2需要存储100个double型的数据为了满足两者的需求此时的M就只能大于100这样对s1来说就会造成极大的空间浪费。
非类型模板参数的引入就很好的解决了这个问题我们可以根据实际的需求去传递参数实例化出最符合自己需求的栈。
注意事项
非类型模板参数一定是一个常量在类中不能对其进行修改。非类型模板参数的类型必须是整型即int、size_t、char等。浮点型、类类型等是不允许作为非类型模板参数的。非类型模板参数必须在在编译阶段就能确认结果。
五、模板的特化
概念 在原模板的基础上针对特殊类型惊醒特殊化的实现方式。模板特化分为函数模板特化和类模板特化。
为什么要有模板特化 通常情况下使用模板可以实现一些与类型无关的代码但是对于一些特殊的类型可能会得到一些错误的结果此时就需要进行特殊处理。比如下面这个专门用来进行小于比较的函数模板
templateclass T
bool Less(T left, T right)
{return left right;
}
int main()
{int a 10;int b 20;cout Less(a, b) endl; // 可以比较结果正确int* pa a;int* pb b;//希望通过传递ab的地址去比较ab的大小cout Less(pa, pb) endl; // 可以比较结果错误return 0;
}第一次我们希望直接去比较a和b的大小于是就直接传递了a和b此时的模板参数T被隐式实例化为intLess函数中就进行的是两个整数的比较比较的结果符合我们的预期。第二次我们希望通过a和b的地址去比较它们两个的大小于是传递了a和b的地址此时的模板参数T被实例化为int*因此Less函数就是进行两个地址的比较并没有按照我们期望的那样去比较两个地址中存储的数据大小因此得到的结果也和我们预期的有所不同此时我们就要对Less函数模板进行特化让它能够满足我们的要求。
5.1 函数模板特化
注意事项
必须要先有一个基础的函数模板。关键字template后面接一对空的尖括号。函数名后跟一对尖括号尖括号中指定需要特化的类型。函数形参列表必须要和函数模板的基础参数类型完全相同如果不同编译器可能会报一些奇怪的错误。
//函数模板
templateclass T
bool Less(T left, T right)
{return left right;
}
//对Less函数模板进行特化
template
bool Lessint*(int* left, int* right)
{return *left *right;
}int main()
{int a 10;int b 20;cout Less(a, b) endl; // 可以比较结果正确int* pa a;int* pb b;//希望通过传递ab的地址去比较ab的大小cout Less(pa, pb) endl; // 调用特化之后的版本不走函数模板return 0;
}bool Lessint*(int* left, int* right)就是对Less函数模板的一个特化当用户调用Less传递的是int*类型的参数时会去调用特化后的版本而不走函数模板。
小Tips根据2.3小节的内容一般情况下如果函数模板遇到不能处理或者处理有误的类型为了实现简单通常都是将该函数直接给出像下面这样。
bool Less(int* left, int* right)
{return *left *right;
}这种实现简单明了代码的可读性高容易书写。
5.2 类模板特化
全特化 全特化就是将模板参数列表中所有的参数都确定化。
//类模板
templateclass T1, class T2
class Data
{
public:Data() { cout DataT1, T2 endl; }
private:T1 _d1;T2 _d2;
};//对Data类模板进行特化
template
class Dataint, char
{
public:Data() { cout Dataint, char endl; }
private:int _d1;char _d2;
};void TestVector()
{Dataint, int d1;//使用类模板Dataint, char d2;//使用全特化
}偏特化 任何针对模板参数进行进一步条件限制设计出来的特化版本叫做偏特化。偏特化有以下两种表现方式
部分特化
将类模板参数列表中的一部分参数进行特化。
//原类模板
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() {coutDataT1, int endl;}
private:T1 _d1;int _d2;
};void TestVector()
{Dataint, int d1;//使用部分特化Datadouble, int d2;//使用部分特化Datachar, int d3;//使用部分特化
}以上面为例将原类模板的第一个参数仍旧使用模板T1第二个模板参数特化成int型此后只要第二个参数传的是int就会走特化后的类。
参数更进一步限制
//两个参数偏特化为指针类型
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;
};void test2()
{Datadouble, int d1; // 调用特化的int版本Dataint, double d2; // 调用基础的模板 Dataint*, int* d3; // 调用特化的指针版本Datadouble*, double* d4; // 调用特化的指针版本Dataint, int d5(1, 2); //调用特化的引用版本Datachar, char d6(a, b); //调用特化的引用版本
}六、模板分离编译
什么是分离编译 一个程序项目由若干个源文件共同实现而每个源文件单独编译生成目标文件最后将所有的目标文件链接起来形成一个可执行文件的过程就叫做分离编译模式。
模板的分离编译 将模板的声明与定义分离在头文件中进行声明源文件中完成定义。
// a.h
templateclass T
T Add(const T left, const T right);//a.cpp
#include a.htemplateclass T
T Add(const T left, const T right)
{return left right;
}//text.cpp
#include a.hint main()
{Add(1, 2);Add(1.1, 2.2);return 0;
}通过结果可以看出上面的代码产生了error LNK2019链接错误。为什么会这样呢下面我们来分析一下原因。首先我们需要明确头文件是不会参与编译的只有以.cpp结尾的文件才能被编译在预处理阶段会把头文件的内容拷贝到包含了该头文件的.cpp文件中其次编译器对工程中的多个源文件是分离开单独编译编译阶段干的主要工作是按照语言的特性进行词法、语法、语义分析检查无误后生成汇编代码最终的到一个.obj结尾的目标文件。
先看没有使用模板的分离编译
// a.hint Add(int left, int right);//a.cpp
#include a.hint Add(int left, int right)
{return left right
}//text.cpp
#include a.hint main()
{Add(1, 2);return 0;
}以上面的代码为例在编译text.cpp时编译器不知道Add函数的实现因为包含的a.h头文件中只有关于Add函数的一个声明所以当编译器碰到对Add函数的调用时只是给出一个指示指示链接器应该为它寻找Add函数的实现体。这也就是说test.obj中没有关于Add函数的任何一行二进制代码。
在编译a.cpp的时候编译器找到了Add函数的实现。于是Add的实现也就是对应的二进制代码出现在a.obj里。
链接时链接器在a.obj中找到Add函数的实现代码二进制地址通过符号表导出。然后将test.obj中悬而未定的call XXX地址改成Add函数的实际地址。
模板需要实例化 然而对于模板来说模板函数的代码其实并不能直接编译生成二进制代码其中要有一个“实例化”的过程以下面的代码为例
//test.cpp
templatetypename T
void Swap(T left, T right)
{T temp left;left right;right temp;
}int main()
{int i1 10;int i2 20;Swap(i1, i2);double d1 1.1;double d2 2.2;Swap(d1, d2);
}如果在test.cpp文件中没有调用Swap函数Swap就得不到实例化从而test.obj中也就没有关于Swap的任何一行二进制代码。如果像上面代码中那样调用了Swap(i1, i2);和Swap(d1, d2);此时test.obj中就有了Swapint和Swapdouble两个函数的二进制代码段。
再来分析模板分离编译
//a.h
templateclass T
class A
{
public:void f();//这里只是声明
};//a.cpp
templateclass T
void AT::f()//模板的实现
{//...实现
}//test.cpp
int main()
{Aint a;a.f();return 0;
}编译器在执行到a.f()的时候并不知道Aint::f的定义因为它不在a.h里面于是编译器只好寄希望于链接器希望它能够在其他的.obj文件里面找到Aint::f的实例在本例中就是a.obj中然而a.obj中是没有Aint::f的二进制代码。因为C标准明确规定当一个模板不被用到的时候他就不该被实例化出来a.cpp中并有用到Aint::f所以实际上a.cpp编译出来的a.obj文件中关于A::f一行二进制代码也没有因为没有进行任何的实例化于是链接器就傻眼了只好给出一个链接错误。
//a.cpp
//显式实例化
template
class Aint;如果在a.cpp中加上上面这段代码a.f();就可以成功执行啦上面这段代码会将模板专用化于是a.obj的符号导出表中就有了Aint::f这个符号的地址于是链接器就能够完成任务。但是如果在test.cpp文件中再定义一个Adouble b;我们就需要在a.cpp中再加入相应的显式实例化模板的分离编译显然是麻烦的。
建议 将声明和定义放到一个文件xxx.hpp里面或者xxx.h其实也是可以的。在同一个文件中对模板的声明和定义进行分离是不会出现链接错误的。
注模板分离编译这块参考了刘未鹏pongba大佬的文章下面附上原文链接传送门感兴趣的小伙伴可以点进去看看。
七、模板总结
优点
模板复用了代码将本来需要人去完成的工作交给编译器去做使人们可以更快的迭代开发C的标准模板库STL因此而产生。增强了代码的灵活性。
缺点
模板会导致代码膨胀问题也会导致编译时间变长。出现模板编译错误时错误信息非常凌乱不易定位错误。
结语本篇文章只是基于语法层面对模板做了简单介绍关于模板我们需要通过大量的实践才能体会到模板的魅力以及它的语法细节。因此在后面的文章中我会通过模拟实现string和STL中的经典容器来帮助大家更好的理解模板感兴趣的小伙伴可以点下关注更新时会第一时间告知。 好书推荐
在这里推荐两本我个人最近正在读的书供暑假有意提升自己能力的小伙伴参考 近期有购书需求的小伙伴可以直接点击下方书名前往选购
NO.1《我看见了风暴人工智能基建革命》 本书深入讲解了阿里、微软等业界巨头在人工智能技术领域的迭代历程从框架设计、平台开发以及云基础设施等三个关键领域对AI的发展历史进行详尽而深入的剖析揭示对未来更远视野的洞察。
NO.2《趣话计算机底层技术》 本书的内容设计独特通过富有吸引力的故事深入浅出地解读了计算机中的CPU、存储、I/O、操作系统、系统编程以及安全六大主题。每一章都深入剖析了计算机的核心概念和关键技术让读者在轻松的阅读时能够迅速提升自身计算机认知水平。
彩蛋