文章目录
一、泛型编程
泛型编程指的是编写与类型无关的通用代码,这是代码复用的一种手段。 C++作为面向对象的语言,它是支持泛型编程的。这其实是对编写代码提供了很大的便利的。试想一下如果没有泛型编程,我们编写代码有可能要做很多重复的冗余的工作。假如我们要实现一个swap函数,简单地交换两个数,我们就得要针对不同的数据类型,写出对应的代码。但其实这些代码的逻辑都是一样的,只是数据类型不一样罢了,就像下面的代码这样,这就会使得代码过于冗余。
#include <iostream>
using namespace std;
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
void swap(double& a, double& b)
{
double tmp = a;
a = b;
b = tmp;
}
int main()
{
int x1 = 2;
int y1 = 3;
swap(x1, y1);// 两个int类型做交换
cout << x1 << " " << y1 << endl;
double x2 = 2.2;
double y2 = 3.3;
swap(x2, y2);// 两个double类型做交换
cout << x2 << " " << y2 << endl;
return 0;
}
因此,C++有了模板来实现泛型编程,模板是泛型编程的基础。比如上面swap函数的例子,我们就可以使用函数模板,只需要写一份代码,我们把代码逻辑写出来,类型不需要我们一个一个去指明,在编译的时候编译器会自动帮助我们推演出调用该函数时传进来的参数的类型是什么。
#include <iostream>
using namespace std;
// 为了防止与库里面的swap函数冲突,我们使用大写的S
template<class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
// void swap(int& a, int& b)
// {
// int tmp = a;
// a = b;
// b = tmp;
// }
// void swap(double& a, double& b)
// {
// double tmp = a;
// a = b;
// b = tmp;
// }
int main()
{
int x1 = 2;
int y1 = 3;
Swap(x1, y1);// 两个int类型做交换
cout << x1 << " " << y1 << endl;
double x2 = 2.2;
double y2 = 3.3;
Swap(x2, y2);// 两个double类型做交换
cout << x2 << " " << y2 << endl;
return 0;
}
二、函数模板
1.函数模板的定义方式
函数模板就好比是一个函数家族的代表,那些实现逻辑相同,数据类型不同的所有函数,都可以只用一个函数模板来实现,该函数模板与类型无关,只有在使用的时候才会根据指定的类型生成对应的函数,这样就使得编写代码变得更方便。
定义方式:
template<typename T1, typename T2, ... typename Tn>//可以有不止一个模板参数
函数返回值 函数名 (参数列表)
{
函数主体
}
typename是用来定义模板参数的关键字,也可以使用class,且一般更多使用的是class(但注意这里千万不能用struct代替class)。
2.函数模板的实例化
当我们调用函数模板的时候,传递不同类型的参数,编译器为我们生成对应类型的函数就叫作函数模板的实例化。函数模板实例化分为 显式实例化 和 隐式实例化 。
(1)隐式实例化
一般情况下函数模板可以隐式实例化,我们不需要自己指定类型是什么,编译器能够根据我们传递进去的参数自动帮我们推演出具体类型,并生成对应的实例化函数,这就叫做隐式实例化。
示例:
#include <iostream>
using namespace std;
template<class T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
Add(9, 10);
Add(1.2, 3.4);
return 0;
}
(2)显式实例化
显式实例化通常是在编译器简单地推演无法实现的时候,我们就要显式地指定数据类型来进行函数模板的实例化。显式实例化在调用函数时,在函数名后加上<数据类型>即可。 比如下面的两个例子:
示例一:
#include <iostream>
using namespace std;
template<class T>
T* func(int n)
{
return new T[n];
}
int main()
{
// 我们想要用func函数来开辟一个int类型的空间
//int *a = func(10); // 隐式实例化无法完成
int *a = func<int>(10);// 使用显式实例化
return 0;
}
示例一中的func函数它的形参没有使用模板类型,而是指定了是int类型,所以此时不能根据实参的类型来推演出模板类型T具体是什么类型。所以我们要使用显式实例化。
示例二:
#include <iostream>
using namespace std;
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 10;
double d = 2.2;
// int res = Add(a, d);// 这种写法是错误的
// int res = Add(a, (int)d);// 这种写法是正确的,可以手动进行强制类型转换
int res = Add<int>(a, d);// 也可以使用显式实例化
cout << res;
return 0;
}
示例二中的Add函数由于模板参数只有一个,所以我们传递的参数必须是相同类型的。但我们如果传递的是不同类型的,比如一个int类型一个double类型,这样就会报错。因为编译器在推演的时候,看到参数a就会推演成int类型,看到参数d就会推演成double类型,但模板参数列表中只有一个T类型,并且模板中并不会帮我们进行类型转换操作,因为它也不知道我们最终是想要int类型的结果还是double类型的结果。所以正确的做法是要么手动进行强制类型转换,要么在调用的时候使用显式实例化。
3.函数模板与同名函数的调用原则
我们看下面这份代码,定义了一个专门处理int类型的加法函数和一个通用的加法函数,这两个函数是可以同时存在的,编译器并不会报错。
但是当我们调用的时候,如果传递的是int类型的数据,编译器会调用专门处理int类型的加法函数,这里的调用原则是:如果有现成的函数就调用现成的,如果没有,再去函数模板推演生成实例化函数。
#include <iostream>
using namespace std;
// 专门处理int类型的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用的加法函数
template <class T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
Add(9, 10);
return 0;
}
三、类模板
1.类模板的定义方式
类模板的定义方式和函数模板定义方式非常相似:
定义方式:
template<class T1, class T2, ..., class Tn>
class 类模板名
{
类主体
}
2.类模板的实例化
类模板的实例化与函数模板的实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可。类模板名字不是真正的类,而实例化的结果才是真正的类。
示例:
// vector是类名,vector<int>才是类型
vector<int> v1;
vector<double> v2;
四、模板参数的缺省值
这里跟函数形参的缺省值一样,模板参数也是可以给缺省值的。如果给了缺省值,并且调用的时候没有进行显式实例化,那就会默认类型为缺省值的类型。下面举例的是函数模板的缺省值,类模板也可以有缺省值,并且用法和函数模板一样。
#include <iostream>
using namespace std;
template<class T = int>// 模板参数的缺省值
T* func(int n)
{
return new T[n];
}
int main()
{
// 我们想要用func函数来开辟一个int类型的空间
int *a = func(10);
return 0;
}
其实模板参数和函数的形参是很相似的,只不过模板参数传递的是数据的类型,而函数参数传递的是对象。所以我们可以类比函数参数来学习模板参数。
五、模板分离编译
在写代码的时候我们会有声明和定义分离的需要,函数模板和类模板的声明和定义分离与普通的函数和类的做法是有区别的,函数模板和类模板的声明和定义分离方法如下:
函数模板的声明与定义分离:
// 函数模板的声明
template<class T>
void Swap(T& a, T& b);
// 函数模板的定义
template<class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
类模板的声明与定义分离:
// 类模板的声明
template<class T>
class Vector
{
public:
Vector(size_t capacity = 10);
private:
T* _pData;
size_t _size;
size_t _capacity;
};
// 类模板的定义,定义的时候还得要指明类域
template<class T>
Vector<T>::Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
但是一定要搞清楚很重要的一点:虽然模板可以实现声明和定义分离,但是不能将模板的声明和模板的定义放在两个文件里面。也就是说对于正常普通的函数和类,我们可以将声明放在 .h 文件中,将定义放在 .cpp 文件中,但是模板的声明和定义不能像这样分开放在两个文件中。
下面我们举个例子解释一下原因:我们将Swap函数模板的声明放在Swap.h文件中,将Swap函数模板的定义放在了Swap.cpp文件中,然后在Test.cpp文件中的main函数调用Swap函数。
C/C++程序如果要运行起来,需要经历以下几个步骤:预处理 -> 编译 -> 汇编 -> 链接。
首先是在预处理阶段,Test.cpp文件和Swap.cpp文件都会进行头文件展开,然后分别生成Test.i文件和Swap.i文件,我们模拟实现头文件展开,如下图所示:
预处理过程没问题以后,接下来就到了编译过程。编译过程是按照语言得特性对程序进行词法、语法、语义的分析和检查。 Test.i文件里面就会检查有没有编译不通过的错误,检查到main函数中调用了Swap函数,向上一找发现确实有Swap函数的声明,并且形参列表也对应得上,此时不会去找函数的定义,因为这不是编译阶段的事情,所以编译器就会让其编译通过,生成Test.s文件。再来看Swap.i文件中,它编译也没有什么问题,可以编译通过生成Swap.s文件,但其实这是一个空文件,原因是编译前的Swap.i文件里只有函数模板的声明和定义,在编译器编译阶段模板会根据调用的类型生成对应的函数,但此时还只是编译阶段,编译器检查的时候只要看到了函数声明就可以编译通过了,要等到链接阶段才会去找函数的具体实现,所以此时Swap.i文件里的函数模板也不知道要生成什么具体数据类型的函数。
再接下来就是汇编过程,汇编过程就比较简单,编译器根据代码生成对应的汇编指令,最后分别生成Test.o文件和Swap.o文件。
到最后才是链接的过程,这个时候就是将多个 .o 文件合并成一个可执行文件,在这个阶段会去处理一些没有解决掉的地址的问题。比如说上面的Swap函数的调用,我们只知道有函数的声明也就编译通过了,但并没有去找函数的定义,也就没有拿到函数具体的地址,是无法实现调用的。所以这个阶段要寻找函数具体的定义。但从编译阶段开始函数模板就不知道该生成什么具体数据类型的函数,所以文件都是空的,根本没有生成对应的函数,也就没有函数地址,在链接阶段就找不到对应的函数,所以会出现链接错误。这就是模板的声明和定义不能分开放在两个文件中的原因。
六、非类型模板参数
模板参数除了可以定义类型模板参数以外,还可以定义非类型模板参数。以下图array为例,class T就是类型模板参数,size_t N就是非类型模板参数。类型模板参数是一个虚拟类型,而非类型模板参数是一个常量。
非类型模板参数的使用场景比较少,举个例子演示一下非类型模板参数的使用:
假如我们要自己手动实现一个静态栈结构,我们自定义栈空间大小为定值N,如果在定义栈的时候我们想让一个栈的空间是100,另一个栈的空间是500,又不浪费空间内存的情况下,就可以使用非类型模板参数。
#include <iostream>
using namespace std;
template<class T, size_t N>
class Stack
{
private:
T a[N];
int top;
};
int main()
{
Stack<int, 100> s1;
Stack<int, 500> s2;
return 0;
}
非类型模板参数注意事项:
- 非类型模板参数是个常量,不能够被修改。
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型模板参数必须在编译期就能确认结果。
七、模板的特化
模板特化指的是:在原来的模板基础上,针对特殊类型进行特殊化处理。模板特化分为函数模板特化和类模板特化。
例如下面这个例子,有一个判断两数是否相等的函数模板,你只需要传递进两个参数就可以判断这两个参数是否相等。
如果我们传递的是内置类型比如说int、double,就可以直接比较出结果。但如果我们传递的是自定义类型,比如说日期类的指针,我们就需要对这种日期类指针类型进行特殊化处理。
#include <iostream>
using namespace std;
struct Date
{
int _year = 1;
int _month = 1;
int _day = 1;
};
template<class T>
bool isEqual(T left, T right)
{
return left == right;
}
int main()
{
Date* d1 = new Date;
Date* d2 = new Date;
// 最后输出的结果是0,因为无法进行比较
cout << isEqual(d1, d2) << endl;
return 0;
}
1.函数模板特化
类似上面的问题,函数模板的特化可以解决。
函数模板的特化步骤:
- 必须要有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后面跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表:必须和模板函数的基础参数类型完全相同,如果不同,编译器可能会报一些奇怪的错误
#include <iostream>
using namespace std;
struct Date
{
int _year = 1;
int _month = 1;
int _day = 1;
};
template <class T>
bool isEqual(T left, T right)
{
return left == right;
}
// 函数模板的特化
template <>
bool isEqual<Date *>(Date *left, Date *right)
{
return left->_year == right->_year && left->_month == right->_month && left->_day == right->_day;
}
int main()
{
Date *d1 = new Date;
Date *d2 = new Date;
cout << isEqual(d1, d2) << endl;
return 0;
}
2.类模板特化
类模板也可以进行特化,如果我们对一个特殊类型的类有特殊化的处理,其它类型只需要用同样方法处理,那么就可以使用类模板特化。
(1)全特化
例如下面的例子:我们想让Util类中特定的类型int和double特殊化处理。
#include <iostream>
using namespace std;
template <class T1, class T2>
class Util
{
public:
Util(T1 a, T2 b)
: _x(a), _y(b)
{
}
bool isEqual()
{
return _x == _y;
}
private:
T1 _x;
T2 _y;
};
template <>
class Util<int, double>
{
public:
Util(int a, double b)
: _x(a), _y(b)
{
}
bool isEqual()
{
cout << "这是Util类关于int、double类型的特化" << endl;
return true;
}
private:
int _x;
double _y;
};
int main()
{
Util<int, int> u1(1, 1);
Util<int, double> u2(1, 1.0);
u1.isEqual();
u2.isEqual();
return 0;
}
最后的结果是u1调用了通用的类,而u2调用了类模板的特化。这种特化方式也叫作全特化,即将模板参数列表中所有的参数都确定化。
(2)偏特化
偏特化指的是任何针对模板参数进一步进行条件限制设计的特化版本。偏特化有部分特化和将参数进一步限制两种表现。
部分特化:
对于上面的类模板,我们可以指定一部分的类型对其进行特化:
template <class T1, class T2>
class Util
{
public:
Util(T1 a, T2 b)
: _x(a), _y(b)
{
}
bool isEqual()
{
return _x == _y;
}
private:
T1 _x;
T2 _y;
};
template <class T1>
class Util<T1, double>
{
public:
Util(T1 a, double b)
: _x(a), _y(b)
{
}
bool isEqual()
{
cout << "这是Util类关于int、double类型的特化" << endl;
return true;
}
private:
T1 _x;
double _y;
};
参数更进一步的限制:
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
#include <iostream>
using namespace std;
// 原生类模板
template <class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 两个参数偏特化为指针类型
template <class T1, class T2>
class Data<T1 *, T2 *>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 两个参数偏特化为引用类型
template <class T1, class T2>
class Data<T1 &, T2 &>
{
public:
Data(const T1 &d1, const T2 &d2)
: _d1(d1), _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1 &_d1;
const T2 &_d2;
};
int main()
{
Data<double, int> d1; // 调用特化的int版本
Data<int, double> d2; // 调用基础的模板
Data<int *, int *> d3; // 调用特化的指针版本
Data<int &, int &> d4(1, 2); // 调用特化的指针版本
return 0;
}