C++模板(初识)
一、泛型编程
我们平时写交换函数的时候,会这样写:
//交换两个int类型变量
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
//交换两个double类型变量
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
//交换两个char类型变量
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
这样写相比于C语言已经很方便了,因为C++支持函数重载和引用。
使用函数重载虽然可以实现,但是有几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增
加对应的函数。- 代码的可维护性比较低,一个出错可能所有的重载均出错。
但这样写又会感觉怪怪的,它们的大体框架一摸一样,实现的功能也一摸一样,就是要交换的变量类型不同,有没有一种方式能解决这种问题?
这时候模板就应运而生了,编译器会根据不同的类型利用该模板来生成相应代码。有函数模板和类模板两种。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
二、函数模板
1、函数模板格式
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生
函数的特定类型版本。
函数模板格式:
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
其中, T1,T2...是一种类型,具体个数自己设定。
对于上述的代码,我们可以写一个函数模板:
template<typename T>
void Swap( T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替
class)。
这样一个模板就可以解决上述出现的问题:
template<class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int i = 1, j = 2;
cout << "Swap Before:" << i << " " << j << endl;
Swap(i, j); //调用时,T会是int类型
cout << "Swap After:" << i << " " << j << endl;
double m = 1.1, n = 2.2;
cout << "Swap Before:" << m << " " << n << endl;
Swap(m, n); //调用时,T会是double类型
cout << "Swap After:" << m << " " << n << endl;
return 0;
}
2、函数模板原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
所以其实模板就是将本来应该我们做的重复的事情交给了编译器。简单来说就是原本我需要写3个函数,现在我只写了1个模板,调用时,其实本质上还是调用了3个函数,只是那是编译器的工作,它会帮助我们来进行具体调用,我们只用写1个模板就行,减少了我们的工作量,增加了编译器的工作量而已,当然,编译器不会喊"累"的。
在调试过程中,它们Swap(i, j);Swap(m, n);都会走模板,但实质上它们走的不是同一个函数,我们通过汇编代码可以看出:
进而说明了是编译器帮助我们生成各自的函数。像下面这样:
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用(调用的不是同一个函数),比如:当用double类型使用函数模板时,编译器通过对实参类型的推演, 将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。用函数模板生成对应的函数这一过程被称为模板的实例化。
T到底是什么类型是根据实参传递给形参是推导出来的。
上述举例中函数模板只有一个参数T:
template<class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int i = 1, j = 2;
double m = 1.1, n = 2.2;
Swap(i, j);//ok
Swap(m, n);//ok
Swap(i, m);//err,参数模板只有一个类型,传参时就不能传2个不同类型的变量
//因为编译器不知道T到底是int类型还是double类型
return 0;
}
函数模板也可以有多个参数:
template<class T1, class T2>
void Func(T1& x,T2& y)
{
//...
}
int main()
{
int i = 1;
double j = 1.1;
Func(i, j); //这里就能判断出T1是int类型,T2是double类型
return 0;
}
3、单参函数模板可能遇到的情况
单参函数模板若传两个不同类型的变量就会报错,因为编译器分不清函数模板中的参数类型到底是哪一个。
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
Add(a1, a2); //这没问题,因为a1和a2的类型一样
Add(d1, d2); //这也没问题,d1和d2的类型一样
Add(a1,d1); //err,这里就会报错,因为编译器不知道T到底是int类型还是double类型
return 0;
}
那有什么办法可以解决这种问题呢?
(1)强制类型转换
将两个不同类型的变量,其中一个强制转换成另一个,这样编译器就可以推导出T的类型了,这个过程就是推导实例化。
//推导实例化
cout << Add(a1, (int)d1) << endl;
cout << Add((double)a1, d1) << endl;
(2)显示实例化
//显示实例化
cout << Add<int>(a1, d1) << endl;
cout << Add<double>(a1, d1) << endl;
这段代码的意思就是,直接显示说明T的类型,第一行T的类型是int,d1是double类型会走隐式类型转换(转换成int类型),第二行T的类型是double,a1是int类型也会走隐式类型转换(转换成doubel类型)。
(3)改为两个参数的函数模板(具体几个参数看题目要求)
template<class T1,class T2>
T1 Add(const T1& a, const T2& b)
{
return a + b;
}
这样就不会有歧义了。
有时候必须用显示实例化,比如:
template<class T>
T* func(int n)
{
return new T[n];
}
int main()
{
func(3);
return 0;
}
这种情况,推导不出来T是什么类型。必须用显示实例化,说明T的类型。
double* p1 = func<double>(10);//这里必须显式实例化,T是double类型
4、函数模板和具体函数同时存在
如果函数模板和具体函数同时存在,会先调用哪一个?
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int Add(const int& a, const int& b)
{
return (a + b) * 10;
}
int main()
{
int a1 = 10, a2 = 20;
cout << Add(a1, a2) << endl;
return 0;
}
运行结果:
编译器会优先调用具体的函数。俗话说的好,能省即省,我放着现成的不用为什么要走模板二次加工呢?
三、类模板
1、类模板格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
}
这里的class也可以用typename,其它的和函数模板差不多。
我们先来写一个类模板:
//类模板
template<typename T>
class Stack
{
public:
Stack(int n = 4)
:_array(new T[n])
, _capacity(n)
, _size(0)
{
}
~Stack()
{
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
void Push(const T& x)
{
if (_size == _capacity)
{
//手动扩容
T* tmp = new T[_capacity * 2];
memcpy(tmp, _array, sizeof(T) * _size);
delete[] _array;
_array = tmp;
_capacity *= 2;
}
_array[_size++] = x;
}
private:
T* _array; //栈开辟空间的起始指针
int _capacity; //栈的容量
int _size; //栈中元素个数
};
这个模板的主要功能是,模拟一个栈,写了一个Push成员函数用来向栈顶添加元素。栈中的成员类型是T。
我们要实现的是动态栈,即容量不够会自动扩容,自动扩容的机制需要我们自己写。
什么时候要判断容量是否满了?
这里其实就是Push时,像栈顶添加元素前要判断栈中容量是否能装下这个元素。
在C语言中有realloc函数支持扩容,但C++中可没有,我们可以手动写一个:
if (_size == _capacity) { //手动扩容 T* tmp = new T[_capacity * 2]; //扩为原来的2倍 memcpy(tmp, _array, sizeof(T) * _size); //拷贝原来的内容到新空间 delete[] _array; //释放原有空间 _array = tmp; //_array再次成为指向空间首位置的指针 _capacity *= 2; //容量扩2倍 } _array[_size++] = x; //在栈顶添加新元素同时_size需要加1
进入Push,首先要判断是否需要扩容,扩容时开辟原来两倍的空间(没有定性,一般都是2倍),再用memcpy函数将原有的空间的内容拷贝到新开辟的空间,释放原有的空间。这就是大致过程。
2、类模板实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的
类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
int main()
{
//类模板都是显示实例化
Stack<int> st1; //栈中的成员都是int类型
st1.Push(1);
st1.Push(2);
st1.Push(3);
Stack<double> st2; //栈中的成员都是double类型
st2.Push(1.1);
st2.Push(2.2);
st2.Push(3.3);
return 0;
}
为什么必须显示实例化呢?
因为如果不用显示实例化,模板中的T根本推导不出来是什么类型,那这个模板还有什么用处呢?
所以必须要显示实例化。
3、类模板中函数声明和定义分离
在同一文件下,类模板中,我们也可以让其中的函数的声明和定义分离:
//类模板
template<typename T>
class Stack
{
public:
Stack(int n = 4)
:_array(new T[n])
, _capacity(n)
, _size(0)
{
}
~Stack()
{
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
//声明
void Push(const T& x);
private:
T* _array;
int _capacity;
int _size;
};
//定义
template<typename T>
void Stack<T>::Push(const T& x)
{
if (_size == _capacity)
{
//手动扩容
T* tmp = new T[_capacity * 2];
memcpy(tmp, _array, sizeof(T) * _size);
delete[] _array;
_array = tmp;
_capacity *= 2;
}
_array[_size++] = x;
}
平时在类中,函数的声明和定义可不是这样写的?
void Stack::Push(const T& x)
但在模板中这样写是不行的,template<typename T>只管下面的一个类或函数,到了Push时就管不到了,所以Push中的T编译器是不认识的,所以我们应该这样写:void Stack<T>::Push(const T& x)。
模板中的函数的定义和分离尽量不要写到两个文件中,也可以理解为不能写到两个文件中。
四、总结
本篇到这里就结束了,主要写了模板的一些基本知识,希望对大家有所收获,祝大家天天开心!