和你有关,观后无感.................................................................................................................
目录
前言
一、【模板的引入和介绍】
1、泛型编程
2、【模板的介绍】
二、【 函数模板】
2.1【模函数板的介绍】
1.2【函数模板的原理】
1.3【函数模板的实例化】
1、隐式实例化
2、显示实例化
1.4【模板参数的匹配原则】
三、【类模板】
3.1【类模板的介绍】
3.2【类模板的实例化】
四、【非类型模板参数】
五、【函数模板特化】
1、概念
2、函数模板的特化步骤
六、【类模板特化】
1、全特化
2、偏特化
七、【模板的分离编译】
1、什么是分离编译
2、模板的分离编译
3、模板分离编译失败的原因
4、解决方法
八、【模板总结】
总结
前言
本片内容主要是C++中模板的有关知识,主要讲述了模板及其实用和注意事项,请耐心观看。
一、【模板的引入和介绍】
1、泛型编程
相必我们大家都听说过泛型编程这个概念:
编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
简单的说就是编写一种可以在多种数据类型上都能用的代码。比如说,你写一次代码,就可以在很多不同的数据类型上使用,而不需要为每种数据类型都重新写一遍相同逻辑的代码。它能让编程变得更简单、更灵活。
比如我们在C语言中如果想实现一个用来交换整形类型的函数可能会像下面那样写:
void swap_int(int* px, int* py) { int tmp = *px; *px = *py; *py = tmp; }
而当我们需要交换浮点类型就需要在写一个函数:
void swap_double(double* px, double* py) { double tmp = *px; *px = *py; *py = tmp; }
我们会发现C语言中并不允许我们定义重名函数,所以哪怕是相同逻辑但是数据类型不同,我们也要编写两个完全不同的函数来实现各自的功能,就像上面的交换函数。
而在C++中,我们引入了函数重载的概念其中就让我们可以定义同名函数,也就是说我们可以对同名函数进行重载使同名函数也就是相同逻辑的函数能够针对不同数据类型进行匹配调用,从而使不同类型的参数能够调用对应参数类型的函数从而实现相同的功能。
下面还是交换的例子:
void Swap(int& p1, int& p2)//交换整形 { int tmp = p1; p1 = p2; p2 = tmp; } void Swap(double& p1, double& p2)//交换浮点型 { double tmp = p1; p1 = p2; p2 = tmp; } void Swap(char& p1, char& p2)//交换字符型 { char tmp = p1; p1 = p2; p2 = tmp; }
但是我们也发现函数重载虽然能实现不同类型参数调用不同参数类型函数,实现相同功能,但是仍然具备很多问题:
1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应参数类型的函数。
2. 代码的可维护性比较低,一个函数重载出错可能导致所有的函数重载均出错。那么有没有一种办法能够让我们只写一份函数代码,实现出对应逻辑,而对于不同参数,编译器能够自动根据参数类型进行函数调用,这样就可以减少代码量,提高代码的简洁度。
2、【模板的介绍】
实际上是有解决方法的,我们可以联想到古代的活字印刷术,古人为提高抄写的效率发明的一种可以代替繁琐的“刀刻竹简”的方法。古人为了提高抄写的效率,往往将一些需要反复抄写的东西制作成活字印刷板,这样到了下次需要抄写时,可以直接将活字印刷板拓印到纸上,从而完成抄写的工作,还有一个例子就是,生活中我们时常能看到某个品牌生产的产品往往具有相同的外观,比如我们中秋节吃的月饼,往往都长得一样,这是因为生产商使用了某个固定的模具俩生产自己的月饼,这会大大提高他们的生产效率。
实际上在C++中,也存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的产品(即生成具体类型的代码),那将会节省许多精力。而这个模具我们称之为模板。
那么模板到底是什么呢?
模板就是我们自己写出一个不指定参数类型的函数,然后在调用函数时,编译器自己根据实参的类型把我们写好的不指定类型的函数,隐式编写一份用来匹配实参,我们所编写的函数就称为模板。
针对于上面的交换函数我们可以使用模板进行演示,先简单看一个例子(后面会提到具体的用法):
template<typename T>//声明模板参数这里也可以使用template<class T> void Swap(T& x, T& y)//编写交换函数逻辑 { T tmp = x; x = y; y = tmp; } int main() { int x = 1; int y = 2; cout << "交换前:" << "x=" << x << " " << "y=" << y << endl; Swap(x, y); cout <<"交换后:" << "x=" << x << " " << "y=" << y << endl; double dx = 1.1; double dy = 2.2; cout << "交换前:" << "dx=" << dx << " " << "dy=" << dy << endl; Swap(dx, dy); cout << "交换后:" << "dx=" << dx << " " << "dy=" << dy << endl; return 0; }
我们可以看到上面的Swap()函数确实能实现我们的需求,但是我们一定有很多疑问像
template<class T> 这句代码是做什么的,这里的Swap()函数,实际上是一种函数模板,除了函数模板还存在类模板,我会一一介绍。
二、【 函数模板】
2.1【模函数板的介绍】
【概念】
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数实例化,根据实参类型产生函数的特定类型版本。
【使用格式】
template<typename T1, typename T2,......,typename Tn> 返回值类型 函数名(参数列表) { //函数内容; }
这里举个例子就针对于上面的“交换函数”,这里举个例子:
template<typename T> void Swap(T& p1, T& p2) { T tmp = p1; p1 = p2; p2 = tmp; }
注意:
typename是用来定义模板参数的关键字,也可以使用class(切记:不能使用struct代替class)
1.2【函数模板的原理】
弄明白了函数模板能为我们做什么,那么函数模板的底层原理到底是什么?大家都知道,瓦特改良蒸汽机,人类开始了工业革命,解放了生产力。机器生产淘汰掉了很多手工产品。本质是什么,重复的工作交给了机器去完成。有人给出了论调:懒人创造世界。
实际上函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器,在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。
那么函数模板真的那么神奇吗,就像下面的Swap()函数他到底是如何根据参数类型实现对应的逻辑的呢?
template<typename T> void Swap(T& p1, T& p2) { T tmp = p1; p1 = p2; p2 = tmp; }
结论:
实际上编译器是在这里先根据T所接收到的参数类型去实例化对应参数类型的Swap()函数,比如T接收到了int,那么编译器就会在底层实例化出一个参数类型为int的Swap()函数。当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
我们知道,模板实际上是编译器帮我们把活给做了,编译器通过参数的类型,自动生成对应参数的函数代码,并自动匹配调用,到这里模板好像也没有那么神奇,只是编译器把本来属于我们的任务给完成了。
1.3【函数模板的实例化】
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
1、隐式实例化
让编译器根据实参推演模板参数的实际类型
那么隐式实例化该怎样使用呢?,下面看这样一个例子:
template<typename T> T Add(const T& x, const T& y) { return x + y; } int main() { int a = 10, b = 20; int c = Add(a, b); //编译器根据实参a和b推演出模板参数为int类型 return 0; }
特别注意:使用模板时,编译器一般不会进行类型转换操作。所以,以下代码将不能通过编译,原因是a,b的类型不统一,编译器不会主动进行类型转换:
int main() { int a = 10; double b = 1.1; int c = Add(a, b); return 0; }
因为在编译期间,编译器根据实参推演模板参数的实际类型时,根据实参a将T推演为int,根据实参b将T推演为double,但是模板参数列表中只有一个T,编译器无法确定此处应该将T确定为int还是double。
此时,我们有两种处理方式,第一种就是我们在传参时将b强制转换为int类型,第二种就是使用下面即将说到的显示实例化。我们看一下强制类型转换:
int main() { int a = 10; double b = 1.1; int c = Add(a, (int)b); return 0; }
我们会发现,这里强制类型转换实际上并不能解决问题,1.1+10=11.1,结果却为11,下面再让我们看一下显示实例化。
2、显示实例化
在函数名后的加上“ <> ”并在其中指定模板参数的实际类型
template<typename T> T Add(const T& x, const T& y) { return x + y; } int main() { int a = 10; double b = 1.1; int c = Add<int>(a, b); //指定模板参数的实际类型为int return 0; }
这里我们看到,将Add()写成了,Add<int>(),就是告诉编译器对模板参数全都按照int类型处理,即使b为double,也会被当作int类型。
注意:
在使用显式实例化时,如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
但是这里好像也没有解决我们上面的问题,下面让我们看一下下面的例子:
template<typename T1,typename T2> T2 Add(const T1& x, const T2& y) { return x + y; } int main() { int a = 10; double b = 1.1; //指定模板参数的实际类型为int和double cout << a << " " << b << " " << Add<int, double>(a, b) << endl; return 0; }
我们可以看到这里显示地把a实例化为int(T1为int),把b实例化为double(T2为double),才能得到正确的结果,所以模板虽然一定程度上解决了我们很多问题,但是它并不是万能的,对于我们多样化的要求,还是需要我们自己加以控制才能得到想要的结果。
1.4【模板参数的匹配原则】
1、 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数 int Add(int left, int right) { return left + right; } // 通用加法函数 template<class T> T Add(T left, T right) { return left + right; } void Test() { Add(10, 20); // 与非模板函数匹配,编译器不需要特化 Add<int>(10, 20); // 调用编译器特化的Add版本 }
2、对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板函数
// 专门处理int的加法函数 int Add(int left, int right) { return left + right; } // 通用加法函数 template<class T1, class T2> T1 Add(T1 left, T2 right) { return left + right; } void Test() { Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化 Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数 }
3、 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
template<typename T> T Add(const T& x, const T& y) { return x + y; } int main() { int a = Add(2, 2.2); //模板函数不允许自动类型转换,不能通过编译 return 0; }
因为模板函数不允许自动类型转换,所以不会将2自动转换为2.0,或是将2.2自动转换为2。
三、【类模板】
3.1【类模板的介绍】
同函数模板一样类模板的定义也要用到关键字template< >:
template<class T1, class T2, ..., class Tn> class //类模板名 { // 类内成员定义 };
举个例子:
template<class T>//这里T可为int,也可为double,分数有90整和90.5 class Score { public: void Print() { cout << "数学:" << _Math << endl; cout << "语文:" << _Chinese << endl; cout << "英语:" << _English << endl; } private: T _Math; T _Chinese; T _English; };
注意:类模板中函数放在类外进行定义时,需要加模板参数列表。
比如下面的T可以写成int 和double
template<class T> class Score { public: void Print(); private: T _Math; T _Chinese; T _English; }; //类模板中的成员函数在类外定义,需要加模板参数列表 template<class T>// void Score<T>::Print() { cout << "数学:" << _Math << endl; cout << "语文:" << _Chinese << endl; cout << "英语:" << _English << endl; }
除此之外,类模板一般不支持分离编译(即声明在xxx.h文件中,而定义却在xxx.cpp文件中)。具体原因在后面会讲解。
3.2【类模板的实例化】
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟“ <> ”。然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
比如下面的例子:
Stack是类名,Stack<int>和Stack<double>才是类型.
template<class T> Stack<int> s1; Stack<double> s2;
注意:
类模板名字不是真正的类,而实例化的结果才是真正的类。
四、【非类型模板参数】
模板参数包括类型形参与非类型形参。
类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。例如,当我们要实现一个静态数组的类,就需要用到非类型模板参数。
template<class T, size_t N> //N:非类型模板参数 class StaticArray { public: size_t arraysize() { return N; } private: T _array[N]; //利用非类型模板参数指定静态数组的大小 };
使用非类型模板参数后,我们就可以在实例化对象的时候指定所要创建的静态数组的大小了。
int main() { StaticArray<int, 10> a1; //定义一个大小为10的静态数组 cout << a1.arraysize() << endl; //10 StaticArray<int, 100> a2; //定义一个大小为100的静态数组 cout << a2.arraysize() << endl; //100 return 0; }
注意:
1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果,因为编译器在编译阶段就需要根据传入的非类型模板参数生成对应的类或函数。五、【函数模板特化】
1、概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。
比如:这里我们实现了一个专门用来进行判断两个对象是否相等的函数模板
template<class T> bool IsEqual(const T& x, const T& y) { return x == y; }
我们大概会这样使用该函数模板:
cout << IsEqual(1, 1) << endl; //1 cout << IsEqual(1.1, 2.2) << endl; //0
这样使用是没有问题的,它的判断结果也是我们所预期的,但是我们也可能会这样去使用该函数模板:
char a1[] = "fantasyuanqian"; char a2[] = "fantasyuanqian"; cout << IsEqual(a1, a2) << endl; //0
判断结果是这两个字符串不相等,这很好理解,因为我们希望的是该函数能够判断两个字符串的内容是否相等,而该函数实际上判断是确实这两个字符串所存储的地址(数组名是数组首元素的地址,这里实际上将T转化为了int)是否相同,这是两个存在于栈区的字符串,其地址显然是不同的。
类似于上述实例,使用模板可以实现一些与类型无关的代码,但对于一些特殊的类型可能会得到一些错误的结果,此时就需要对模板进行特化,即在原模板的基础上,针对特殊类型进行特殊化的实现方式。就像这里对于字符串的判断,我们应该拿到通过地址拿到存储内容再进行比较,所以要进行特殊处理:
我们知道当传入的类型是char*时,应该依次比较各个字符的ASCII码值进而判断两个字符串是否相等,或是直接调用strcmp函数进行字符串比较,那么此时我们就可以对char*类型进行特殊化的实现。
下面是特化后的例子:
//基础的函数模板 template<class T> bool IsEqual(const T& x, const T& y) { return x == y; } //对于char*类型的特化 template<> bool IsEqual<char*>(const char* x, const char* y) { return strcmp(x, y) == 0; }
我们发现,特化后的template没有模板参数了,并且在函数名后显示实例化了参数类型,最后重写了模板函数的实现内容。
2、函数模板的特化步骤
- 首先必须要有一个基础的函数模板。
- 关键字template后面接一对空的尖括号<>。
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
- 函数形参表必须要和模板函数的基础参数类型完全相同,否则不同的编译器可能会报一些奇怪的错误。
注意:
一般情况下,如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。例如,上述实例char*类型的特化还可以这样给出:
//基础的函数模板 template<class T> bool IsEqual(T x, T y) { return x == y; } //对于char*类型的特化 bool IsEqual(char* x, char* y) { return strcmp(x, y) == 0; }
六、【类模板特化】
不仅函数模板可以进行特化,类模板也可以针对特殊类型进行特殊化实现,并且类模板的特化又可分为全特化和偏特化(半特化)。
1、全特化
全特化就是将模板参数列表中所有的参数都确定化。
比如对于下面的模板类:
template<class T1, class T2> class Fantasy { public: //构造函数 Fantasy() { cout << "Fantasy<T1, T2>" << endl; } private: T1 _F1; T2 _F2; };
当T1和T2分别是double和int时,我们若是想对实例化的类进行特殊化处理,那么我们就可以对T1和T2分别是double和int时的模板进行全特化:
template<class T1, class T2> class Fantasy { public: //构造函数 Fantasy() { cout << "Fantasy<T1, T2>" << endl; } private: T1 _F1; T2 _F2; }; //对于T1是double,T2是int时进行特化 template<> class Fantasy<double, int> { public: //构造函数 Fantasy() { cout << "Fantasy<double, int>" << endl; } private: double _F1; int _F2; };
类模板的特化步骤:
- 首先必须要有一个基础的类模板。
- 关键字template后面接一对空的尖括号<>。
- 类名后跟一对尖括号,尖括号中指定需要特化的类型。
那么如何证明当T1是double,T2是int时,使用的就是我们自己特化的类模板呢?
当我们实例化一个对象时,编译器会自动调用其默认构造函数,我们若是在构造函数当中打印适当的提示信息,那么当我们实例化对象后,通过观察控制台上打印的结果,即可确定实例化该对象时调用的是不是我们自己特化的类模板了。2、偏特化
任何针对模版参数进一步进行条件限制设计的特化版本。
比如对于以下模板类:
template<class T1, class T2> class Fantasy { public: //构造函数 Fantasy() { cout << "Fantasy<T1,T2>" << endl; } private: T1 _F1; T2 _F2; };
偏特化又可分为以下两种表现形式:
1、部分特化
我们可以仅对模板参数列表中的部分参数进行确定化。
例如,我们可以对T1为int类型的类进行特殊化处理。//对T1为int的类进行特化 template<class T2> class Fantasy<int, T2> { public: //构造函数 Fantasy() { cout << "Fantasy<int,T2>" << endl; } private: int _F1; T2 _F2; }; int main() { Fantasy<int, int> f1; Fantasy<double,int> f2; Fantasy<double,double> f3; return 0; }
2、参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。template<class T1,class T2> class Fantasy<T1*, T2*> { public: //构造函数 Fantasy() { cout << "Fantasy<T1*,T2*>" << endl; } private: T1 _F1; T2 _F2; }; template<class T1, class T2> class Fantasy<T1&, T2&> { public: //构造函数 Fantasy() { cout << "Fantasy<T1&,T2&>" << endl; } private: T1 _F1; T2 _F2; }; int main() { Fantasy<int, int> f1; Fantasy<int*,int*> f2; Fantasy<int& ,int&> f3; return 0; }
此时,当实例化对象的T1和T2同时为指针类型或同时为引用类型时,就会分别调用我们特化的两个类模板。
七、【模板的分离编译】
这里附上一篇资料:
为什么C++编译器不能支持对模板的分离式编译-CSDN博客
1、什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。又叫模块化编程,定义和声明分开。
2、模板的分离编译
在分离编译模式下,我们一般创建三个文件,一个头文件用于进行函数声明,一个源文件用于对头文件中声明的函数进行定义,最后一个源文件用于调用头文件当中的函数。
按照此方法,我们若是对一个加法函数模板进行分离编译,其三个文件当中的内容大致如下:但是使用这三个文件生成可执行文件时,却会在链接阶段产生报错。
下面我们对其进行分析:
我们都知道,程序要运行起来一般要经历以下四个步骤:
- 预处理: 头文件展开、去注释、宏替换、条件编译等。
- 编译: 检查代码的规范性、是否有语法错误等,确定代码实际要做的工作,在检查无误后,将代码翻译成汇编语言。
- 汇编: 把编译阶段生成的文件转成目标文件。
- 链接: 将生成的各个目标文件进行链接,生成可执行文件。
以上代码在预处理阶段需要进行头文件的包含以及去注释操作。
这三个文件经过预处理后实际上就只有两个文件了,若是对应到Linux操作系统当中,此时就生成了 Add.i 和 main.i 文件了。
预处理后就需要进行编译,在 Add.i 当中有Add函数的定义,并且在 main.i 里面也有Add函数模板的声明和调用Add函数的代码,因此在编译阶段并不会发现任何语法错误,之后便顺利将 Add.i 和 main.i 翻译成了汇编语言,对应到Linux操作系统当中就生成了 Add.s 和 main.s 文件。
之后就到达了汇编阶段,此阶段利用 Add.s 和 main.s 这两个文件分别生成了两个目标文件,对应到Linux操作系统当中就是生成了 Add.o 和 main.o 两个目标文件。
前面的预处理、编译和汇编都没有问题,现在就需要将生成的两个目标文件进行链接操作了,但在链接时发现,在main函数当中调用的两个Add函数实际上并没有被真正定义,主要原因是函数模板并没有生成对应的函数,因为在全过程中都没有实例化过函数模板的模板参数T,所以函数模板根本就不知道该实例化T为何类型的函数。
3、模板分离编译失败的原因
在函数模板定义的地方(Add.cpp)没有进行实例化,而在需要实例化函数的地方(main.cpp)没有模板函数的定义,无法进行实例化。4、解决方法
解决类似于上述模板分离编译失败的方法有两个,第一个就是在模板定义的位置进行显示实例化。
例如,对于上述代码解决方案如下:1、模板定义的位置显式实例化。
在函数模板定义的地方,对T为int和double类型的函数进行了显示实例化,这样在链接时就不会找不到对应函数的定义了,也就能正确执行代码了。
2、将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。
虽然第一种方法能够解决模板分离编译失败的问题,但是我们这里并不推荐这种方法,因为我们需要用到一个函数模板实例化的函数,就需要自己手动显示实例化一个函数,非常麻烦。
现在就来说说解决该问题的第二个方法,也是我们所推荐的,那就是对于模板来说最好不要进行分离编译,不论是函数模板还是类模板,将模板的声明和定义都放到一个文件当中就行了。一般都放在头文件中,该头文件常命名为 “.hpp ”
八、【模板总结】
优点:
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
- 增强了代码的灵活性。
缺陷:
- 模板会导致代码膨胀问题,也会导致编译时间变长。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误。
总结
本篇内容到这里就结束了,如有错误还请指正,最后模板确实是C++中不可或缺的一部分,有了模板能减少很多重复冗余的工作,希望本篇博客能帮到你。
.............................................................................我好像在哪儿见过你,我在劝我,该忘了你
————《我好像在哪见过你》