- ⛄️一、模板与泛型编程的关系
- ⛄️模板的介绍
- ⛄️泛型编程
- ⛄️1.函数模板
- ⛄️1.1格式与使用方法
- ⛄️1.2函数模板的实例化
- 隐式实例化
- 显式实例化
- ⛄️1.3模板参数的匹配原则
- ⛄️2.类模板
- 2.1类模板与函数模板的区别
- 2.1格式与使用方法
- 2.2类模板的实例化
- ⛄️二、模板不可以声明和定义分离在不同文件
- 解决方法
- 1.在定义的地方显式示例化
- 函数模板:
- 类模板:
- 2.将声明和定义放在一个.h或者.hpp文件中
- 函数模板:
- 类模板:
在进入正题之前 先来看一个例子!
实现一个可以支持各种类型的交换函数swap()
首先我们就会想到 函数重载 同名函数参数的类型不同就可以实现重载
如下:
void swap(int& a,int& b)//整数类型交换
{
int tmp=a;
a=b;
b=tmp;
}
void swap(double& a,double& b)//浮点型交换
{
double tmp=a;
a=b;
b=tmp;
}
//还有很多类型.....
//用模板实现就是下面这样 不需要写很多个重复的只是参数不同的代码
template
void swap(T& a,T& b)
{
T tmp=a;
a=b;
b=tmp;
}
//调用的时候就直接传参数就行 例如 编译器会自动识别参数类型生成不同的swap函数 不用我们自己来写
int a=10,b=20;
double c=1.2,d=2.2;
swap(a,b);
swap(c,d);
这仅仅是两种类型的交换函数 就要写两个 那么如果有很多种类型的函数就需要自己一个个写 那岂不是很麻烦 ?
模板就是来解决这种问题的 对于编译器而言模板就是一个模子,对于不同的类型 在编译阶段编译器自动识别实参的类型确定模板参数的类型 模板参数确定后就可生成对应的代码,就不需要我们自己写了,解放程序员的双手!
- 这个过程就像是往一个雪糕模具里,投放不同的原材料,那么做出来的就是不同味道的雪糕。他们只是口味不同,但都是雪糕,都是可以吃的。类比此处的模板 都是雪糕就是代表着都是相同功能的代码 不同的原材料就是不同的类型 雪糕模具就是本文的模板
模板的种类:模板包括函数模板和类模板(上图是函数模板)
⛄️泛型编程泛型编程就是针对复用性高但是适用的类型不同的代码 使其可以忽略类型的限制 只需要一份代码就可以适用多种类型的数据 减少了代码量。
而上面的模板就是泛型编程的基础,没有模板就实现不了泛型编程,两者具有紧密的联系。
⛄️1.函数模板函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定
类型版本。
//格式 template返回值类型 函数名(参数列表){}
模板函数定义和声明的时候需要在函数的上方声明一个模板(注意函数模板不能声明和定义分离,否则会出现链接错误,后边会详细介绍)
⛄️1.2函数模板的实例化 隐式实例化隐式实例化就是不用我们自己指定模板参数的类型,让编译器自己根根据实参类型推演对弈的代码。
例如:
templateT add(T& a,T& b) { return a+b; } int main() { int a=1,b=2; double c=1.1,d=2.2; cout< 显式实例化 当函数实参的参数和模板参数无关时,编译器就不知道模板参数是什么类型,无法自动推演代码,这时就需要我们手动显式实例化来指定函数模板的模板参数了 使得编译器能生成对应的代码
例如:
//错误的代码 template//函数模板不一定都是由编译器自动推导出来的 有时候也需要自己显式的实例化 指定模板参数 T* creatarr(int n) { return new T[n]; } int main() { cteatarr(10);//给的实参跟模板参数T无关 编译器无法确定模板参数的类型 也就无法自动推演出对应的代码了 return 0; } //正确的代码 templateT* creatarr(int n) { return new T[n]; } int main() { cteatarr (10); //显式的指定模板参数的类型为int 那么编译器就可以确定模板参数类型就可以生成对应的代码 creatarr (10); return 0; } 函数模板的大部分的使用场景都是不需要我们显式实例化的,但是像上面这种情况就需要我们显式实例化指定模板参数类型!
- 如果只有一个模板参数显式实例化后如果还有实参的类型是模板参数的类型且多个实参的类型不相同的时候 编译器会尝试隐式类型转换来解决,如实在无法转换,就会报错。
例如:
templateT add(T& a,T& b) { return a+b; } int main() { int a=1,b=2; double c=1.1,d=2.2; add (a,c);//显式实例化 return 0; } 上面的代码如果不显式实例化就会编译失败,显式实例化之后就会生成int类型的add函数 但是实参的类型不统一 一个为int 一个为double那么编译器就会对其进行隐式类型转换处理 将double类型的实参转换为int(取浮点型的整数部分) 类型 然后传给形参,实现参数的类型统一。
⛄️1.3模板参数的匹配原则1.同名的普通函数可以和函数模板同时存在,并且该函数模板还可以被实例化为那个普通函数
例:
templateT sub(T& a,T& b) { return a-b; } int sub(int& a,int& b) { return a-b; } int main() { int a=10,b=2; sub(a,b);//调用普通函数 sub (a,b);//调用函数模板实例化出来的函数 return 0; }
- 其中sub(a,b)会调用已经存在的普通函数,因为其与普通函数和函数模板都匹配,但是普通函数已经存在了,而函数模板还要去实例化处理再用,编译器会优先使用现成的已经存在的且完全匹配的那个普通函数sub
sub(a,b)是显式实例化了sub这个函数模板,那么就告诉编译器我就是要调用实例化处来的,不用现成的可以匹配的。就会去实例化函数模板,生成对应的函数,并且调用。
2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
例:
templateT sub(T& a,K& b) { return a-b; } int sub(int& a,int& b) { return a-b; } int main() { int a=10,b=20; double c=2.3; sub(a,c);//会调用函数模板实例化出来的函数 return 0; } 对于上述代码 存在一个整型的sub函数 和一个含有俩个模板参数的sub函数模板 调用sub(a,c)的时候因为c是double类型 所以现成的sub函数不够匹配 那么编译器就会通过该函数模板推演出一个第一个形参为int,第二个形参为double的sub函数 并调用
⛄️2.类模板类中的成员变量的类型不同可以实现不同类型的数据的存储和操作,例如一个栈的类,当成员变量是char类型的时候可以对字符类型的数据进行存储和相关操作,当成员变量的参数类型是int的时候可以实现对int类型的数据的存储和相关操作。
2.1类模板与函数模板的区别
- 那么这个时候类型对一个类的影响也显现出来了,如果可以像函数模板那样也忽略类型对自身的影响那么就不用为了面向数据类型的不同而写多份功能相同但是适用数据类型不同的代码了,刚好类模板就是解决这一问题的。
函数模板可以隐式实例化,也可以显式实例化,但是类模板不同,类模板必须要显式实例化,指定模板参数。
2.1格式与使用方法
- 与函数模板几乎一样 在类的声明前声明一个模板即可
templateclass 类名 { //返回值或参数类型的函数 T func(T& s){} ..... //T类型的成员变量 T k; }
- 例:丐版的vector类
template2.2类模板的实例化//类模板 class vector { public: vector(int n=10) { _size = 0; _capacity = n; _a = new T[_capacity]; } void push_back(const T& x) { if (int ret = _size == _capacity ? 1 : 0) { T* _b = new T(_size * 2); memcpy(_b, _a, _size * sizeof(T)); _a = _b; _capacity *= 2; } _a[_size++] = x; } void pop_back() { if (_size > 0) --_size; else { cout << "null" << endl; } } void print() { for (int i = 0; i < _size; i++) { cout << _a[i] << endl; } } int :getsize() { return _size; } int getcapacity() { return _capacity; } private: int _size; int _capacity; T* _a; }; 以上述vector类的实例化为例 非常简单:
类名<模板参数> 对象名(参数列表); vector⛄️二、模板不可以声明和定义分离在不同文件v(10); 类模板还有个需要注意的点就是声明和定义不可以分离,即不可以一个放到.h 文件中,一个放到.cpp文件中,否则会出现链接错误!
- 以上图的丐版vector类为例,类的声明,定义,分离在两个文件中,最后在main.cpp中调用 会出现链接错误
为什么呢?因为调用函数的时候到符号表里找不到地址,这里我们要回顾一下程序编译链接的过程了
在预处理阶段,头文件会在template.cpp 和 main.cpp中展开 ,生成.i文件 ,然后再由.i文件进行编译生成汇编代码,这其中就要将函数的地址放到符号表里去,但是在template.cpp中虽然有定义,压根就不知道模板参数的类型是什么,编译器无法生成对应的代码,那么符号表里自然就没有对应函数的地址了,main.cpp中又调用了对应的函数,就会有call _Zxxx(ii)(?)…之类的汇编代码,其中?代表的是调用函的地址待定,运行时到符号表里找;在两个.o文件链接的时候,函数调用到符号表里找对应函数的地址就找不到,就会链接失败!
解决方法 1.在定义的地方显式示例化在定义的时候显式示例化就可以生成对应的函数代码,符号表里就会有对应函数的地址,调用的时候就可以找到,完成函数调用。
函数模板:格式: template 返回值类型 函数名<模板参数类型>(参数列表); 例: template void swap类模板:(T& a,T& b); 格式: template class 类名<模板参数类型>; 例: template class vector2.将声明和定义放在一个.h或者.hpp文件中; 声明和定义放到一个.h或者.hpp 中那么在头文件在main.cpp中展开的时候就会有也具有声明和定义 那么在函数调用的时候编译器就会直接生成对应的代码,直接调用函数,不再会去符号表里找对应的函数地址了(只有声明的时候调用函数才会去符号表中找地址,有定义直接调,根本就不会将此函数的地址放到符号表!)
函数模板:格式: template类模板:void swap(T& a,T& b){函数实现} 格式: template类名<模板参数>::函数名(参数){函数实现} 以上两种方式都可以使得程序编译通过 运行成功 但是第二种方法较为推荐!



