目录
1、泛型编程
2、函数模板
2.1、函数模板的概念
2.2、函数模板格式:
2.3、函数模板的原理
2.4、函数模板的实例化
2.5、函数模板的模板参数的匹配原则
3、类模板
3.1、类模板的定义格式
3.2、类模板的实例化
4、拓展
//在C++中,为了支持各种类型的两个数据间进行交换操作,我们不得不写出多个重载函数,如下所示:
//1、
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
//2、
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
//3、
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
......
在使用模板时,引出了关键字 template、
template
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
注意:typename是定义模板参数的关键字,也可以使用关键字class,但不能使用关键字struct、
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
//template
//template
//使用关键字class和关键字typename都是可以的,目前为止,两者没有任何区别,但是不能使用关键字struct在C++旧版本中,
//习惯使用关键字class,在新版本中,习惯使用关键字typename,但两者都是可以的,目前使用关键字 typename 比较多,此处
//的模板参数 T 代表某一种类型,具体是哪一种类型是不知道的,可以为任意类型, 模板分为两类: 函数模板 和 类模板、
//template
1、函数模板
//void Swap(T& left, T& right)
//{
// T temp = left;
// left = right;
// right = temp;
//}
此时的模板参数 T 为自定义类型也是可以的,任何类型都是可以的,但是要保证该自定义类型的变量left和right之间要支持赋值操作,T是模板参数,是我们定义函数模板要用的类型名,也可以使用其他字母来表示模板参数,一般习惯大写、
int main()
{
int a = 1,b = 2;
double c = 1.1, d = 2.2;
//方式1:
//Swap(a, b);
//Swap(c, d);
//注意:此处两次调用Swap函数,并不是调用的同一个函数,要知道,不管是函数模板还是类模板,都不是我们能够直接调用的,函数模板本身不是函数
//此处调用的两次Swap函数分别是由该模板根据两者的实参a和b的类型,c和d的类型,生成的两个具有特定类型的函数,两者构成函数重载,即,Swap(a,b); 调用的应该是函数
//void Swap(int& left, int& right) ,而 Swap(c, d); 调用的则是函数: void Swap(double& left, double& right) 、
//方式2:
swap(a, b);
swap(c, d);
cout << a << " " << b << endl;
cout << c << " " << d << endl;
return 0;
}
在C++中,当再使用交换函数时,就不需要自己实现 Swap 函数了,此时C++库中有库函数 swap ,并且该库函数支持任何类型的数据进行交换操作,但要保证,该任何类型的变量a和b之间要支持赋值操作,直接使用库函数 swap 即可,规则如下所示:
在C语言中,不支持函数重载,再加上其他的原因导致,在C语言中没有和C++库中的库函数 swap具有相同功能的库函数 swap,C语言中不提供数据结构的库也是这个原因、
通过函数模板自动生成具有特定的各种类型的函数时,该过程称为函数模板的实例化,函数模板的实例化分为:隐式实例化和显式实例化、1、隐式实例化:让编译器根据实参的类型自动推导出来模板参数的实际类型、2、显式实例化:当编译器无法通过实参的类型自动推导出来模板参数的实际类型时,此时需要我们手动的在函数的调用处的函数名后面显式的指定出明确的类型,即,明确的指定模板参数的实际类型,这一步骤称为函数模板的显示实例化、#define _CRT_SECURE_NO_WARNINGS 1 #include
using namespace std; //一个template 的下面只能对一个函数模板进行定义,比如,若再写一个Func1的函数模板时,仍需要在Fun1的函数模板上面再写出一个template //1、 template T* Func(int n) { return new T[n]; } //2、 template //此时需要再次写出一个template ,否则会报错、 T* Func1(int n) { return new T[n]; } //3、 //模板参数也可以存在缺省参数,但只能从右往左进行缺省,可以全缺省,也可以半缺省,和函数参数(形参)的用法一样,如下所示: template T* Func2(int n) { return new T[n]; } int main() { //1、 //在函数名Func后面进行函数模板的显式实例化、 int* p1=Func (10); double* p2 = Func (10); //若此处的代码中不写 的话,此时编译器不知道函数模板中的模板参数T具体是什么类型,此时也没办法通过实参来自动推导模板参数T为何种类型, //则会报错说是:未能推导出模板参数T的类型,要知道: 对于函数模板而言,模板参数T一般情况下是根据实参的类型来推导,但是也会有一些情况不能通 //过传入的实参的类型来推导模板参数T,如此处所示这种情况那么此时就只能在函数名后面对函数模板显式的实例化出模板参数T的类型才可以,具体操 //作如上代码所示、 //3、 char* p3 = Func2(10);//此时就不需要在函数名Func2后面进行函数模板的显式实例化了、 return 0; }
注意:#define _CRT_SECURE_NO_WARNINGS 1 #include
using namespace std; template T Add(const T& left, const T& right) { return left + right; } int main() { int a = 10; double b = 10.0; //Add(a, b); //如果写成上述所示,则编译器就会报错,因为,此时属于函数模板的隐式实例化,是编译器通过实参的类型自动推导出 //模板参数T的实际类型,而此时,变量a为整型int类型,变量b为双精度浮点型double类型,则此时的模板类型T的具体类 //型就会出现歧义,不清道到底是整型int类型还是双精度的浮点型double类型,所以才会报错,解决方法如下所示: //注意:在模板中,如上所示,编译器一般不会主动进行隐式类型转换操作,即,编译器不会主动将变量a的类型(整型int类型) //转换为变量b的类型(双精度浮点型double类型)也不会主动将变量b的类型(双精度浮点型double类型)转换为变量a的类型 //(整型int类型),因为一旦转化出问题,编译器就需要背黑锅、 //解决方法: //1、 Add (a, b); //注意:对于函数模板的实例化而言,若存在显式实例化,那么隐式实例化就不再进行了,只有当显示实例化不存在时,才会执行隐式实例化, //此时进行显示实例化,明确指定模板参数T的实际类型为整型int类型,那么在传实参的过程中,变量b的类型则是由double变成const int //这属于隐式类型转换,会发生截断,可能会造成数据丢失,则会报警告,此处的隐式类型转换并不属于有道云中记录的前两种情况,因为双 //精度浮点型变量b明确指定了隐式类型转换为const int类型,注意这三种情况不要混淆、 //Add (a, b); //此时,整型int类型的变量a的类型由整型int类型变成了双精度浮点型const double类型,也属于隐式类型转换的第三种情况,但是会发生 //提升(不是整型提升),提升则不会造成数据丢失,不会报警告、 //2、 Add(a, (int)b); //此时属于函数模板的隐式实例化,由于不存在显式实例化,故会执行隐式实例化,此处把双精度浮点型的变量b强制类型转换为整型int类型 //那么在执行隐式实例化时,对于模板参数T而言,就不会产生歧义,可以正常运行,下面代码也是类似的道理、 Add((double)a,b); return 0; }
1、一个非函数模板的函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非函数模板的函数、
#define _CRT_SECURE_NO_WARNINGS 1 #include
using namespace std; //1.一个非函数模板的函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非函数模板的函数、 //专门处理整型int类型数据的Add加法函数 int Add(int left, int right) { return left + right; } //函数模板 template T Add(T left, T right) { return left + right; } //上述两者可以同时存在,两者不能构成函数重载,因为,函数模板本身就不是函数,在此时也不考虑该函数模板的函数名修饰规则 //只会修饰由该函数模板实例化出来的具有特定类型(各种类型)的函数、 void Test() { Add(1, 2); //此处调用的就是专门处理整型int类型数据的Add加法函数,只有当专门处理整型int类型数据的Add加法函数不存在时,才会调用由函数模板 //实例化出来的具有特定类型(整型int类型)的函数,即,才会调用由编译器根据实参类型和函数模板实例化出来的的具有特定类型(整型int类型)的函数, //有现成的就会优先调用现成的,没有现成的再去调用由函数模板实例化得到的具有特定类型(整型int类型)的函数、 Add (1, 2); //若在此处的函数调用处的函数名的后面加上 的话,即,函数模板在显式实例化时,即使存在专门处理整型int类型数据的Add加法函数,系统也不会去 //调用这个专门处理整型int类型数据的Add加法函数,而是调用由函数模板实例化得到的具有特定类型(整型int类型)的函数、 } int main() { Test(); return 0; }
2、在通过函数模板实例化时,实参在传给函数模板形参的过程中,不能自动进行隐式类型转换,而在当调用普通函数,在实参传给形参的过程中,允许自动进行隐式类型转换、
3、如果可以通过函数模板实例化出一个具有更好匹配的特定类型的函数,那么此时则会调用该函数、
#define _CRT_SECURE_NO_WARNINGS 1 #include
using namespace std; //1、 //专门处理整型int类型数据的Add加法函数 int Add(int left, int right) { return left + right; } //2、 //函数模板 template //函数模板和类模板都可以存在多个模板参数,如这里的模板参数 K 和 V 、 void Add(const K& key, const V& value) { cout << key << ":" << value << endl; } //3、 //函数模板 //模板参数也可以存在缺省参数,但只能从右往左进行缺省,可以全缺省,也可以半缺省,和函数参数(形参)的用法一样、 template //函数模板和类模板都可以存在多个模板参数,如这里的模板参数 K 和 V 、 void Func() { cout << sizeof(K) << endl; cout << sizeof(V) << endl; } void Test1() { //1、 Add(1, 2); //存在专门处理整型int类型数据的Add加法函数,且该行代码与专门处理整型int类型数据的Add加法函数完全匹配,则优先调用专门处理整型int类型数据的Add加法函数,不需要通过函数模板实例化出具有特定类型(整型int类型)的函数、 //2、 Add(1, 2.0); //此时,若函数模板2不存在的话,该行代码会去调用专门处理整型int类型数据的Add加法函数,因为该函数是一个普通函数,在该普通函数调用时,在实参传给形参的过程中,支持自动进行隐式类型转换,故,双精度浮点型double类型的数据2.0,在传给形参的 //过程中,会自动进行隐式类型转换为整型int类型,则是可以的成功编译的,只不过会发生截断,会报错说是可能会造成数据丢失,但是可以成功编译,而当函数模板2存在时,此时可以通过该函数模板2实例化出来一个与该行代码非常匹配的一个具有特定类型的 //函数,即: void Add(const int& key, const double& value),所以,此时该行代码执行时,就不再调用专门处理整型int类型数据的Add加法函数,而是调用由函数模板2实例化出来的一个具有特定类型(int,double)的函数、 //此时需要调用通过函数模板实例化出来的具有特定类型的函数,即: void Add(const int& key, const double& value) ,此时模板参数 K 就是整型int类型,模板参数 V 就是双精度浮点型double类型,因为,2.0后面没有加f,则默认为双精度浮点型double类型、 Add (1, 'A'); //若此处不加 的话,则会进行函数模板2的隐式实例化,由于可以通过函数模板2实例化出来的一个与该行代码非常匹配的具有特定类型(int,char)的函数,所以会调用由 //函数模板2实例化出来的具有特定类型(int,char)的函数,即:void Add(const int& key, const char& value),但是若加上 的话,则会直接去调用由函数模板2实例化出来的 //具有特定类型(int,cahr)的函数,此时属于函数模板2的显式实例化,当显式实例化存在时,就不会再执行隐式实例化操作,此时一定调用的是由函数模板2实例化出来的具有特定类型(int,char)的函数, //即,调用通过函数模板2实例化出来的具有特定类型(int,char)的函数,即: void Add(const int& key, const char& value) ,此时模板参数 K 就是整型int类型,模板参数 V 就是字符型char类型、 } void Test2() { //3、 Func (); Func (); Func(); } int main() { Test1(); Test2(); return 0; }
template
class 类模板的类名
{
//类模板的类体内的类成员函数和类成员变量、
};
// Vector 是类名,而 Vector , Vector 才是类型(自定义类型)、
Vector s1;
Vector s2;
//注意:自定义类型的对象 s1和s2 都属于自定义类型,但两者并不是相同的类型,两者不能进行相互赋值、
//2 、类模板 #define _CRT_SECURE_NO_WARNINGS 1 #include
using namespace std; typedef int STDateType; //若该数据结构的栈中存储的数据不再是整型int类型的话,那么应该怎么办呢? //此时可以进行 typedef 操作,但是这种方法也不是很好: //1.若该数据结构的栈中存储的数据是整型int类型,则需要手动改为:typedef int STDateType; ,若存储的是浮点型double类型 //则需要手动改为: typedef double STDateType; 每次都需要手动去更改,比较麻烦、 //2.最重要的缺点是在于:若在main函数中定义两个数据结构栈st1和st2,但是这两个数据结构栈中所存储的数据并不是同一种类型, //比如:数据结构栈st1中存储整型int类型,数据结构栈st2中存储浮点型double类型,那么此时的 typedef 操作就不能满足要求了、 //此时根据上述两个缺点,C++中引入了 类模板 的概念,具体操作如下所示: template class Stack //此处的类模板Stack并不是一个具体的类,只是一个模板(类模板),由该类模板实例化出来的具有特定类型(各种类型)的类,才是具体的类、 { public: Stack(int capacity = 10) { cout << "Stack(int capacity = 10)" << endl; _a = new T[capacity]; _capacity = capacity; _top = 0; } ~Stack() { cout << "~Stack()" << endl; delete[] _a; _capacity = _top = 0; } void Push(const T& x) {} //此时这里最好不要传值传参,因为,模板参数T可能是自定义的类型,若为自定义类型的话,在C++中传值传参会自动调用拷贝构造函数, //若我们在对应的类体中不显式的实现拷贝构造函数,且对应的类体中的内置类型的类成员变量再涉及到深拷贝的话,问题就更大了,所 //以此处最好不要传值传参,尽量使用传引用传参如此,不管模板参数T是什么类型,都不会出现问题,包括自定义类型、 private: T* _a; int _capacity; int _top; }; int main() { //在类名 Stack 后面进行类模板的显式实例化、 //对于自定义类型的对象st1而言,其类型是Stack ,对于自定义类型的对象st2而言,其类型是Stack ,这两种类型 //都属于自定义类型,但并不是同一种类型、 Stack st1; //数据结构栈st1存储整型int类型的数据、 st1.Push(1); Stack st2;//数据结构栈st2存储双精度浮点型double类型的数据、 st1.Push(2.2); //此处的类模板与函数模板的使用中不同的是,在函数模板的使用中,当函数模板的显式实例化不存在时,编译器能够通过传入的实参的类型来推导出对应类型的函数,即自动推导出模板参数T的类型,即执行函数模板的隐式实例化, //但这里的类模板的使用中,必须要求我们自己手动的显式指定类型,以便编译器能够知道模板参数T的类型,对于类模板而言,不存在类模板的隐式实例化,必须且只能进行类模板的显示实例化,还要注意:上述我们写出的 Stack类 //是类模板,也是模板,当我们执行代码: Stack st1; 和 Stack st2;时,编译器会根据类模板以及手动显式指定的类型自动生成两个类,这两个类中对应的模板参数T分别是整型int类型和双精度浮点型double类型、 return 0; }
问:模板参数 和 函数参数 有什么区别呢?
答:模板参数在用法上和函数参数是很相似的,但两者也存在着一定的区别:
模板参数:调用的时候传的是类型、
函数参数:调用的时候传的是参数的值、
拓展:
#define _CRT_SECURE_NO_WARNINGS 1 #include
using namespace std; //1、 //函数模板的声明和定义不能分离(在不同的文件中),否则会报链接错误,但可以不分离却分开写,如下所示: //函数模板的声明、 template void Swap(T& left, T& right); //函数模板的定义、 template //注意:此时还需要加上模板参数列表、 void Swap(T& left, T& right) { T temp = left; left = right; right = temp; } //2、 //类模板的声明和定义不能分离(在不同的文件中),否则会报链接错误,但可以不分离却分开写,注意,类模板的声明和定义不分离却分开写 //主要是指的类模板中的类成员函数的声明和定义不分离却分开写,如下所示: template class Vector { public: //类模板中的类成员函数在类模板的类体中的声明、 Vector(size_t capacity = 10); private: T* _pData; size_t _size; size_t _capacity; }; //类模板中的类成员函数在类模板的类体外的定义、 template //注意:此时还需要加上模板参数列表、 Vector ::Vector(size_t capacity = 10) : _pData(new T[capacity]) , _size(0) , _capacity(capacity) { cout << sizeof(T) << endl; } int main() { int a = 0, b = 1; Swap(a, b); cout << a <<" "<< b << endl; Vector v1; Vector v2; return 0; } 函数模板的声明和定义与类模板的声明和定义均不可以分离,但是可以不分离却分开写,只要保证在同一个文件中即可,具体分不分开写都是可以的,一般都在头文件中实现,对于涉及到模板(包括函数模板和类模板)的头文件,有的公司使用 .hpp 为后缀,当然使用 .h 也是可以的,只不过没有 .hpp 寓意更好、
注意:所有的链接错误,都是在符号表中找不到所调用的函数的地址导致的、
若函数模板和类模板的声明和定义分离,分离则一定会分开写,即,把函数模板和类模板的声明放在头文件 templat.hpp 头文件中,把函数模板和类模板的定义放在 template.cpp 源文件中时,那么对于 templat.cpp 源文件而言,在进行预处理后产生一个新的 templat.i 文件,然后再经过编译产生一个新的 templat.s 文件,再经过汇编产生一个新的 templat.o 目标文件,则这三个文件中均是空的,因为,虽然在 templat.cpp 源文件中既有函数模板和类模板的声明和定义,但是,在该源文件中,并不知道模板参数 T 的具体类型是什么,对于 test.cpp 源文件而言,这三个过程则是可以的,因为,在该源文件中的模板参数 T 的类型是可以根据 main 函数中的代码得出来的,所以,test.i ,test.s,test.o 这三个文件则都不会出现问题,此时,在 main 函数中,有函数模板和类模板的声明,故不会报编译错误,但需要去找具有特定类型(整型int类型的) Swap 函数,还需要去找具有特定类型,分别为整型 int 类型和双精度浮点型 double 类型的两个类的类体中的构造函数的地址,那么此时就需要去多个目标文件 template.o 和 test.o 文件中去查找所需要的地址,而在生成 templat.o 目标文件的过程中,它里面的符号表是空的,因为在 templat.cpp 源文件中并不知道模板类型 T 的具体类型,该源文件里面均是模板,模板不会被编译成二进制的指令,只有通过模板实例化出来的具有特定类型(各种类型)的函数或者是具有特定类型(各种类型)的类才会被编译成二进制的代码,从而导致 templat.i , templat.s , templat.o 文件中都是空的,那么此时就不能在 templat.o 目标文件中找到所需要调用的函数的地址,所以会报链接错误、
通过以下方法可以解决这个问题,如下所示:
1、Tets.cpp源文件:
#include"template.hpp" int main() { int a = 0, b = 1; Swap(a, b); cout << a << " " << b << endl; Vector
v1; Vector v2; return 0; } 2、template.cpp源文件:
#define _CRT_SECURE_NO_WARNINGS 1 #include"template.hpp" //函数模板的定义、 template
//注意:此时还需要加上模板参数列表、 void Swap(T& left, T& right) { T temp = left; left = right; right = temp; } //类模板中的类成员函数在类模板的类体外的定义、 template //注意:此时还需要加上模板参数列表、 Vector ::Vector(size_t capacity = 10) //指明类域,注意类域后的 、 : _pData(new T[capacity]) , _size(0) , _capacity(capacity) { cout << sizeof(T) << endl; } //类模板中的类成员函数在类模板的类体外的定义、 template //注意:此时还需要加上模板参数列表,不可省略、 void Vector ::PushBack(const T& x) //指明类域,注意类域后的 、 { //... } //解决方法: //显式实例化指定、 //1、 //在该源文件中,当头文件包含之后,既有函数模板的声明,也有函数模板的定义,但是不知道该函数模板中的模板参数 T 的具体类型是什么, //此时可以直接显式实例化指定该函数模板中的模板参数 T 的类型,如下所示: template //注意:此时还需要加上关键字 template、 void Swap (int& left, int& right);//此时再调用具有特定类型(整型int类型)的 Swap 函数就不会再报链接错误了、 //2、 template //注意:此时还需要加上关键字 template、 class Vector ; //3、 template //注意:此时还需要加上关键字 template、 class Vector ; //上述方法可行,但是并不是很好,所以最好使用下面的解决方式: //把函数模板和类模板的声明和定义都写到头文件中,即保证不分离,但要不要分开写,都是可以的,只要保证不分离就行, //此时,在 test.cpp 源文件中包含头文件后,函数模板和类模板既有声明也有定义,并且还能知道他们的模板参数分别是 //什么具体类型,此时直接调用需要的函数即可,也不会涉及到去符号表中查找所调用函数的地址,因为此时函数模板和类 //模板的声明和定义并没有分离(不同文件中),不会涉及到链接问题、 3、template.hpp头文件:
#pragma once #define _CRT_SECURE_NO_WARNINGS 1 #include
using namespace std; //1、 //函数模板的声明、 template void Swap(T& left, T& right); //2、 template class Vector { public: //类模板中的类成员函数在类模板的类体中的声明、 Vector(size_t capacity = 10); //类模板中的类成员函数在类模板的类体中的声明、 void PushBack(const T& x); private: T* _pData; size_t _size; size_t _capacity; };