欢迎来到博主的专栏——c++编程
博主ID:代码小豪
泛型编程
泛型编程多应用于面向对象编程的语言当中,如:java,c#,c++等。其目的是提高代码的复用性和灵活性。
泛型编程的原理是将函数的参数类型(包括返回类型和局部变量的类型),通过将类型作为参数的方式,实例化这些参数的参数类型。使得代码可以支持不同类型的参数进行运算。
在c++当中,支持泛型编程的关键字是template,也称为模板。在c++的标准库中,有许多接口应用了template这一语言特性,最著名的莫过于c++标准模板库(Standard Template Library),即STL。因此,想要学习STL,了解模板是不可缺少的。
函数模板
再实际编程当中,我们会遇到这么一种情况:我们要实现一个逻辑类似,但是参数类型不同的函数,这时候我们就有以下解决方案。
方案1:重载这些函数。
比如要实现一个支持多个参数类型的加法函数add,那么编写时就需要重载支持这些类型的函数,比如最常见的浮点型和和整形
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}
但是重载这些函数需要多次编写对应的代码,而且需要重载类型还不止于此,比如还要支持不同类型的参数相加,比如一个类型是int,另一个类型是double,而且还需要考虑到不同参数调用的情况,
double add(int a, double b)
{
return a + b;
}
double add(double a, int b)
{
return a + b;
}
如果这个函数还要支持类类型的加法功能,需要重载的函数会变得更多,代码会变得臃肿且繁杂。显然存在弊端的。template被推出的目的就是要解决这样的问题。
方案2:模板函数。
模板的使用方法如下:
template<\typename type>
注意,这个typename也是一个关键字而非类型名,而type则是一个参数,只是这个参数是类型而非变量。
template<typename T>
T add(T a,T b)
{
return a + b;
}
当我们调用这个add时。
int a = 1, b = 2;
add(a, b);
因为我们传入的两个参数都是int类型,于是编译器判断我们想要的模板函数是处理int类型的数据的。于是编译器就会将模板函数当中T替换成int。
这等价于编译器将模板函数初始化成
int add(int a,int b)
{
return a + b;
}
编译器会根据实际上传的参数判断这个模板类型T的具体类型是什么,如果我调用这个函数时传递的参数是(int,int),那么编译器就会判断这个模板函数的实际参数是int,当然如果我们传递(int,double)当然是不行的,因为编译器无法判断T的类型究竟是代表什么类型。这时候就要用到类型转换,或者实例化模板函数了
int a1 = 1, b1 = 2;
double a2 = 1.1, b2 = 2.2;
add(a1, a1);//add<int>
add(a2, b2);//add<double>
add(a1, a2);//error,编译器无法判断实例化函数的具体类型
add(a1, (int)a2);//add<int>
add((double)a1, a2);//add<double>
通过这么一个案例,我们可以了解到模板的表层原理:
创建一个模板函数后,编译器会根据调用该函数时传递的实参类型,将类型T实例化成具体的类型。
那么我们试试往模板的深层原理思考一下。前面提到,具体的类型是由编译器决定的,那么编译器是怎么做的呢?我这里先提供两个猜想。
猜想1:编译器会调用模板函数,这个模板函数是动态的,具体的类型由调用这个函数的参数类型决定。
猜想2:模板函数不是函数,而是一个模板。当我们调用这个函数时,编译器会根据模板生成对应的函数,不同的类型会被重载。
这两个猜想的区别就在于,当我们使用不同类型的参数调用函数时,是调用同一个函数(模板函数),还是多个函数(根据模板重载成多个函数)。那么想要判断上文中不同类型调用的add是不是同一个函数,那么就要去看看这些add的函数地址。
我们在调试的过程当中打开vs当中的反汇编。
可以发现,猜想2是正确的,于是我们可以得出下面的结论
函数模板是一个提供给编译器的模板,当调用函数时,编译器会根据传递的参数类型,实例化出一个函数。
模板实例化
实际上编译器不单单会根据参数类型实例化模板,我们也可以指定编译器将模板实例化成需要的类型。
比如在上例当中,我们出现了编译器不能判断模板类型的情况,此时使用模板实例化可以解决这个问题。
实例化的方法如下:在函数名后加上尖括号(<>),在尖括号当中指定实例化的类型。
比如:
add<int>(a1, a2);//实例化成add<int>
add<double>(a1, a2);//实例化成add<double>
我比较推荐使用指定的方式完成模板函数的实例化,最主要的目的就在于提高代码的可读性。
我们可以想象这么一个场景。一个模板支持20几种的类型完成实例化,但是在程序运行的过程中,有上百的函数调用了模板函数。如果这些函数一旦发生运算错误(注意不是编译错误,这是不报错的)。那么检查代码的过程当中,我们就需要人工的判断这些函数到底实例化成了什么类型的函数,可以想象,这个过程肯定是无比痛苦的。
模板不仅能实例化c++的内置类型,还可以实例化成类类型,能否运行取决于你是否重载了能支持如此使用的函数。比如c++的标准库中有一个striing类,而这个string类正好重载了operator +(重载运算符),因此在add中也可以使用string类型的对象。
std::string s1("hello");
std::string s2("world");
std::cout << add<std::string>(s1, s2);
运行结果如下:
多参数模板
在上面的例子当中,模板只存在一个参数T,这就说明这个模板函数中的T只能实例化成一种类型,如果函数存在多种类型的调用需求,这显然是无法满足的。
因此我们可以在模板当中使用多个参数。使用方式如下:
template<\typename type1,typename type2,……>
以加法函数为例,我们现在不想加法函数只支持相同类型的参数相加了,现在想让加法函数支持不同类型的参数相加。但是一个模板参数只能生成一种类型,因此我们需要定义两个模板参数
template<typename T1, typename T2>
auto add(T1 a,T2 b)
{
return a + b;
}
auto作为类型时,编译器会自动为其匹配合适的类型。为什么要使用auto呢?我们可以来设想一下这个add的返回类型到底是什么。
如果用T1作为add的返回类型,若是add<int,double>。那么这个函数的返回值就是int类型的,但是(a+b)的结果却是double类型的(这里涉及隐式转换类型)。如果将double强制转换成int,会导致精度丢失,所以不合适。
如果用T2作为add类型,若是add<double,int>,同理,也会导致精度丢失,因此在多参数模板函数当中,返回类型最好是auto,而非模板类型。
类模板
类当中的成员变量的类型也可以使用模板,在STL当中,我们可以使用各种类型的vector,stack等。
我们在模板中可以使用typename,也可以使用class,比如
template<typename T>
template<class T>
这两者的作用都是相同的,不过博主建议还是使用typename,因为class总是给人一种这是一个类类型的感觉。当然,你可以在函数模板当中使用typename,在类模板当中使用class,从而起到一种区分的作用,这取决于每个人的命名习惯。
我们这里写一个类模板
template<class T>
class Vector
{
public :
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
// 使用析构函数演示:在类中声明,在类外定义。
~Vector();
void PushBack(const T& data);
void PopBack();
// ...
size_t Size() {return _size;}
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
类模板可以作用与类中的所有成员,包括成员函数和成员变量。
类模板的实例化
类模板的实例化必须显示的实例化。
比如我们要实例化一个int类型的vector类和double类型的vector类。
Vector<int> v1;
Vector<double> v1;
和函数模板类似的一点,只有我们具体写出实例化的类型,编译器才会帮助我们实例化出来使用这个类型的类。
模板的其他作用
模板不仅仅能替换类型,还可以替换参数。我们都知道vs当中不支持数组的声明当中出现变量。
例如
int n=10;
int arr[n];//error
但是这个限制只有vs才有,其他支持c99的编译器都能这么使用
使用模板可以解决这个问题,我们可以定义一个类Array。
template<typename T,int N>
class Array
{
private:
T _Array[N];
public:
int Size() {
return N;
}
};
实例化模板的过程中也会为N附上一个值,这个值会替换掉N。
Array<int, 10> arr1;
std::cout << arr1.Size();//结果为10
后言
对于初学c++的程序员来说,template绝对是一个易学难精的c++特性,在一些软件公司当中甚至会禁止员工使用模板。
这是因为模板的功能太强大了,如果我们真的考虑设计一个功能完善的模板,不仅仅要考虑使用这个模板的类型(包括类类型)会有什么样的应用场景,还要在自定义的类中,重载满足条件的操作符或者函数。甚至还要考虑到迭代器等种种因素。
因此这篇博客并不是教会大家怎么使用模板。而是作为简单认识模板的一个博客,旨在让大家能了解博主专栏关于的STL使用的博客当中,关于模板的相关知识。