目录
- 泛型编程思想
- 问题
- 函数模板
- 函数模板的概念
- 函数模板的格式
- 函数模板的原理
- 函数模板的实例化
- 模板参数的匹配原则
- 类模板
- 类模板的定义格式
- 类模板的实例化
泛型编程思想
问题
- 有的时候,我们在做大项目的过程中,难免会对差不多功能的函数因为类型不同等原因需要重复实现差不多功能的函数。
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;
}
- 这时候我们就可以引进一个新东西叫做模板,他的原理类似我们中国古代的活字印刷术,如我们现在有一份离骚的诗作,现在很多人需要这个诗作,如果我们一份一份的抄那就很浪费时间,不如我们刻在一个石头上,用石头一沾墨,我们就可以拿一张纸,把他刷在纸上,这样就完成了。
函数模板
函数模板的概念
- 函数模板与类型无关,他在使用该模板的时候确定参数的类型,并且实例化出一个函数。
函数模板的格式
- 然后正式来声明一个函数模版,这里我们要学一个新的关键字叫template,接下去加一个<>尖括号,内部就是模版的参数了,可以使用typename或者class来进行类型的声明(不可以用struct),若是只有一个类型的话我们一般会叫做【T】,不过这个没有强制要求
template<typename T1, typename T2,......,typename Tn>
- 马上,我们就来为上面的swap()函数写一个通用的函数模版把
template<typename T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}
- 然后来用一用这个函数模版,分别传入不同数据类型的参数,通过结果的观察可以发现这个函数模版可以根据不同的类型去做一个自动推导,继而去起到一个交换的功能
- 不过,需要注意的事,我们不是调用的同一个函数。就从函数栈帧的角度来说,开辟char类型和int类型的参数,开辟的空间大小完全不一样。
- 不信我们可以看看下面的原理
函数模板的原理
- 对于函数模板而言其实是一个【蓝图】,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器,让我们来看看编译器做了什么
- 在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然后产生一份专门处理int类型的代码,对于浮点类型、字符型也是如此
- 我现在还想问,如果我使用的是两个日期类Date的对象呢,能不能对它们进行交换那
- 答案是可以的,int、double、char这些都是内置类型,Date呢是自定义类型,很明显它们都是类型,我们所定义的模版参数也是类型(这里不是类模板的意思,类模板与之不同),那为何不可去做一个接受呢?从运行结果可以来看,确实是可以发现它们也发生了一个交换
- 既然这里说到了Swap函数,其实C++对于这种经常用的函数已经在库进行了封装实现。就是我们的swap(注意这里的s是小写)
- 这里我们看一下函数的文档
函数模板的实例化
上面我们所定义的都是单个模版参数,那多个模版参数是否可以定义呢?
template<typename T1, typename T2>
T1 Func(const T1& x, const T2& y)
{cout << x << " " << y << endl;
}
👉 用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化
- 隐式实例化:让编译器根据实参推演模板参数的实际类型
- 知道模版可以定义多个参数之后,其返回值也可以是模版参数
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
- 下面这种就是【隐式实例化】,让编译器根据实参自动去推导模板参数的实际类型,然后返回返回不同类型的数据
int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;// 根据实参传递的类型,推演T的类型cout << Add(a1, a2) << endl;cout << Add(d1, d2) << endl;
}
- 但是呢像下面这种就不可以了,因为a1是【int】类型,d1是【double】类型,在编译期间,当编译器看到该实例化时,需要推演其实参类型 通过实参a1将T推演为int类型,通过实参d1将T推演为double类型,但模板参数列表中只有一个T, 编译器无法确定此处到底该将T确定为int 或者 double类型而报错。此时在函数调用完后进行返回时,编译器也识别不出是哪个类型了,它们两个就像在打架一样,很难一绝高下
cout << Add(a1, d2) << endl
对于这种情况,我们给出两种解决方案:
- 第一种强制类型转换其中一个参数的类型,这样就可以保证两个类型相同了
cout << Add(a1, (int)d2) << endl
- 第二种则是要用到我们的显示实例化,
// 显式实例化,用指定类型实例化
cout << Add<int>(a1, d2) << endl;
cout << Add<double>(a1, d2) << endl;
虽然上面我们介绍了两种处理方式,但是对于某些场景来说,却只能进行【显式实例化】
- 例如下面我写的一个模板函数,他的形参部分不是模板参数,而是一个内置类型的参数,那么我们在使用这个函数模板实例化出对应类型的函数时,编译器就不能自动匹配类型,其原因就是模板函数的形参不是模板参数类型,而是内置类型,这时候我们就不能通过实参传形参来确定这个函数,这时候我们就使用显示实例化可以确定类型了。
template<class T>
T* Alloc(int n)
{return new T[n];
}
- 例如下面的这些,我们想开什么数据类型的空间,只需要显示指定类型即可
// 有些函数无法自动推,只能显示实例化
double* p1 = Alloc<double>(10);
float* p1 = Alloc<float>(20);
int* p2 = Alloc<int>(30);
模板参数的匹配原则
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
- 可以看到,我在下面写了一个专门处理int的加法函数,为普通的函数,又写了一个函数模版,它们是可以进行共存的,在进行普通传参的时候,就会去调用这个普通的Add函数;若是显式指明了类型的话,就会去调用这个函数模版让编译器生成对应的函数
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}// 通用加法函数
template<class T>
T Add(T left, T right)
{return left + right;
}int main(void)
{Add(1, 2);Add<int>(1, 2);return 0;
}
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
- 也是和上面类似的代码,不过对于函数模版这一块我使用到了两个模版参数,就是为了匹配多种数据类型
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{return left + right;
}int main(void)
{Add(1, 2);Add(1, 2.2);return 0;
}
- 观察调试结果我们可以发现Add(1, 2)优先去调了普通的加法函数,因为传递进去的是两个【int】类型的参数,完全吻合;但是呢对于第二个Add(1, 2.2)来说,却去调用了函数模版,因为第二个参数是【double】类型,普通的函数它也接不住呀,此时模版参数就可以根据这个类型来进行一个自动推导
模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
- 首先对于普通函数而言很好理解,看到print函数的形参所给的类型为【int】,但是在外界传入了一个【double】类型的数值,如果你学习过 隐式类型转换 的话,就可以知道这个浮点数传入的话会发生一个转换,这就叫做【自动类型转换】
// 普通函数,允许自动类型转换
void print(int value) {std::cout << "Integer: " << value << std::endl;
}int main() {print(3); print(3.14); return 0;
}
- 但是呢,对于模版函数来说是无法进行自动类型转换的,例如下面这个,我为这个函数模版定义了一个模版参数,但是在外界进行传递的时候却传递进来两种数据类型,为【int】或【double】,那么一个模版参数T就使得编译器无法去进行自动推导
template <class T>
void print(T a, T b) {cout << a << " " << b << endl;
}int a = 1;
double b = 1.11;print(a, b);
- 这个其实我们在上面也讲到过,再来回顾一下,改进的方法有两种,一个是【强制类型转换】,还记得罚站的事情吗;另一个则是【显式实例化】,还记得我们看的汇编吗
// 强制类型转换
print(a, (int)b);
print((double)a, b);
// 显式实例化
print<int>(a, b);
print<double>(a, b);
- 其实还有一种改进的方法,那就是增加模版参数,因为一个模版参数接收两种类型是无法进行自动推导的,此时若是有两个模版参数的话就可以接收两种类型了,不会出现错误
类模板
类模板的定义格式
- 先来看到的就是其定义格式,函数模版加在函数上,那对于类模版的话就是加在类上
template<class T1, class T2, ..., class Tn>
class 类模板名
{// 类内成员定义
};
我们以下面这个Stack类为例来进行讲解
如果你学习了模版的相关知识后,一定会觉得这个类的限制性太大了,只能初始化一个具有整型数据的栈,如果此时我想要放一些浮点型的数据进来的话也做不到
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
- 下面就是使用模版去定义的一个类,简称【模板类】,不限制死数据类型,将所有的DataType都改为【T】
template<class T>
class Stack
{
public:Stack(size_t capacity = 3){_array = (T*)malloc(sizeof(T) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(T data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:T* _array;int _capacity;int _size;
};
- 但是这不是我们在C++中规范的写法,我们通常规范的写法就是,定义和声明文件分离进行编写。
template<class T> // 类模版
class Stack
{
public:Stack(size_t capacity = 3);void Push(T data);~Stack();
private:T* _array;int _capacity;int _size;
};
- 不过呢可以看到直接像我们之前那样去进行类外定义似乎行不通,说缺少类模版“Stack”的参数列表,因为这个成员函数内部也使用到了模版参数T,那么这个函数也要变为函数模版才行
- 但是加上模板参数也不行,还是提示我们缺少模板的参数列表,这其实是Stack这个函数也要使用我们的模板,那他就应该是属于模板的模板函数。就像类的成员函数也要用::指定类域一样。
- 这里强调一下,对于普通类来说,类名和类型是一样的,所以我们可以直接使用类名来指定类域。但是函数模板的模板名和模板类型可不一样,这里我们需要的是函数模板的类型,而不是模板名,还记得函数模板的类型吗?就是我们显示实例化的类型这个模板类的类型即为Stack
- 以下即是对这个模版类中的成员函数在类外实现所需要变化成的模版函数
template<class T>
Stack<T>::Stack(size_t capacity)
{_array = new T(capacity);_capacity = capacity;_size = 0;
}
- 那么对于其他函数也一样,在使用模板之前都要定义模板参数,因为上一个模板参数的作用域只管理到上一个模板函数,如果我们这个函数想要用函数模板就需要再次定义模板参数
template<class T> // 每个函数或类前都要加上其对应的模版参数
void Stack<T>::Push(T data)
{// CheckCapacity();_array[_size] = data;_size++;
}template<class T>
Stack<T>::~Stack()
{if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}
}
类模板的实例化
👉 类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类
- 可以看到因为我们将这个类定义为了类模版,此时便可以去初始化不同数据类型的栈了,上面说到过Stack是类名,但是像Stack、Stack这些都是它的类型