初识模版(C++)
模版是C++的一个重大发明,是让C++突飞猛进的原因之一。
实现一个通用的交换函数?
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;
}int main()
{return 0;
}
可以看到以前我们要交换不同数据类型的变量时,需要调用各自不同类型的交换函数,虽然可以重载,但还是写了很多个Swap。
我们很难不想将它们合为一个通用的Swap,对吧?
就像活字印刷的模具,我们能否有一个通用的Swap函数,别人想要交换什么类型,就自动生成为那个类型的Swap函数呢?
//模版类型
template<class T>
void Swap(T& x, T& y)//以前这里是写具体类型,现在写的是模版类型
{T tmp = x;x = y;y = tmp;
}int main()
{int i = 1, j = 2;double m = 1.1, n = 2.2;Swap(i, j);Swap(m, n);return 0;
}
其实就是让编译器去具体写我们想要类型的函数。
而且当我们用调试时观察可以发现,调用Swap时确实走到void Swap(T& x, T& y)里面去了。
然而事实上调用的不是同一个函数。IDE做了特殊处理,这样是为了方便调试看。
如果我们再深究一下,去看反汇编,就会发现调用的确实不是同一个函数:
这两个函数我们是没有写的,所以是编译器帮助我们生成的。模版的特点就是让C++在有些地方能够半自动化。
生成具体类型函数这个活是免不了的,只是由我们做变成编译器去做了。
模版有两类,函数模版和类模版,我们刚才写的就叫做函数模版。
函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生 函数的特定类型版本。
函数模板格式
template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){}
这里的typename关键字是后期增加的,也可以用class。typename,类型名称,会更形象一点。
模版参数列表可以类比着函数参数列表去学习,它们的规则是类似的。
函数参数列表是()圆括号,模版参数列表是<>尖括号。
函数的参数列表定义的是变量或者说对象,(C++不是纯面向对象的因为C++兼容C语言,也可以走面向过程这一套。C++中变量喜欢叫对象,其实像C一样叫变量也没什么问题。)
(类型 变量1,类型 变量2)
而模版的参数列表定义的是关键字
<class 类型1,class 类型2>
class后面不是变量名称了,而是类型。
多个类型:
template<typename T1, typename T2>
void func(const T1& x,const T2& y)
{}int main()
{int i = 1, j = 2;double m = 1.1, n = 2.2;func(i, m);func(i, j);return 0;
}
要注意的一点是,在前一个:
template<class T>
void Swap(T& x, T& y)
单类型的函数中,这个T到底是什么类型是由我们传递的实参推理决定的,所以如果我们在实参中传递了两个不同类型的值就会产生歧义,不知道该生成哪个类型的Swap函数:
template<class T>
void Swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}int main()
{int i = 1, j = 2;double m = 1.1, n = 2.2;Swap(i,m);//这是不行的return 0;
}
从此以后,我们就可以告辞手写Swap的时代了。C++11下这个函数在头文件中,一般会被简介包含从,所以不需要我们自己包含这个头文件。
是小写字母s开头。
int main()
{int i = 1, j = 2;double m = 1.1, n = 2.2;swap(i, j);swap(m,n);return 0;
}
以后直接使用即可。
现在我们怎么解决想要传两个不同类型的参数的问题?
第一种办法,强制类型转换
int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;cout<<Add(a1, (int)d1)<<endl;cout<<Add((double)a1, d1)<<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;//推导实例化cout<<Add(a1, (int)d1)<<endl;cout<<Add((double)a1, d1)<<endl;//显式实例化cout << Add<int>(a1, d1) << endl;
}
在函数名和参数直接加尖括号写类型,不通过推导而是通过显式实例化。这样我们的d1可以传给生成好的int类型Swap是因为可以走隐式类型转换。
当然也可以选择写一个两个类型的Swap。
有些情况下,必须走显式实例化。不能走自动推导:
template<class T>
T* func1(int n)
{return new T[n];
}int main()
{func1(10);return 0;
}
这样的形参无法推导出T是什么类型。
所以必须显式实例化。double* p1= func1<double>(10);
如果一个模版和一个具体类型的函数同时存在,会优先走对应类型的函数。
int a1=10,a2=20;
cout<<Add(a1,a2)<<endl;
会优先去调用第二个类型直接匹配的Add函数。
类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{// 类内成员定义};
这里的class也能用typename替代。
// 类模版
template<typename T>
class Stack
{
public:Stack(int n = 4):_array(new T[n])//不用检查失败,不会返回空,_size(0),_capacity(n){}~Stack(){delete[] _array;_array = nullptr;_size = _capacity = 0;}void Push(const T& data)//用传值传参要拷贝,代价太大,加了引用尽量加 const{if (_size == _capacity)//满了{//没有renew这种东西,也不能用reallocT* tmp = new T[_capacity * 2];memcpy(tmp, _array, sizeof(T) * _size);delete[] _array;_array = tmp;_capacity *= 2;}_array[_size++] = x;}//……private:T* _array;size_t _capacity;size_t _size;
};
记得我们以前使用typedef来写栈的,也可以修改栈存放的数据类型,但是为什么现在要用类模版来实现呢?有什么区别呢?
int main()
{Stack st1;//intStack st2;//doublereturn 0;
}
用typedef,如果我们想要一个栈存放int类型,一个存放double类型,这是做不到的。得写两份一个存int,一个存double。
同时我们也可以发现,类模板无法推导实例化,只能显式实例化,只有函数才有推导实例化。
int main()
{Stack<int> st1;st1.Push(1);st1.Push(2);st1.Push(3);Stack<double> st2;st2.Push(1.1);st2.Push(1.1);st2.Push(1.1);return 0;
}
从此我们的栈可以不止存一类数据了。
这两个栈其实不是同一个类型,和之前的函数模版一样,也是“活字印刷”。本质上生成了两个类型的栈类型,只是编译器做了这件事。
如果我们现在想要将类成员函数的声明与定义分离呢?
像以前一样指定类域,不行。
我们定义的模版参数只能给当前的函数或类去用,每个函数模版都得定义自己的模版参数,不一定叫T。
所以出了类,T就不能用了。得在分离出来的定义前面重新定义模版参数:
//类外
template<class T>
void Stack<T>::Push(const T& data)//用传值传参要拷贝,代价太大,加了引用尽量加 const
{if (_size == _capacity)//满了{//没有renew这种东西,也不能用reallocT* tmp = new T[_capacity * 2];memcpy(tmp, _array, sizeof(T) * _size);delete[] _array;_array = tmp;_capacity *= 2;}_array[_size++] = x;
}
其次要注意的是,模版不支持声明和定义分离到两个文件。会报链接错误。以后再说原因。也有办法分离但是非常麻烦,一般模版放在一个文件。
如果我们将函数定义里的T换为X,不会报错:
为什么?因为这个本来就只是个代号,其实实际在调用Push这个函数时就已经没有这个T或者X的概念了,而是实例化为对应的类型了。调用的也不是模版的这个函数,而是生成后的函数。
这些只是模版的初阶用法,以后再进阶。
本文到此结束=_=