网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
//赋值号两边的数据类型不一致
Point<float, float> *p = new Point<float, int>(10.6, 109);
//赋值号右边没有指明数据类型
Point<float, float> *p = new Point(10.6, 109);
综合示例
【实例1】将上面的类定义和类实例化的代码整合起来,构成一个完整的示例,如下所示:
#include <iostream>
using namespace std;
template<class T1, class T2> //这里不能有分号
class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const; //获取x坐标
void setX(T1 x); //设置x坐标
T2 getY() const; //获取y坐标
void setY(T2 y); //设置y坐标
private:
T1 m_x; //x坐标
T2 m_y; //y坐标
};
template<class T1, class T2> //模板头
T1 Point<T1, T2>::getX() const /*函数头*/ {
return m_x;
}
template<class T1, class T2>
void Point<T1, T2>::setX(T1 x){
m_x = x;
}
template<class T1, class T2>
T2 Point<T1, T2>::getY() const{
return m_y;
}
template<class T1, class T2>
void Point<T1, T2>::setY(T2 y){
m_y = y;
}
int main(){
Point<int, int> p1(10, 20);
cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
Point<int, char*> p2(10, "东经180度");
cout<<"x="<<p2.getX()<<", y="<<p2.getY()<<endl;
Point<char*, char*> *p3 = new Point<char*, char*>("东经180度", "北纬210度");
cout<<"x="<<p3->getX()<<", y="<<p3->getY()<<endl;
return 0;
}
运行结果:
x=10, y=20
x=10, y=东经180度
x=东经180度, y=北纬210度
在定义类型参数时我们使用了 class,而不是 typename,这样做的目的是让读者对两种写法都熟悉。
【实例2】用类模板实现可变长数组。
#include <iostream>
#include <cstring>
using namespace std;
template <class T>
class CArray
{
int size; //数组元素的个数
T *ptr; //指向动态分配的数组
public:
CArray(int s = 0); //s代表数组元素的个数
CArray(CArray & a);
~CArray();
void push_back(const T & v); //用于在数组尾部添加一个元素v
CArray & operator=(const CArray & a); //用于数组对象间的赋值
T length() { return size; }
T & operator[](int i)
{//用以支持根据下标访问数组元素,如a[i] = 4;和n = a[i]这样的语句
return ptr[i];
}
};
template<class T>
CArray<T>::CArray(int s):size(s)
{
if(s == 0)
ptr = NULL;
else
ptr = new T[s];
}
template<class T>
CArray<T>::CArray(CArray & a)
{
if(!a.ptr) {
ptr = NULL;
size = 0;
return;
}
ptr = new T[a.size];
memcpy(ptr, a.ptr, sizeof(T ) * a.size);
size = a.size;
}
template <class T>
CArray<T>::~CArray()
{
if(ptr) delete [] ptr;
}
template <class T>
CArray<T> & CArray<T>::operator=(const CArray & a)
{ //赋值号的作用是使"="左边对象里存放的数组,大小和内容都和右边的对象一样
if(this == & a) //防止a=a这样的赋值导致出错
return * this;
if(a.ptr == NULL) { //如果a里面的数组是空的
if( ptr )
delete [] ptr;
ptr = NULL;
size = 0;
return * this;
}
if(size < a.size) { //如果原有空间够大,就不用分配新的空间
if(ptr)
delete [] ptr;
ptr = new T[a.size];
}
memcpy(ptr,a.ptr,sizeof(T)*a.size);
size = a.size;
return *this;
}
template <class T>
void CArray<T>::push_back(const T & v)
{ //在数组尾部添加一个元素
if(ptr) {
T *tmpPtr = new T[size+1]; //重新分配空间
memcpy(tmpPtr,ptr,sizeof(T)*size); //拷贝原数组内容
delete []ptr;
ptr = tmpPtr;
}
else //数组本来是空的
ptr = new T[1];
ptr[size++] = v; //加入新的数组元素
}
int main()
{
CArray<int> a;
for(int i = 0;i < 5;++i)
a.push_back(i);
for(int i = 0; i < a.length(); ++i)
cout << a[i] << " ";
return 0;
}
大话C++模板编程的来龙去脉
计算机编程语言种类繁多,目前能够查询到的有 600 多种,常用的不超过 20 种,TIOBE 每个月都会发布世界编程语言排行榜,统计前 50 名编程语言的市场份额以及它们的变动趋势。该榜单反映了编程语言的热门程度,程序员可以据此来检查自己的开发技能是否跟得上趋势,公司或机构也可以据此做出战略调整。
这些编程语言根据不同的标准可以分为不同的种类,根据“在定义变量时是否需要显式地指明数据类型”可以分为强类型语言和弱类型语言。
1) 强类型语言
强类型语言在定义变量时需要显式地指明数据类型,并且一旦为变量指明了某种数据类型,该变量以后就不能赋予其他类型的数据了,除非经过强制类型转换或隐式类型转换。典型的强类型语言有 C/C++、Java、C# 等。
下面的代码演示了如何在 C/C++ 中使用变量:
int a = 100; //不转换
a = 12.34; //隐式转换(直接舍去小数部分,得到12)
a = (int)"http://c.biancheng.net"; //强制转换(得到字符串的地址)
下面的代码演示了如何在 Java 中使用变量:
int a = 100; //不转换
a = (int)12.34; //强制转换(直接舍去小数部分,得到12)
Java 对类型转换的要求比 C/C++ 更为严格,隐式转换只允许由低向高转,由高向低转必须强制转换。
2) 弱类型语言
弱类型语言在定义变量时不需要显式地指明数据类型,编译器(解释器)会根据赋给变量的数据自动推导出类型,并且可以赋给变量不同类型的数据。典型的弱类型语言有 JavaScript、Python、PHP、Ruby、Shell、Perl 等。
下面的代码演示了如何在 JavaScript 中使用变量:
var a = 100; //赋给整数
a = 12.34; //赋给小数
a = "http://c.biancheng.net"; //赋给字符串
a = new Array("JavaScript","React","JSON"); //赋给数组
var 是 JavaScript 中的一个关键字,表示定义一个新的变量,而不是数据类型。
下面的代码演示了如何在 PHP 中使用变量:
$a = 100; //赋给整数
$a = 12.34; //赋给小数
$a = "http://c.biancheng.net"; //赋给字符串
$a = array("JavaScript","React","JSON"); //赋给数组
$ 是一个特殊符号,所有的变量名都要以 $ 开头。PHP 中的变量不用特别地定义,变量名首次出现即视为定义。
这里的强类型和弱类型是站在变量定义和类型转换的角度讲的,并把 C/C++ 归为强类型语言。另外还有一种说法是站在编译和运行的角度,并把 C/C++ 归为弱类型语言。本节我们只关注第一种说法。
类型对于编程语言来说非常重要,不同的类型支持不同的操作,例如class Student
类型的变量可以调用 display() 方法,int
类型的变量就不行。不管是强类型语言还是弱类型语言,在编译器(解释器)内部都有一个类型系统来维护变量的各种信息。
对于强类型的语言,变量的类型从始至终都是确定的、不变的,编译器在编译期间就能检测某个变量的操作是否正确,这样最终生成的程序中就不用再维护一套类型信息了,从而减少了内存的使用,加快了程序的运行。
不过这种说法也不是绝对的,有些特殊情况还是要等到运行阶段才能确定变量的类型信息。比如 C++ 中的多态,编译器在编译阶段会在对象内存模型中增加虚函数表、type_info 对象等辅助信息,以维护一个完整的继承链,等到程序运行后再执行一段代码才能确定调用哪个函数,这在《C++多态与虚函数》一章中进行了详细讲解。
对于弱类型的语言,变量的类型可以随时改变,赋予它什么类型的数据它就是什么类型,编译器在编译期间不好确定变量的类型,只有等到程序运行后、真的赋给变量一个值了,才能确定变量当前是什么类型,所以传统的编译对弱类型语言意义不大,因为即使编译了也有很多东西确定不下来。
弱类型语言往往是一边执行一边编译,这样可以根据上下文(可以理解为当前的执行环境)推导出很多有用信息,让编译更加高效。我们将这种一边执行一边编译的语言称为解释型语言,而将传统的先编译后执行的语言称为编译型语言。
强类型语言较为严谨,在编译时就能发现很多错误,适合开发大型的、系统级的、工业级的项目;而弱类型语言较为灵活,编码效率高,部署容易,学习成本低,在 Web 开发中大显身手。另外,强类型语言的 IDE 一般都比较强大,代码感知能力好,提示信息丰富;而弱类型语言一般都是在编辑器中直接书写代码。
为了展示弱类型语言的灵活,我们以 PHP 为例来实现上节中的 Point 类,让它可以处理整数、小数以及字符串:
class Point{
public function Point($x, $y){ //构造函数
$this -> m_x = $x;
$this -> m_y = $y;
}
public function getX(){
return $this -> m_x;
}
public function getY(){
return $this -> m_y;
}
public function setX($x){
$this -> m_x = $x;
}
public function setY($y){
$this -> m_y = $y;
}
private $m_x;
private $m_y;
}
$p = new Point(10, 20); //处理整数
echo $p->getX() . ", " . $p->getY() . "<br />";
$p = new Point(24.56, "东京180度"); //处理小数和字符串
echo $p->getX() . ", " . $p->getY() . "<br />";
$p = new Point("东京180度", "北纬210度"); //处理字符串
echo $p->getX() . ", " . $p->getY() . "<br />";
运行结果:
10, 20
24.56, 东京180度
东京180度, 北纬210度
看,PHP 不需要使用模板就可以处理多种类型的数据,它天生对类型就不敏感。C++ 就不一样了,它是强类型的,比较“死板”,所以后来 C++ 开始支持模板了,主要就是为了弥补强类型语言“不够灵活”的缺点。
模板所支持的类型是宽泛的,没有限制的,我们可以使用任意类型来替换,这种编程方式称为泛型编程(Generic Programming)。相应地,可以将参数 T 看做是一个泛型,而将 int、float、string 等看做是一种具体的类型。除了 C++,Java、C#、Pascal(Delphi)也都支持泛型编程。
C++ 模板也是被迫推出的,最直接的动力来源于对数据结构的封装。数据结构关注的是数据的存储,以及存储后如何进行增加、删除、修改和查询操作,它是一门基础性的学科,在实际开发中有着非常广泛的应用。C++ 开发者们希望为线性表、链表、图、树等常见的数据结构都定义一个类,并把它们加入到标准库中,这样以后程序员就不用重复造轮子了,直接拿来使用即可。
但是这个时候遇到了一个无法解决的问题,就是数据结构中每份数据的类型无法提前预测。以链表为例,它的每个节点可以用来存储小数、整数、字符串等,也可以用来存储一名学生、教师、司机等,还可以直接存储二进制数据,这些都是可以的,没有任何限制。而 C++ 又是强类型的,数据的种类受到了严格的限制,这种矛盾是无法调和的。
要想解决这个问题,C++ 必须推陈出新,跳出现有规则的限制,开发新的技术,于是模板就诞生了。模板虽然不是 C++ 的首创,但是却在 C++ 中大放异彩,后来也被 Java、C# 等其他强类型语言采用。
C++ 模板有着复杂的语法,可不仅仅是前面两节讲到的那么简单,它的话题可以写一本书。C++ 模板也非常重要,整个标准库几乎都是使用模板来开发的,STL 更是经典之作。
STL(Standard Template Library,标准模板库)就是 C++ 对数据结构进行封装后的称呼。
C++函数模板的重载
当需要对不同的类型使用同一种算法(同一个函数体)时,为了避免定义多个功能重复的函数,可以使用模板。然而,并非所有的类型都使用同一种算法,有些特定的类型需要单独处理,为了满足这种需求,C++ 允许对函数模板进行重载,程序员可以像重载常规函数那样重载模板定义。
在《C++函数模板》一节中我们定义了 Swap() 函数用来交换两个变量的值,一种方案是使用指针,另外一种方案是使用引用,请看下面的代码:
//方案①:使用指针
template<typename T> void Swap(T *a, T *b){
T temp = *a;
*a = *b;
*b = temp;
}
//方案②:使用引用
template<class T> void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
这两种方案都可以交换 int、float、char、bool 等基本类型变量的值,但是却不能交换两个数组。
对于方案①,调用函数时传入的是数组指针,或者说是指向第 0 个元素的指针,这样交换的仅仅是数组的第 0 个元素,而不是整个数组。数组和指针本来是不等价的,只是当函数的形参为指针时,传递的数组也会隐式地转换为指针,这在《C语言入门教程》中的《数组和指针绝不等价,数组是另外一种类型》《数组到底在什么时候会转换为指针》两节中做了详细讲解,不了解的读者请猛击链接深入学习。
对于方案②,假设传入的是一个长度为 5 的 int 类型数组(该数组的类型是 int [5]),那么 T 的真实类型为int [5]
,T temp
会被替换为int [5] temp
,这显然是错误的。另外一方面,语句a=b;
尝试对数组 a 赋值,而数组名是常量,它的值不允许被修改,所以也会产生错误。总起来说,方案②会有两处语法错误。
交换两个数组唯一的办法就是逐个交换所有的数组元素,请看下面的代码:
template<typename T> void Swap(T a[], T b[], int len){
T temp;
for(int i=0; i<len; i++){
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
在该函数模板中,最后一个参数的类型为具体类型(int),而不是泛型。并不是所有的模板参数都必须被泛型化。
下面是一个重载函数模板的完整示例:
#include <iostream>
using namespace std;
template<class T> void Swap(T &a, T &b); //模板①:交换基本类型的值
template<typename T> void Swap(T a[], T b[], int len); //模板②:交换两个数组
void printArray(int arr[], int len); //打印数组元素
int main(){
//交换基本类型的值
int m = 10, n = 99;
Swap(m, n); //匹配模板①
cout<<m<<", "<<n<<endl;
//交换两个数组
int a[5] = { 1, 2, 3, 4, 5 };
int b[5] = { 10, 20, 30, 40, 50 };
int len = sizeof(a) / sizeof(int); //数组长度
Swap(a, b, len); //匹配模板②
printArray(a, len);
printArray(b, len);
return 0;
}
template<class T> void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
template<typename T> void Swap(T a[], T b[], int len){
T temp;
for(int i=0; i<len; i++){
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
void printArray(int arr[], int len){
for(int i=0; i<len; i++){
if(i == len-1){
cout<<arr[i]<<endl;
}else{
cout<<arr[i]<<", ";
}
}
}
运行结果:
99, 10
10, 20, 30, 40, 50
1, 2, 3, 4, 5
C++函数模板的实参推断
在使用类模板创建对象时,程序员需要显式的指明实参(也就是具体的类型)。例如对于下面的 Point 类:
template<typename T1, typename T2> class Point;
我们可以在栈上创建对象,也可以在堆上创建对象:
Point<int, int> p1(10, 20); //在栈上创建对象
Point<char*, char*> *p = new Point<char*, char*>("东京180度", "北纬210度"); //在堆上创建对象
因为已经显式地指明了 T1、T2 的具体类型,所以编译器就不用再自己推断了,直接拿来使用即可。
而对于函数模板,调用函数时可以不显式地指明实参(也就是具体的类型)。请看下面的例子:
//函数声明
template<typename T> void Swap(T &a, T &b);
//函数调用
int n1 = 100, n2 = 200;
Swap(n1, n2);
float f1 = 12.5, f2 = 56.93;
Swap(f1, f2);
虽然没有显式地指明 T 的具体类型,但是编译器会根据 n1 和 n2、f1 和 f2 的类型自动推断出 T 的类型。这种通过函数实参来确定模板实参(也就是类型参数的具体类型)的过程称为模板实参推断。
在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找类型参数的具体类型。
模板实参推断过程中的类型转换
对于普通函数(非模板函数),发生函数调用时会对实参的类型进行适当的转换,以适应形参的类型。这些转换包括:
- 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
- 派生类向基类的转换:也就是向上转型,请猛击《C++向上转型(将派生类赋值给基类)》了解详情。
- const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
- 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
- 用户自定的类型转换。
例如有下面两个函数原型:
void func1(int n, float f);
void func2(int *arr, const char *str);
它们具体的调用形式为:
int nums[5];
char *url = "http://c.biancheng.net";
func1(12.5, 45);
func2(nums, url);
对于 func1(),12.5 会从double
转换为int
,45 会从int
转换为float
;对于 func2(),nums 会从int [5]
转换为int *
,url 会从char *
转换为const char *
。
而对于函数模板,类型转换则受到了更多的限制,仅能进行「const 转换」和「数组或函数指针转换」,其他的都不能应用于函数模板。例如有下面几个函数模板:
template<typename T> void func1(T a, T b);
template<typename T> void func2(T *buffer);
template<typename T> void func3(const T &stu);
template<typename T> void func4(T a);
template<typename T> void func5(T &a);
它们具体的调用形式为:
int name[20];
Student stu1("张华", 20, 96.5); //创建一个Student类型的对象
func1(12.5, 30); //Error
func2(name); //name的类型从 int [20] 换转换为 int *,所以 T 的真实类型为 int
func3(stu1); //非const转换为const,T 的真实类型为 Student
func4(name); //name的类型从 int [20] 换转换为 int *,所以 T 的真实类型为 int *
func5(name); //name的类型依然为 int [20],不会转换为 int *,所以 T 的真实类型为 int [20]
对于func1(12.5, 30)
,12.5 的类型为 double,30 的类型为 int,编译器不知道该将 T 实例化为 double 还是 int,也不会尝试将 int 转换为 double,或者将 double 转换为 int,所以调用失败。
请读者注意 name,它本来的类型是int [20]
:
- 对于
func2(name)
和func4(name)
,name 的类型会从 int [20] 转换为 int *,也即将数组转换为指针,所以 T 的类型分别为 int * 和 int。 - 对于
func5(name)
,name 的类型依然为 int [20],不会转换为 int *,所以 T 的类型为 int [20]。
可以发现,当函数形参是引用类型时,数组不会转换为指针。这个时候读者要注意下面这样的函数模板:
template<typename T> void func(T &a, T &b);
如果它的具体调用形式为:
int str1[20];
int str2[10];
func(str1, str2);
由于 str1、str2 的类型分别为 int [20] 和 int [10],在函数调用过程中又不会转换为指针,所以编译器不知道应该将 T 实例化为 int [20] 还是 int [10],导致调用失败。
为函数模板显式地指明实参(也就是具体的类型)
函数模板的实参推断是指「在函数调用过程中根据实参的类型来寻找类型参数的具体类型」的过程,这在大部分情况下是奏效的,但是当类型参数的个数较多时,就会有个别的类型无法推断出来,这个时候就必须显式地指明实参。
下面是一个实参推断失败的例子:
template<typename T1, typename T2> void func(T1 a){
T2 b;
}
func(10); //函数调用
func() 有两个类型参数,分别是 T1 和 T2,但是编译器只能从函数调用中推断出 T1 的类型来,不能推断出 T2 的类型来,所以这种调用是失败的,这个时候就必须显式地指明 T1、T2 的具体类型。
「为函数模板显式地指明实参」和「为类模板显式地指明实参」的形式是类似的,就是在函数名后面添加尖括号< >
,里面包含具体的类型。例如对于上面的 func(),我们要这样来指明实参:
func<int, int>(10);
显式指明的模板实参会按照从左到右的顺序与对应的模板参数匹配:第一个实参与第一个模板参数匹配,第二个实参与第二个模板参数匹配,以此类推。只有尾部(最右)的类型参数的实参可以省略,而且前提是它们可以从传递给函数的实参中推断出来。
对于上面的 func(),虽然只有 T2 的类型不能自动推断出来,但是由于它位于类型参数列表的尾部(最右),所以必须同时指明 T1 和 T2 的类型。对代码稍微做出修改:
template<typename T1, typename T2> void func(T2 a){
T1 b;
}
//函数调用
func<int>(10); //省略 T2 的类型
func<int, int>(20); //指明 T1、T2 的类型
由于 T2 的类型能够自动推断出来,并且它位于参数列表的尾部(最右),所以可以省略。
显式地指明实参时可以应用正常的类型转换
上面我们提到,函数模板仅能进行「const 转换」和「数组或函数指针转换」两种形式的类型转换,但是当我们显式地指明类型参数的实参(具体类型)时,就可以使用正常的类型转换(非模板函数可以使用的类型转换)了。
例如对于下面的函数模板:
template<typename T> void func(T a, T b);
它的具体调用形式如下:
func(10, 23.5); //Error
func<float>(20, 93.7); //Correct
在第二种调用形式中,我们已经显式地指明了 T 的类型为 float,编译器不会再为「T 的类型到底是 int 还是 double」而纠结了,所以可以从容地使用正常的类型转换了。
C++模板的显式具体化
C++ 没有办法限制类型参数的范围,我们可以使用任意一种类型来实例化模板。但是模板中的语句(函数体或者类体)不一定就能适应所有的类型,可能会有个别的类型没有意义,或者会导致语法错误。
例如有下面的函数模板,它用来获取两个变量中较大的一个:
template<class T> const T& Max(const T& a, const T& b){
return a > b ? a : b;
}
请读者注意a > b
这条语句,>
能够用来比较 int、float、char 等基本类型数据的大小,但是却不能用来比较结构体变量、对象以及数组的大小,因为我们并没有针对结构体、类和数组重载>
。
另外,该函数模板虽然可以用于指针,但比较的是地址大小,而不是指针指向的数据,所以也没有现实的意义。
除了>
,+``-``*``/``==``<
等运算符也只能用于基本类型,不能用于结构体、类、数组等复杂类型。总之,编写的函数模板很可能无法处理某些类型,我们必须对这些类型进行单独处理。
模板是一种泛型技术,它能接受的类型是宽泛的、没有限制的,并且对这些类型使用的算法都是一样的(函数体或类体一样)。但是现在我们希望改变这种“游戏规则”,让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的显示具体化(Explicit Specialization)。
函数模板和类模板都可以显示具体化,下面我们先讲解函数模板的显示具体化,再讲解类模板的显示具体化。
函数模板的显式具体化
在讲解函数模板的显示具体化语法之前,我们先来看一个显示具体化的例子:
#include <iostream>
#include <string>
using namespace std;
typedef struct{
string name;
int age;
float score;
} STU;
//函数模板
template<class T> const T& Max(const T& a, const T& b);
//函数模板的显示具体化(针对STU类型的显示具体化)
template<> const STU& Max<STU>(const STU& a, const STU& b);
//重载<<
ostream & operator<<(ostream &out, const STU &stu);
int main(){
int a = 10;
int b = 20;
cout<<Max(a, b)<<endl;
STU stu1 = { "王明", 16, 95.5};
STU stu2 = { "徐亮", 17, 90.0};
cout<<Max(stu1, stu2)<<endl;
return 0;
}
template<class T> const T& Max(const T& a, const T& b){
return a > b ? a : b;
}
template<> const STU& Max<STU>(const STU& a, const STU& b){
return a.score > b.score ? a : b;
}
ostream & operator<<(ostream &out, const STU &stu){
out<<stu.name<<" , "<<stu.age <<" , "<<stu.score;
return out;
}
运行结果:
20
王明 , 16 , 95.5
本例中,STU 结构体用来表示一名学生(Student),它有三个成员,分别是姓名(name)、年龄(age)、成绩(score);Max() 函数用来获取两份数据中较大的一份。
要想获取两份数据中较大的一份,必然会涉及到对两份数据的比较。对于 int、float、char 等基本类型的数据,直接比较它们本身的值即可,而对于 STU 类型的数据,直接比较它们本身的值不但会有语法错误,而且毫无意义,这就要求我们设计一套不同的比较方案,从语法和逻辑上都能行得通,所以本例中我们比较的是两名学生的成绩(score)。
不同的比较方案最终导致了算法(函数体)的不同,我们不得不借助模板的显示具体化技术对 STU 类型进行单独处理。第 14 行代码就是显示具体化的声明,第 34 行代码进行了定义。
请读者注意第 34 行代码,Max<STU>
中的STU
表明了要将类型参数 T 具体化为 STU 类型,原来使用 T 的位置都应该使用 STU 替换,包括返回值类型、形参类型、局部变量的类型。
Max 只有一个类型参数 T,并且已经被具体化为 STU 了,这样整个模板就不再有类型参数了,类型参数列表也就为空了,所以模板头应该写作template<>
。
另外,Max<STU>
中的STU
是可选的,因为函数的形参已经表明,这是 STU 类型的一个具体化,编译器能够逆推出 T 的具体类型。简写后的函数声明为:
template<> const STU& Max(const STU& a, const STU& b);
函数的调用规则
回顾一下前面学习到的知识,在 C++ 中,对于给定的函数名,可以有非模板函数、模板函数、显示具体化模板函数以及它们的重载版本,在调用函数时,显示具体化优先于常规模板,而非模板函数优先于显示具体化和常规模板。
类模板的显式具体化
除了函数模板,类模板也可以显示具体化,并且它们的语法是类似的。
在《C++类模板》一节中我们定义了一个 Point 类,用来输出不同类型的坐标。在输出结果中,横坐标 x 和纵坐标 y 是以逗号,
为分隔的,但是由于个人审美的不同,我希望当 x 和 y 都是字符串时以|
为分隔,是数字或者其中一个是数字时才以逗号,
为分隔。为了满足我这种奇葩的要求,可以使用显示具体化技术对字符串类型的坐标做特殊处理。
下面的例子演示了如何对 Point 类进行显示具体化:
#include <iostream>
using namespace std;
//类模板
template<class T1, class T2> class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
T1 m_x;
T2 m_y;
};
template<class T1, class T2> //这里要带上模板头
void Point<T1, T2>::display() const{
cout<<"x="<<m_x<<", y="<<m_y<<endl;
}
//类模板的显示具体化(针对字符串类型的显示具体化)
template<> class Point<char*, char*>{
public:
Point(char *x, char *y): m_x(x), m_y(y){ }
public:
char *getX() const{ return m_x; }
void setX(char *x){ m_x = x; }
char *getY() const{ return m_y; }
void setY(char *y){ m_y = y; }
void display() const;
private:
char *m_x; //x坐标
char *m_y; //y坐标
};
//这里不能带模板头template<>
void Point<char*, char*>::display() const{
cout<<"x="<<m_x<<" | y="<<m_y<<endl;
}
int main(){
( new Point<int, int>(10, 20) ) -> display();
( new Point<int, char*>(10, "东京180度") ) -> display();
( new Point<char*, char*>("东京180度", "北纬210度") ) -> display();
return 0;
}
运行结果:
x=10, y=20
x=10, y=东京180度
x=东京180度 | y=北纬210度
请读者注意第 25 行代码,Point<char*, char*>
表明了要将类型参数 T1、T2 都具体化为char*
类型,原来使用 T1、T2 的位置都应该使用char*
替换。Point 类有两个类型参数 T1、T2,并且都已经被具体化了,所以整个类模板就不再有类型参数了,模板头应该写作template<>
。
再来对比第 19、40 行代码,可以发现,当在类的外部定义成员函数时,普通类模板的成员函数前面要带上模板头,而具体化的类模板的成员函数前面不能带模板头。
部分显式具体化
在上面的显式具体化例子中,我们为所有的类型参数都提供了实参,所以最后的模板头为空,也即template<>
。另外 C++ 还允许只为一部分类型参数提供实参,这称为部分显式具体化。
部分显式具体化只能用于类模板,不能用于函数模板。
仍然以 Point 为例,假设我现在希望“只要横坐标 x 是字符串类型”就以|
来分隔输出结果,而不管纵坐标 y 是什么类型,这种要求就可以使用部分显式具体化技术来满足。请看下面的代码:
#include <iostream>
using namespace std;
//类模板
template<class T1, class T2> class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
T1 m_x;
T2 m_y;
};
template<class T1, class T2> //这里需要带上模板头
void Point<T1, T2>::display() const{
cout<<"x="<<m_x<<", y="<<m_y<<endl;
}
//类模板的部分显示具体化
template<typename T2> class Point<char*, T2>{
public:
Point(char *x, T2 y): m_x(x), m_y(y){ }
public:
char *getX() const{ return m_x; }
void setX(char *x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
char *m_x; //x坐标
T2 m_y; //y坐标
};
template<typename T2> //这里需要带上模板头
void Point<char*, T2>::display() const{
cout<<"x="<<m_x<<" | y="<<m_y<<endl;
}
int main(){
( new Point<int, int>(10, 20) ) -> display();
( new Point<char*, int>("东京180度", 10) ) -> display();
( new Point<char*, char*>("东京180度", "北纬210度") ) -> display();
return 0;
}
运行结果:
x=10, y=20
x=东京180度 | y=10
x=东京180度 | y=北纬210度
本例中,T1 对应横坐标 x 的类型,我们将 T1 具体化为char*
,第 25 行代码就是类模板的部分显示具体化。
模板头template<typename T2>
中声明的是没有被具体化的类型参数;类名Point<char*, T2>
列出了所有类型参数,包括未被具体化的和已经被具体化的。
类名后面之所以要列出所有的类型参数,是为了让编译器确认“到底是第几个类型参数被具体化了”,如果写作template<typename T2> class Point<char*>
,编译器就不知道char*代表的是第一个类型参数,还是第二个类型参数。
C++模板中的非类型参数
模板是一种泛型技术,目的是将数据的类型参数化,以增强 C++ 语言(强类型语言)的灵活性。C++ 对模板的支持非常自由,模板中除了可以包含类型参数,还可以包含非类型参数,例如:
template<typename T, int N> class Demo{ };template<class T, int N> void func(T (&arr)[N]);
T 是一个类型参数,它通过class
或typename
关键字指定。N 是一个非类型参数,用来传递数据的值,而不是类型,它和普通函数的形参一样,都需要指明具体的类型。类型参数和非类型参数都可以用在函数体或者类体中。
当调用一个函数模板或者通过一个类模板创建对象时,非类型参数会被用户提供的、或者编译器推断出的值所取代。
在函数模板中使用非类型参数
在《C++函数模板的重载》一节中,我们通过 Swap() 函数来交换两个数组的值,其原型为:
template void Swap(T a[], T b[], int len);
形参 len 用来指明要交换的数组的长度,调用 Swap() 函数之前必须先通过sizeof
求得数组长度再传递给它。
有读者可能会疑惑,为什么在函数内部不能求得数组长度,一定要通过形参把数组长度传递进去呢?这是因为数组在作为函数参数时会自动转换为数组指针,而sizeof
只能通过数组名求得数组长度,不能通过数组指针求得数组长度,这在《数组和指针绝不等价,数组是另外一种类型》《数组到底在什么时候会转换为指针》两节中已经作了详细讲解。
多出来的形参 len 给编码带来了不便,我们可以借助模板中的非类型参数将它消除,请看下面的代码:
template<typename T, unsigned N> void Swap(T (&a)[N], T (&b)[N]){
T temp;
for(int i=0; i<N; i++){
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
T (&a)[N]
表明 a 是一个引用,它引用的数据的类型是T [N]
,也即一个数组;T (&b)[N]
也是类似的道理。分析一个引用和分析一个指针的方法类似,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析,这一点已在《只需一招,彻底攻克C语言指针》中进行了讲解。
调用 Swap() 函数时,需要将数组名字传递给它:
int a[5] = { 1, 2, 3, 4, 5 };
int b[5] = { 10, 20, 30, 40, 50 };
Swap(a, b);
编译器会使用数组类型int
来代替类型参数T
,使用数组长度5
来代替非类型参数N
。
下面是一个完整的示例:
#include <iostream>
using namespace std;
template<class T> void Swap(T &a, T &b); //模板①:交换基本类型的值
template<typename T, unsigned N> void Swap(T (&a)[N], T (&b)[N]); //模板②:交换两个数组
template<typename T, unsigned N> void printArray(T (&arr)[N]); //打印数组元素
int main(){
//交换基本类型的值
int m = 10, n = 99;
Swap(m, n); //匹配模板①
cout<<m<<", "<<n<<endl;
//交换两个数组
int a[5] = { 1, 2, 3, 4, 5 };
int b[5] = { 10, 20, 30, 40, 50 };
Swap(a, b); //匹配模板②
printArray(a);
printArray(b);
return 0;
}
template<class T> void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
template<typename T, unsigned N> void Swap(T (&a)[N], T (&b)[N]){
T temp;
for(int i=0; i<N; i++){
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
template<typename T, unsigned N> void printArray(T (&arr)[N]){
for(int i=0; i<N; i++){
if(i == N-1){
cout<<arr[i]<<endl;
}else{
cout<<arr[i]<<", ";
}
}
}
运行结果:
99, 10
10, 20, 30, 40, 50
1, 2, 3, 4, 5
printArray() 也使用了非类型参数,这样只传递数组名字就能够打印数组元素了。
在类模板中使用非类型参数
C/C++ 规定,数组一旦定义后,它的长度就不能改变了;换句话说,数组容量不能动态地增大或者减小。这样的数组称为静态数组(Static array)。静态数组有时候会给编码代码不便,我们可以通过自定义的 Array 类来实现动态数组(Dynamic array)。所谓动态数组,是指数组容量能够在使用的过程中随时增大或减小。
动态数组的完整实现代码如下:
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
template<typename T, int N>
class Array{
public:
Array();
~Array();
public:
T & operator[](int i); //重载下标运算符[]
int length() const { return m_length; } //获取数组长度
bool capacity(int n); //改变数组容量
private:
int m_length; //数组的当前长度
int m_capacity; //当前内存的容量(能容乃的元素的个数)
T *m_p; //指向数组内存的指针
};
template<typename T, int N>
Array<T, N>::Array(){
m_p = new T[N];
m_capacity = m_length = N;
}
template<typename T, int N>
Array<T, N>::~Array(){
delete[] m_p;
}
template<typename T, int N>
T & Array<T, N>::operator[](int i){
if(i<0 || i>=m_length){
cout<<"Exception: Array index out of bounds!"<<endl;
}
return m_p[i];
}
template<typename T, int N>
bool Array<T, N>::capacity(int n){
if(n > 0){ //增大数组
int len = m_length + n; //增大后的数组长度
if(len <= m_capacity){ //现有内存足以容纳增大后的数组
m_length = len;
return true;
}else{ //现有内存不能容纳增大后的数组
T *pTemp = new T[m_length + 2 * n * sizeof(T)]; //增加的内存足以容纳 2*n 个元素
if(pTemp == NULL){ //内存分配失败
cout<<"Exception: Failed to allocate memory!"<<endl;
return false;
}else{ //内存分配成功
memcpy( pTemp, m_p, m_length*sizeof(T) );
delete[] m_p;
m_p = pTemp;
m_capacity = m_length = len;
}
}
}else{ //收缩数组
int len = m_length - abs(n); //收缩后的数组长度
if(len < 0){
cout<<"Exception: Array length is too small!"<<endl;
return false;
}else{
m_length = len;
return true;
}
}
}
int main(){
Array<int, 5> arr;
//为数组元素赋值
for(int i=0, len=arr.length(); i<len; i++){
arr[i] = 2*i;
}
//第一次打印数组
for(int i=0, len=arr.length(); i<len; i++){
cout<<arr[i]<<" ";
}
cout<<endl;
//扩大容量并为增加的元素赋值
arr.capacity(8);
for(int i=5, len=arr.length(); i<len; i++){
arr[i] = 2*i;
}
//第二次打印数组
for(int i=0, len=arr.length(); i<len; i++){
cout<<arr[i]<<" ";
}
cout<<endl;
//收缩容量
arr.capacity(-4);
//第三次打印数组
for(int i=0, len=arr.length(); i<len; i++){
cout<<arr[i]<<" ";
}
cout<<endl;
return 0;
}
运行结果:
0 2 4 6 8
0 2 4 6 8 10 12 14 16 18 20 22 24
0 2 4 6 8 10 12 14 16
Array 是一个类模板,它有一个类型参数T
和一个非类型参数N
,T 指明了数组元素的类型,N 指明了数组长度。
capacity() 成员函数是 Array 类的关键,它使得数组容量可以动态地增加或者减小。传递给它一个正数时,数组容量增大;传递给它一个负数时,数组容量减小。
之所以能通过[ ]
来访问数组元素,是因为在 Array 类中以成员函数的形式重载了[ ]
运算符,并且返回值是数组元素的引用。如果直接返回数组元素的值,那么将无法给数组元素赋值。
非类型参数的限制
非类型参数的类型不能随意指定,它受到了严格的限制,只能是一个整数,或者是一个指向对象或函数的指针(也可以是引用)。引用和指针在本质上是一样的,我们已在《引用在本质上是什么,它和指针到底有什么区别》中讲到。
- 当非类型参数是一个整数时,传递给它的实参,或者由编译器推导出的实参必须是一个常量表达式,例如
10
、2 * 30
、18 + 23 - 4
等,但不能是n
、n + 10
、n + m
等(n 和 m 都是变量)。
对于上面的 Swap() 函数,下面的调用就是错误的:
int len;
cin>>len;
int a[len];
int b[len];
Swap(a, b);
对上面的 Array 类,以下创建对象的方式是错误的:
int len;
cin>>len;
Array<int, len> arr;
这两种情况,编译器推导出来的实参是 len,是一个变量,而不是常量。
- 当非类型参数是一个指针(引用)时,绑定到该指针的实参必须具有静态的生存期;换句话说,实参必须存储在虚拟地址空间中的静态数据区。局部变量位于栈区,动态创建的对象位于堆区,它们都不能用作实参。
C++模板的实例化
模板(Templet)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张“图纸”。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数或类的过程叫做模板的实例化(Instantiate),相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例(Instantiation)。
在学习模板以前,如果想针对不同的类型使用相同的算法,就必须定义多个极其相似的函数或类,这样不但做了很多重复性的工作,还导致代码维护困难,用于交换两个变量的值的 Swap() 函数就是一个典型的代表。而有了模板后,这些工作都可以交给编译器了,编译器会帮助我们自动地生成这些代码。从这个角度理解,模板也可以看做是编译器的一组指令,它命令编译器生成我们想要的代码。
模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类,并且相同的类型只生成一次。实例化的过程也很简单,就是将所有的类型参数用实参代替。
例如,给定下面的函数模板和函数调用:
template<typename T> void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
int main(){
int n1 = 100, n2 = 200, n3 = 300, n4 = 400;
float f1 = 12.5, f2 = 56.93;
Swap(n1, n2); //T为int,实例化出 void Swap(int &a, int &b);
Swap(f1, f2); //T为float,实例化出 void Swap(float &a, float &b);
Swap(n3, n4); //T为int,调用刚才生成的 void Swap(int &a, int &b);
return 0;
}
编译器会根据不同的实参实例化出不同版本的 Swap() 函数。对于Swap(n1, n2)
调用,编译器会生成并编译一个 Swap() 版本,其中 T 被替换为 int:
void Swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
对于Swap(f1, f2)
调用,编译器会生成另一个 Swap() 版本,其中 T 被替换为 float。对于Swap(n3, n4)
调用,编译器不会再生成新版本的 Swap() 了,因为刚才已经针对 int 生成了一个版本,直接拿来使用即可。
另外需要注意的是类模板的实例化,通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码。
通过类模板创建对象时,一般只需要实例化成员变量和构造函数。成员变量被实例化后就能够知道对象的大小了(占用的字节数),构造函数被实例化后就能够知道如何初始化了;对象的创建过程就是分配一块大小已知的内存,并对这块内存进行初始化。
请看下面的例子:
#include <iostream>
using namespace std;
template<class T1, class T2>
class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; };
void setY(T2 y){ m_y = y; };
void display() const;
private:
T1 m_x;
T2 m_y;
};
template<class T1, class T2>
void Point<T1, T2>::display() const{
cout<<"x="<<m_x<<", y="<<m_y<<endl;
}
int main(){
Point<int, int> p1(10, 20);
p1.setX(40);
p1.setY(50);
cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
Point<char*, char*> p2("东京180度", "北纬210度");
p2.display();
return 0;
}
运行结果:
x=40, y=50
x=东京180度, y=北纬210度
p1 调用了所有的成员函数,整个类会被完整地实例化。p2 只调用了构造函数和 display() 函数,剩下的 get 函数和 set 函数不会被实例化。
值得提醒的是,Point<int, int>
和Point<char*, char*>
是两个相互独立的类,它们的类型是不同的,不能相互兼容,也不能自动地转换类型,所以诸如p1 = p2;
这样的语句是错误的,除非重载了=
运算符。
将C++模板应用于多文件编程
在将函数应用于多文件编程时,我们通常是将函数定义放在源文件(.cpp
文件)中,将函数声明放在头文件(.h
文件)中,使用函数时引入(#include
命令)对应的头文件即可。
编译是针对单个源文件的,只要有函数声明,编译器就能知道函数调用是否正确;而将函数调用和函数定义对应起来的过程,可以延迟到链接时期。正是有了链接器的存在,函数声明和函数定义的分离才得以实现。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; };
void setY(T2 y){ m_y = y; };
void display() const;
private:
T1 m_x;
T2 m_y;
};
template<class T1, class T2>
void Point<T1, T2>::display() const{
cout<<“x=”<<m_x<<“, y=”<<m_y<<endl;
}
int main(){
Point<int, int> p1(10, 20);
p1.setX(40);
p1.setY(50);
cout<<“x=”<<p1.getX()<<“, y=”<<p1.getY()<<endl;
Point<char*, char*> p2(“东京180度”, “北纬210度”);
p2.display();
return 0;
}
运行结果:
x=40, y=50
x=东京180度, y=北纬210度
p1 调用了所有的成员函数,整个类会被完整地实例化。p2 只调用了构造函数和 display() 函数,剩下的 get 函数和 set 函数不会被实例化。
值得提醒的是,`Point<int, int>`和`Point<char*, char*>`是两个相互独立的类,它们的类型是不同的,不能相互兼容,也不能自动地转换类型,所以诸如`p1 = p2;`这样的语句是错误的,除非重载了`=`运算符。
## 将C++模板应用于多文件编程
在将函数应用于多文件编程时,我们通常是将函数定义放在源文件(`.cpp`文件)中,将函数声明放在头文件(`.h`文件)中,使用函数时引入(`#include`命令)对应的头文件即可。
编译是针对单个源文件的,只要有函数声明,编译器就能知道函数调用是否正确;而将函数调用和函数定义对应起来的过程,可以延迟到链接时期。正是有了链接器的存在,函数声明和函数定义的分离才得以实现。
[外链图片转存中...(img-HZuhvhhc-1715664845214)]
[外链图片转存中...(img-d3BnCvAu-1715664845215)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**