c/c++/jave 静态语言强类型语言编译型语言,想要在运行以前就能够找出错误。
python是一个弱类型语言,在运行的时候再确定变量的类型。
背景需求:想要在强类型严格的要求下希望能够更加灵活。
有三种方式可以实现:宏定义(不便检错),函数重载,模板
模板:在使用时,通用类型可被具体的类型,如 int、double 甚至是用户自定义的类型来代替。模板引入一种全新的编程思维方式,称为“泛型编程”或“通用编程”。
//希望将类型参数化
//使用class关键字或typename关键字都可以
template <class T>
T add(T x, T y)
{
return x + y;
}
int main(void){
cout << add(1,2) << endl;
cout << add(1.2,3.4) << endl;
string s1 = "xixi";
string s2 = "haha";
cout << add(s1, s2) << endl;
return 0;
}
将类型参数化
函数模板相较于我们以前使用的函数重载不用定义出各种类型的函数,再调用时可以实例化对应的函数。
函数模板在调用的时候,就可以知道参数的类型。
模板发生的时机是在编译时
模板本质上就是一个代码生成器,它的作用就是让编译器根据实际调用来生成代码。
函数模板 --》 生成相应的模板函数 --》编译 ---》链接 --》可执行文件
相比于普通的函数就是多了一步实例化出生成相应的模板函数
模板的定义
将类型也参数化
模板可以分为两类,一个是函数模版,另外一个是类模板。
模板参数是一个更大的概念,包含了类型参数和非类型参数。
在具体的调用的时候根据函数模板可以实例化出模板函数,也可以由类模板实例化出模板类。
上述的模板实例化的时候是一种隐式实例化,下述的模板的实例化是一种显示实例化,模板参数一定会转化为指定的类型。
template <class T>
T add(T t1,T t2)
{ return t1 + t2; }
void test0(){
int i1 = 3, i2 = 4;
cout << "add(i1,i2): " << add<int>(i1,i2) << endl; //显式实例化
}
函数模板的重载
背景需求:因为可能出现同为形参的两个参数的类型也是不同的。就像是上面add(int,double)
【注意】以下在工作中不会出现,出现歧义
template <class T> //模板一
T add(T t1,T t2)
{
return t1 + t2;
}
template <class T1, class T2> //模板二
T1 add(T1 t1, T2 t2)
{
return t1 + t2;
}
这样的返回的类型是T1类型还是有问题。
add<int> (1, 1.2) 这个显示实例化也只是将第一个参数实例化为指定的类型,第二个参数类型不变
区分下列哪些情况调用哪个模板函数
【注意】尽量不要定义可以给同一个函数可以使用多个模板的写法,如果要使用的话也不要进行显示定义,这样很难分析究竟使用了哪一个模板。
double x = 9.1;
int y = 10;
cout << add<int,int>(x,y) << endl; //模板二 (1)
cout << add<int>(x,y) << endl; //模板一 (2)
//第(2)次调用时,指定了返回类型和第一个参数类型为int,那么x会经历一次类型转换变成int型,而y本身就是int,可以匹配模板一;
cout << add<int>(y,x) << endl; //模板二 (3)
//第(3)次调用时,同样指定了返回类型和第一个参数类型为int,y本身就是int,x是double
//类型,匹配模板二,可以不需要进行任何类型转换,所以优先匹配模板二。
//因为如果要是没有模板二的话就只能走模板一再继续对于参数二继续进行转换
在一个模块中定义多个通用模板的写法应该谨慎使用(尽量避免),如果实在需要也尽量使用隐式实例化的方式进行调用,编译器编译器会选择参数类型最匹配的模板(通常是参数类型需要更少转换的模板)。
【了解】下列的情况,不要这样使用
template <class T1, class T2>
T1 add(T1 t1, T2 t2)
{
cout << "模板一" << endl;
return t1 + t2;
}
template <class T1, class T2>
T1 add(T2 t2, T1 t1)
{
cout << "模板二" << endl;
return t1 + t2;
}
int a = 10;
double b = 1.2;
cout << add(a,b) << endl; //error
//冲突不能知道走那一条路
cout << add<int>(a,b) << endl; //模板一
cout << add<double>(a,b) << endl; //模板二
//template <class T1, class T2> 根据这个来所以说在模板二这种情况还是让T1是double类型
函数模板与函数模板重载的条件
1.两个模板名字相同
2.参数顺序不同也可重载
3.模板参数的个数不同重载
template <class T2, class T1>
T1 add(T2 t2, T1 t1)
{
return t1 + t2;
}
template <class T1, class T2, class T3>
T1 add(T1 t1, T2 t2, T3 t3)
{
return t1 + t2 + t3;
}
模板函数与普通函数的重载
这种情况使用普通函数的效率更高会使用普通函数
//函数模板与普通函数重载
template <class T1, class T2>
T1 add(T1 t1, T2 t2)
{
return t1 + t2;
}
short add(short s1, short s2){
cout << "add(short,short)" << endl;
return s1 + s2;
}
void test1(){
short s1 = 1, s2 = 2;
cout << add(s1,s2) << endl; //调用普通函数
}
头文件和实现文件形式
声明和实现可以分离
//函数模板的声明
template <class T>
T add(T t1, T t2);
void test1(){
int i1 = 1, i2 = 2;
cout << add(i1,i2) << endl;
}
//函数模板的实现
template <class T>
T add(T t1, T t2)
{
return t1 + t2;
}
1. 但是注意当在不同的文件中定义的时候,如果是在头文件中写出实现是可以的实现的。
2. 但是如果是在实现文件中写出,需要将模板实例化为两个double函数相加
3. 也可以在头文件中再加一个引用,这样就知道它的实现了
对模板的使用,必须要拿到模板的全部实现,如果只有一部分,那么推导也只能推导出一部分,无法满足需求。
换句话说,就是模板的使用过程中,其实没有了头文件和实现文件的区别,在头文件中也需要获取模板的完整代码,不能只有一部分。
【最终】现在的解决办法是C++的标准库都是由模板开发的,所以经过标准委员会的商讨,将这些头文件取消了后缀名,与C的头文件形成了区分;这些实现文件的后缀名设为了tcc
实现
//temple.cc
#include "add"
#include <iostream>
using namespace std;
int main()
{
cout << add<int>(1,1.2) << endl;
return 0;
}
template <class T1, class T2>
T1 add(T1 a, T2 b);
#include "add.tcc"
template <class T1, class T2>
T1 add(T1 a, T2 b){
return a+b;
}
这种情况下就不用再进行联合编译,因为实际上就相当于将实现文件也定义头文件中,是c++定义的一种特殊的书写方式。
模板的特化
背景需求:通用模板不能使用的时候可以使用特化模板。
就像是上面的例子不能实现char*类型的+,可以采用的方法有可以指定为<string>这样就会进行类型转换。
//特化模板
//这里就是告诉编译器这里是一个模板
template <>
const char * add<const char *>(const char * p1,const char * p2){
//先开空间
char * ptmp = new char[strlen(p1) + strlen(p2) + 1]();
strcpy(ptmp,p1);
strcat(ptmp,p2);
return ptmp;
}
void test0(){
//通用模板无法应对如下的调用
const char * p = add<const char *>("hello",",world");
cout << p << endl;
delete [] p;
}
【注意】
1.如果没有相对应的通用模板(函数名参数类型),特化模板在能够定义
2. 再使用特化模板的时候最好写成上述的形式,不是使用隐式的方式
使用模板的规则
1. 一个函数可以调用多个模板均可实现的时候谨慎使用
2. 尽量使用隐式转换,不然不知道调用的是哪一个模板
3. 尽量不要指定模板转换的类型,也会造成不知道是哪一个函数
4. 使用特化模板的时候,使用固定的格式声明。
模板的参数类型
1.类型参数
class T类型可以被推导
2.非类型参数
整型数据不能是浮点型数据char/short/int/long/size_t,不能是float/double
因为是在编译的阶段确定的
template <class T,int kBase>
T multiply(T x, T y){
return x * y * kBase;
}
void test0(){
int i1 = 3,i2 = 4;
//此时想要进行隐式实例化就不允许了,因为kBase无法推导
cout << multiply(i1,i2) << endl; //error
cout << multiply<int,10>(i1,i2) << endl; //ok
}
当然可以有默认值,类型化和非类型化参数都可以。
template <class T = int,int kBase = 10>
T multiply(T x, T y){
return x * y * kBase;
}
void test0(){
int i1 = 1.2,i2 = 1.2;
cout << multiply<int,100>(i1,i2) << endl; //100
cout << multiply<int>(i1,i2) << endl; //10
cout << multiply(i1,i2) << endl; //14.4
}
根据上述的三个例子
优先级:指定的类型 > 推导出的类型 > 类型的默认参数
但是那再什么时候或使用到默认值情况,是在既没有指定,又不能推导的时候,使用默认类型。
往往是在单独设置返回类型的时候
template <class T1 = double,class T2 = double,class kBase = 10>
T1 multiply(T2 t1, T2 t2){
return t1 * t2 * kBase;
}
cout << multiply(1.2,1.2) << endl; //ok
成员函数模板
class Point
{
public:
Point(double x,double y)
: _x(x)
, _y(y)
{}
template <class T>
T add(T t1)
{
return _x + _y + t1;
}
private:
double _x;
double _y;
};
void test0(){
Point pt(1.5,3.8);
cout << pt.add(8.8) << endl;
}
普通成员函数指针也是有this指针的。
同理static成员函数,就没有this指针,只能访问静态数据成员。
成员函数模板 不能设为 virtual,成员函数模板起效在编译阶段,虚函数实现是在运行时候。
类模板
类模板定义的是一类类
存放任意元素类型的类
template <class T = int, int kCapacity = 10>
class Stack
{
public:
Stack()
: _top(-1)
, _data(new T[kCapacity]())
{
cout << "Stack()" << endl;
}
~Stack(){
if(_data){
delete [] _data;
_data = nullptr;
}
cout << "~Stack()" << endl;
}
bool empty() const;
bool full() const;
void push(const T &);
void pop();
T top();
private:
int _top;
T * _data;
};
int main(){
Stack<> st;
}
这个时候已经有默认值,但是也是需要加上<>
可变参数模板
对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。
例如:printf函数的参数个数可能有很多个,用...表示,参数的个数、类型、顺序可以随意,可以写0到任意多个参数。
printf函数的参数个数可能有很多个,用...表示,参数的个数、类型、顺序可以随意,可以写0到任意多个参数。
template <class ...Args>
void func(Args ...args);
//普通函数模板做对比
template <class T1,class T2>
void func(T1 t1, T2 t2);
利用可变参数模板输出参数包中参数的个数
template <class ...Args>//Args 模板参数包
void display(Args ...args)//args 函数参数包
{
//输出模板参数包中类型参数个数
cout << "sizeof...(Args) = " << sizeof...(Args) << endl;
//输出函数参数包中参数的个数
cout << "sizeof...(args) = " << sizeof...(args) << endl;
}
void test0(){
display();
display(1,"hello",3.3,true);
}
当类型相同的时候,也并不会减少类型个数,还是和参数个数相同,因为最多有这些个类型,这样就不会有可能导致类型的缺少。
//递归的出口
void print(){
cout << endl;
}
//重新定义一个可变参数模板,至少得有一个参数
//不断递归到最后就没有相应的函数可以调用,因为没有可以适用于无参的,所以
//定义一个优先级比较高的无参的函数来实现
template <class T,class ...Args>
void print(T x, Args ...args)
//有多少个参数个数有多少个类型个数与之对应
{
cout << x << " ";
print(args...); //省略号在参数包右边
//这样就会继续继续解包
}
设置不同的出口,如果最后的是int,就会从int直接输出,因为只剩一个参数且是int类型
void print(){
cout << endl;
}
void print(int x){
cout << x << endl;
}
template <class T,class... Args>
void print(T x, Args... args)
{
cout << x << " ";
print(args...);
}
print(1,"hello",3.6,true,100);
想要获取所有类型的元素的出口
【注意】1. 递归的出口最好使用普通函数作为递归出口(因为普通函数优先级更高,不会出现函数重载情况)
2. 这个地方也可以两个两个或者三个参数一块解包,但是需要准备只有一个参数或者无参数的出口。
获取参数的类型(了解)
void printType(){
cout << endl;
}
//重新定义一个可变参数模板,至少得有一个参数
template <class T,class... Args>
void printType(T x, Args... args)
{
cout << typeid(x).name() << " ";
printType(args...);
}
printType(1,"hello",3.6,true,100);