泛型编程
在一个项目中,我们可能需要交换不同类型的数据。虽然C++支持函数重载,解决了C语言中函数名不能相同的问题,但是代码复用率任然极低
void Swap(int& ra, int& rb) { int tmp = ra; ra = rb; rb = tmp; } void Swap(double& c, double& d) { double tmp = c; c = d; d = tmp; } int main() { int a = 10, b = 20; Swap(a, b); cout << a << " " << b << endl; //但是如果我们要交换浮点数类型,就要重新写一个函数 double c = 12.1, d = 13.2; Swap(c, d); return 0; }
为了提高编写效率,C++引入了一个叫做泛型编程的概念,所谓泛型编程就是编写与类型无关的通用代码,模板是泛型编程的基础。
函数模板
1.函数模板的使用
函数模板与普通函数编写几乎没有很大的区别,只是用一个泛型来代表函数的类型,一个函数模板代表的是一个函数家族,不受类型限制
template<typename T>//这里的typename可以使用class来代替,这个T也可以用其他字符来代替 // 不过typename的可读性会有一点提高,因为typename本身就是类型名称的缩写 void Swap(T& left, T& right) { T tmp = left; left = right; right = tmp; } int main() { int a = 10; int b = 20; Swap(a, b); cout << a << " " << b << endl; double c=10.2,d=13.3; Swap(c,d); cout<<c<<" "<<d<<endl; return 0; }
那么这里有个问题,交换整形和浮点型的函数是从哪来,是调用的模板吗?
地址不同也就是表明它们调用的不是同一个函数,所以说它们并不是通过调用函数模板来解决问题的,而是调用的函数模板根据传参的类型经由编译器推演以后实例化出来的函数 。
2.不同类型的传参处理
1.强制类型转换
既然函数模板是编译器根据我所传的参数自动推演而来,那么一个函数模板是否可以处理两个不同类型的参数呢?以如下代码为例:
template<typename T> T Add(T& left, T& rigth) { return left + right; } int main() { int a = 10, b = 20; cout<<Add(a, b)<<endl; double c = 10.0; cout << Add(a, (int)c) << endl; return 0; }
为啥我在这里强制类型转换(double转到int)也不行?这里可以参考前面说过的隐式类型转换,在强制类型转换的过程中,中间产生了一个临时变量,这个临时变量具有常性,而上面所写的Swap函数参数并没有加const,也就是说有权限放大的风险(只有指针和引用才会涉及到权限) 。所以只要对参数加上const就可以使这段代码成功跑过:
2.显示实例化
除了强制类型转换以外,还可以在传参时对模板的参数显示实例化明确的告诉编译器应当产生什么类型的函数,这个时候如果传参是两个不同类型,就会发生隐式类型转换,隐式类型转换,转换的过程中会产生一个临时变量,而这个临时变量具有常性,所以代码也要加const修饰
template<typename T> T Add(const T&left,const T&right) { return left + right; } int main() { int a = 10, b = 20; double c = 10.0; cout << Add<int>(a, c) << endl; cout << Add<double>(c, b) << endl; return 0; }
3.多参数模板
上述的模板中,只使用了一个模板参数,所以也就是一个函数模板只能同时对一个类型进行推演,但是如果在函数模板中使用多个参数,自然就可以同时对不同的类型进行推演:
template <typename T1,typename T2> T1 Add(T1& left, T2& right) { return left + right; } int main() { int a = 10, b = 20; double c = 10.2, d = 20.3; cout << Add(a, b) << endl; cout<<Add(a, c)<<endl; return 0; }
可以看到当我在Add函数使用两个模板参数时对于不同类型参数的传参不用我做任何处理,编译器有足够的泛型参数对两个不同的类型进行推演,不过返回值还是只能是两个类型中的一个。
3.模板可以和实例函数同时存在,编译器优先调用实例函数
template <typename T1,typename T2> T1 Add(T1& left, T2& right) { return left + right; } int Add(int left, int right) { return left + right; } int main() { int a = 10, b = 20; double c = 10.2, d = 20.3; cout << Add(a, b) << endl; cout<<Add(a, c)<<endl; return 0; }
1.可以看到模板函数和实例函数同时存在编译器并不会报错,这是因为模板函数和实例函数的函数名修饰规则不同。
2.模板函数和实例函数同时存在时,编译器优先调用实例函数
3.如果模板可以生成更匹配的函数,则选择模板函数
可以看到这里因为两个参数的类型不同,所以编译器选择了模板函数。
类模板
在之前我们写一个类就只能实例化出一个类型的类,尽管可以通过typedef来获得一些便利,但是当我们同时需要多个类型的类时,就会存在大量的重复代码,为了解决这个问题,类模板应运而生。
template<typename T> class Stack { public: Stack(int capacity = 4) :_capacity(capacity) , _top(0) { _a = (T*)malloc(sizeof(T) * _capacity); if (_a == NULL) { perror("malloc fail\n"); exit(-1); } } ~Stack() { free(_a); _a = NULL; _top = _capacity = 0; } void Push(T x) { _a[_top++] = x; } private: T* _a; int _top; int _capacity; };//当然这个栈并不完善但是在这里讲解也足够使用
1.类模板需要显示实例化
int main() { Stack<int> st; st.Push(1); Stack<double> stt; st.Push(2.3); }
对于函数模板编译器可以根据传参来自动推演类型,但类模板没有推演时机所以必须要我们显示实例化。
模板使用参数不同,对于类而言就是不同的类型,也就是说st!=stt。
2.类模板不能声明定义分离
类模板如果定义和声明分离就会出现链接错误:因为类模板没有推演时机必须要我们显示实例化,如果将定义和声明分离就会出现在定义的地方没有实例化,而在使用的地方虽然有实例化但是没有定义。
总之就是我在Test.cpp文件中实例化了该模板并调用,但是向上查找却未找到定义,因此就发生了链接错误。
解决办法:
1.实例化的地方没有定义我们不能增加定义否则代码冗余,那就让定义的地方实例化:
template class Stack<int>//显示实例化为整形,可以放在任意位置
2.不将声明和定义分离,全部放在.h文件中(因为该文件中含有定义,所以有些人又将该文件叫.hpp)
非类型模板参数
C语言通过宏来定义数组大小已经是最方便的静态数组了,尽管如此在我们同时需要多个数组时它们的大小和类型都是一样的,但C++可以通过类型参数和非类型参数联合来达到获得不同类型和大小的数组。
template<typename T,size_t N> //T是类型参数,N是非类型参数 class Array { private: T _Array[N]; }; int main() { Array<int, 10> arr1; Array<double, 20> arr2; return 0; }
不过非类型模板参数只支持整型常量,浮点型、变量、类对象等都不行。