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;
}
但是真的很方便吗?这里只有三种类型的数据需要交换,若是我们需要增加交换的数据呢?再CV然后写一个函数吗?
这样,我们先通过一个案例来做引入
所以,总结上面的这么一个技术,C++的祖师爷呢就想到了【模版】这个东西,告诉编译器一个模子,然后其余的工作交给它来完成,根据不同的需求生成不同的代码
这就是泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础
知晓了模版的基本概念后,首先我们要来看的就是【函数模版】
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
通过函数模板,可以编写一种通用的函数定义,使其能够适用于多种数据类型,从而提高代码的复用性和灵活性。
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;
}
Swap()
函数,函数模版都可以进行自动的识别
那我现在想问一个问题,请问它们调用的真的是同一个函数吗?
接下去我们来说说这个函数模版的原理,带你理清编译器内部究竟做了什么事情
还是不太懂的老铁可以看看下面这张图,就能明白了
int
类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int
类型,然后产生一份专门处理int
类型的代码,对于浮点类型、字符型也是如此 那我现在还想问,如果我使用的是两个日期类Date
的对象呢,能不能对它们进行交换
既然谈到了这个【Swap】交换函数,我们就顺便来说说库里的这个【swap】
swap()
函数了,直接用库里的即可上面我们所定义的都是单个模版参数,那多个模版参数是否可以定义呢?
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
好比你做错事了,你爸爸让你罚站,不让你吃饭,此时呢你妈妈回来了,让你赶紧过来吃饭,那此时你该听谁的呢?
那当他们商量好了之后,就会有下面这两种情况
int
类型然后再传递进去,此时就不会出现类型冲突的问题了cout << Add(a1, (int)d2) << endl;
double
类型然后再传递进去,也不会出现类型冲突的问题cout << Add((double)a1, 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的加法函数
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;
}
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;
};
如果没有模版技术的话你会如何去解决这个问题呢?很简单那就是定义多个类
StackInt
存放整型数据,StackDouble
存放浮点型数据class StackInt
class StackDouble
但是本文我们重点要讲解的就是【模版技术】,技术界有一句话说得好 “不要重复造轮子”
template<class T>
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(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;
};
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,那么这个函数也要变为函数模版才行::
这个操作符我们在C++命名空间中有提到过,叫做【域作用限定符】,是我们使用命名空间去访问特定成员变量或成员函数时使用的,对于类来说它一定要是一个类名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
、Stack
这些都是它的类型int main(void)
{
Stack<int> s1; // int
Stack<double> s2; // double
Stack<char> s3; // char
return 0;
}
最后我们来总结一下本文所学习的内容
template
,用它再配合模版参数就可以去定义出一个函数模版,有了它,我们在写一些相同类型函数的时候就无需去进行重复的CV操作了,在通过汇编观察函数模版的原理后,清楚了我们只需要传入不同的类型,此时模版参数就会去进行一个自动类型推导,从而产生不同的函数。函数模版定义好后还要对其实例化才能继续使用,但此时要注意的一点是如果传递进去的类型个数与模版参数的个数不匹配的话,其就无法完成自动类型推导,因为这会产生一个歧义。所以想要真正学好模版,这点是一定要搞清楚的!!!Stack
类为例,对于类模版而言,其类名和类型与普通类是不一样的,这点要注意了,尤其体现在类的成员函数放在类外进行定义的时候,也要将其定义为函数模版,函数名前面指明其类型,这才不会出问题。有了类模版之后,我们去显式实例化不同的数据类型后也可以让模版参数去做一个自动类型推导从而得到不同数据类型的栈以上就是本文要介绍的所有内容,感谢您的阅读