文章目录
- 泛型编程
- 函数模板
- 函数模板实例化
- 隐式实例化
- 显式实例化
- 函数模板的匹配规则
- 类模板
- 类模板的实例化
泛型编程
泛型编程旨在削减重复工作,如:
- 将一个函数多次重载不如将他写成泛型。
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函数,虽然c++支持重载,但是如果对于这种函数我们每一个都要自己去实现,是一件很麻烦并且多余的工作,因为我们将重复逻辑的东西多次实现。
- 比如我们想向一个封装好的类添加新的数据成员,重新写一个不如一开始就写成泛型。
class Stack
{private:int capacity;int size;int* arr;
};
如果我们实现了一个栈,如果他此时要存储其他的数据类型,那我们就必须还要重新修改一个,这无疑是繁琐的。
所以,C++
基于重载这一项机制,实现了模板编程,模板作为类或者函数的蓝图,可以针对不同类型,来实现对应操作。
函数模板
函数模板与类型无关,被调用时根据实参类型产生函数的对应类型版本。
template<typename T1, typename T2,......,typename Tn>
//typename即类型名,也可以用class替代,这里的class指的是类型,不能用struct替换
编写函数模板时,只需要在对应的函数前面加上上面的语句即可,然后将所有需要替换类型的参数改为上面的 T1、T2
即可
template<class T>
void Swap(T& left, T& right) {T temp = left;left = right;right = temp;
}int main()
{int i = 3, j = 4;double a = 3.4, b = 5.6;char x = 'x', y = 'y';Swap(i, j);Swap(a, b);Swap(x, y);cout << i << ' ' << j << endl;cout << a << ' ' << b << endl;cout << x << ' ' << y << endl;
}
当我们调用这个模板函数的时候,是生成一个函数重载并调用三次呢?还是生成三个返回值不同的函数各调用一次呢?
通过地址信息可以看到,它调用的是三个不同的函数。
因此,模板函数本身并不是一个函数,而是一个蓝图,通过识别我们传入的参数,然后在底层生成一个该类型的模板函数,并调用该函数。
并且这个阶段是在预处理的时候就进行了,因为如果它是在编译阶段才进行函数的生成,那肯定是无法通过语法的检查的,所以这一阶段只能在预处理进行。
函数模板实例化
函数模板只在第一次使用的时候才会实例化出来,它的实例化有两种方法,一种是显式实例化,一种是隐式实例化。
隐式实例化
template<class T> T Add(const T& left, const T& right)
{return left + right;
}int main()
{int i = 3, j = 4;double a = 3.4, b = 5.6;Add(i, j);Add(a, b);/*Add(i, a);这时编译器就会报错,因为需要通过参数推演出对应类型,但是此时就无法推演出到底是将 T 推演成 double 还是将 T 推演成 int*/// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化Add(i, (int)a); // 强制转化,将 T 推演成 intreturn 0;
}
显式实例化
int main() {int i = 3;double x = 3.4;// 显式实例化Add<int>(i, x);return 0; }
用尖括号来显式的声明,STL
中的容器等就是采用这种显式的实例化来明确参数的类型。
函数模板的匹配规则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。这时如果使用隐式实例化,则会调用非模板函数;如果使用显式实例化,则调用模板函数。
//非模板函数
int Add(int left, int right)
{return left + right;
}template<class T>
T Add(T left, T right)
{return left + right;
}int main()
{Add(1, 2); // 调用非模板函数Add<int>(1, 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函数
}
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
类模板
类模板的用法和函数模板其实一样
template<class T>
class Stack
{private:int capacity;int size;T* arr;
};
这里的 Stack
不是一个具体的类,他是类模板,也就是一个蓝图,通过这个模板来识别参数的类型并生成对应的模板类。
所以可以理解为类模板是一个类家族,模板类是通过类模板实例化的具体类。
如果类中的成员函数需要在类外定义的话,需要每一个定义前都要声明一次类模板的参数列表。
template<class T>
class Stack
{
public:void push();void pop();
private:int capacity;int size;T* arr;
};template<class T>
void Stack<T>::push()
{}template<class T>
void Stack<T>::pop()
{}
还有一个需要注意的地方就是,类模板的所有成员函数都是模板函数。
类模板的实例化
类模板只能够通过显式实例化
如 STL
中的几个容器都是通过类模板实现的:
stack<int> s1;stack<double> s2;
stack<int>
并不是类名,而是显式实例化指定的类型,stack
才是类名。