文章目录

  • C++函数模板5分钟入门教程
  • C++类模板5分钟入门教程
    • 使用类模板创建对象
    • 综合示例
  • 大话C++模板编程的来龙去脉
    • 1) 强类型语言
    • 2) 弱类型语言
  • C++函数模板的重载
  • C++函数模板的实参推断
    • 模板实参推断过程中的类型转换
    • 为函数模板显式地指明实参(也就是具体的类型)
    • 显式地指明实参时可以应用正常的类型转换
  • C++模板的显式具体化
    • 函数模板的显式具体化
      • 函数的调用规则
    • 类模板的显式具体化
    • 部分显式具体化
  • C++模板中的非类型参数
    • 在函数模板中使用非类型参数
    • 在类模板中使用非类型参数
    • 非类型参数的限制
  • C++模板的实例化
  • 将C++模板应用于多文件编程
    • 将函数模板的声明和定义分散到不同的文件
    • 将类模板的声明和实现分散到不同的文件
    • 总结
  • C++模板的显式实例化
    • 函数模板的显式实例化
    • 类模板的显式实例化
    • 总结
    • 显式实例化的缺陷
  • C++类模板中的静态成员

转载于http://c.biancheng.net/cplus/

C++函数模板5分钟入门教程

在《C++函数重载》一节中,为了交换不同类型的变量的值,我们通过函数重载定义了四个名字相同、参数列表不同的函数,如下所示:

//交换 int 变量的值
void Swap(int *a, int *b){int temp = *a;*a = *b;*b = temp;
}
//交换 float 变量的值
void Swap(float *a, float *b){float temp = *a;*a = *b;*b = temp;
}
//交换 char 变量的值
void Swap(char *a, char *b){char temp = *a;*a = *b;*b = temp;
}
//交换 bool 变量的值
void Swap(bool *a, bool *b){char temp = *a;*a = *b;*b = temp;
}

这些函数虽然在调用时方便了一些,但从本质上说还是定义了三个功能相同、函数体相同的函数,只是数据的类型不同而已,这看起来有点浪费代码,能不能把它们压缩成一个函数呢?

能!可以借助本节讲的函数模板。

我们知道,数据的值可以通过函数参数传递,在函数定义时数据的值是未知的,只有等到函数调用时接收了实参才能确定其值。这就是值的参数化。

在C++中,数据的类型也可以通过参数来传递,在函数定义时可以不指明具体的数据类型,当发生函数调用时,编译器可以根据传入的实参自动推断数据类型。这就是类型的参数化。

值(Value)和类型(Type)是数据的两个主要特征,它们在C++中都可以被参数化。

所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板(Function Template)

在函数模板中,数据的值和类型都被参数化了,发生函数调用时编译器会根据传入的实参来推演形参的值和类型。换个角度说,函数模板除了支持值的参数化,还支持类型的参数化。

一但定义了函数模板,就可以将类型参数用于函数定义和函数声明了。说得直白一点,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替。

下面我们就来实践一下,将上面的四个Swap() 函数压缩为一个函数模板:

#include <iostream>
using namespace std;
template<typename T> void Swap(T *a, T *b){T temp = *a;*a = *b;*b = temp;
}
int main(){//交换 int 变量的值int n1 = 100, n2 = 200;Swap(&n1, &n2);cout<<n1<<", "<<n2<<endl;//交换 float 变量的值float f1 = 12.5, f2 = 56.93;Swap(&f1, &f2);cout<<f1<<", "<<f2<<endl;//交换 char 变量的值char c1 = 'A', c2 = 'B';Swap(&c1, &c2);cout<<c1<<", "<<c2<<endl;//交换 bool 变量的值bool b1 = false, b2 = true;Swap(&b1, &b2);cout<<b1<<", "<<b2<<endl;return 0;
}

运行结果:
200, 100
56.93, 12.5
B, A
1, 0

请读者重点关注第 4 行代码。template是定义函数模板的关键字,它后面紧跟尖括号<>,尖括号包围的是类型参数(也可以说是虚拟的类型,或者说是类型占位符)。typename是另外一个关键字,用来声明具体的类型参数,这里的类型参数就是T。从整体上看,template<typename T>被称为模板头。

模板头中包含的类型参数可以用在函数定义的各个位置,包括返回值、形参列表和函数体;本例我们在形参列表和函数体中使用了类型参数T

类型参数的命名规则跟其他标识符的命名规则一样,不过使用 T、T1、T2、Type 等已经成为了一种惯例。

定义了函数模板后,就可以像调用普通函数一样来调用它们了。

在讲解C++函数重载时我们还没有学到引用(Reference),为了达到交换两个变量的值的目的只能使用指针,而现在我们已经对引用进行了深入讲解,不妨趁此机会来实践一把,使用引用重新实现 Swap() 这个函数模板:

#include <iostream>
using namespace std;
template<typename T> void Swap(T &a, T &b){T temp = a;a = b;b = temp;
}
int main(){//交换 int 变量的值int n1 = 100, n2 = 200;Swap(n1, n2);cout<<n1<<", "<<n2<<endl;//交换 float 变量的值float f1 = 12.5, f2 = 56.93;Swap(f1, f2);cout<<f1<<", "<<f2<<endl;//交换 char 变量的值char c1 = 'A', c2 = 'B';Swap(c1, c2);cout<<c1<<", "<<c2<<endl;//交换 bool 变量的值bool b1 = false, b2 = true;Swap(b1, b2);cout<<b1<<", "<<b2<<endl;return 0;
}

引用不但使得函数定义简洁明了,也使得调用函数方便了很多。整体来看,引用让编码更加漂亮。

下面我们来总结一下定义模板函数的语法:

template <typename 类型参数1 , typename 类型参数2 , …> 返回值类型 函数名(形参列表){
//在函数体中可以使用类型参数
}

类型参数可以有多个,它们之间以逗号,分隔。类型参数列表以< >包围,形式参数列表以( )包围。

typename关键字也可以使用class关键字替代,它们没有任何区别。C++ 早期对模板的支持并不严谨,没有引入新的关键字,而是用 class 来指明类型参数,但是 class 关键字本来已经用在类的定义中了,这样做显得不太友好,所以后来 C++ 又引入了一个新的关键字 typename,专门用来定义类型参数。不过至今仍然有很多代码在使用 class 关键字,包括 C++ 标准库、一些开源程序等。

本教程会交替使用 typename 和 class,旨在让读者在别的地方遇到它们时不会感觉陌生。更改上面的 Swap() 函数,使用 class 来指明类型参数:

template<class T> void Swap(T &a, T &b){T temp = a;a = b;b = temp;
}

除了将 typename 替换为 class,其他都是一样的。

为了加深对函数模板的理解,我们再来看一个求三个数的最大值的例子:

#include <iostream>
using namespace std;
//声明函数模板
template<typename T> T max(T a, T b, T c);
int main( ){//求三个整数的最大值int i1, i2, i3, i_max;cin >> i1 >> i2 >> i3;i_max = max(i1,i2,i3);cout << "i_max=" << i_max << endl;//求三个浮点数的最大值double d1, d2, d3, d_max;cin >> d1 >> d2 >> d3;d_max = max(d1,d2,d3);cout << "d_max=" << d_max << endl;//求三个长整型数的最大值long g1, g2, g3, g_max;cin >> g1 >> g2 >> g3;g_max = max(g1,g2,g3);cout << "g_max=" << g_max << endl;return 0;
}
//定义函数模板
template<typename T>  //模板头,这里不能有分号
T max(T a, T b, T c){ //函数头T max_num = a;if(b > max_num) max_num = b;if(c > max_num) max_num = c;return max_num;
}

运行结果:
12 34 100↙
i_max=100
73.234 90.2 878.23↙
d_max=878.23
344 900 1000↙
g_max=1000

函数模板也可以提前声明,不过声明时需要带上模板头,并且模板头和函数定义(声明)是一个不可分割的整体,它们可以换行,但中间不能有分号。

C++类模板5分钟入门教程

C++ 除了支持函数模板,还支持类模板(Class Template)。函数模板中定义的类型参数可以用在函数声明和函数定义中,类模板中定义的类型参数可以用在类声明和类实现中。类模板的目的同样是将数据的类型参数化。

声明类模板的语法为:

template<typename 类型参数1 , typename 类型参数2 , …> class 类名{
//TODO:
};

类模板和函数模板都是以 template 开头(当然也可以使用 class,目前来讲它们没有任何区别),后跟类型参数;类型参数不能为空,多个类型参数用逗号隔开。

一但声明了类模板,就可以将类型参数用于类的成员函数和成员变量了。换句话说,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替。

假如我们现在要定义一个类来表示坐标,要求坐标的数据类型可以是整数、小数和字符串,例如:

  • x = 10、y = 10
  • x = 12.88、y = 129.65
  • x = “东经180度”、y = “北纬210度”

这个时候就可以使用类模板,请看下面的代码:

template<typename T1, typename 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坐标
};

x 坐标和 y 坐标的数据类型不确定,借助类模板可以将数据类型参数化,这样就不必定义多个类了。

注意:模板头和类头是一个整体,可以换行,但是中间不能有分号。

上面的代码仅仅是类的声明,我们还需要在类外定义成员函数。在类外定义成员函数时仍然需要带上模板头,格式为:

template<typename 类型参数1 , typename 类型参数2 , …>
返回值类型 类名<类型参数1 , 类型参数2, …>::函数名(形参列表){
//TODO:
}

第一行是模板头,第二行是函数头,它们可以合并到一行,不过为了让代码格式更加清晰,一般是将它们分成两行。

下面就对 Point 类的成员函数进行定义:

template<typename T1, typename T2>  //模板头
T1 Point<T1, T2>::getX() const /*函数头*/ {return m_x;
}
template<typename T1, typename T2>
void Point<T1, T2>::setX(T1 x){m_x = x;
}
template<typename T1, typename T2>
T2 Point<T1, T2>::getY() const{return m_y;
}
template<typename T1, typename T2>
void Point<T1, T2>::setY(T2 y){m_y = y;
}

请读者仔细观察代码,除了 template 关键字后面要指明类型参数,类名 Point 后面也要带上类型参数,只是不加 typename 关键字了。另外需要注意的是,在类外定义成员函数时,template 后面的类型参数要和类声明时的一致。

使用类模板创建对象

上面的两段代码完成了类的定义,接下来就可以使用该类创建对象了。使用类模板创建对象时,需要指明具体的数据类型。请看下面的代码:

Point<int, int> p1(10, 20);
Point<int, float> p2(10, 15.5);
Point<float, char*> p3(12.4, "东经180度");

与函数模板不同的是,类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型。

除了对象变量,我们也可以使用对象指针的方式来实例化:

Point<float, float> *p1 = new Point<float, float>(10.6, 109.3);
Point<char*, char*> *p = new Point<char*, char*>("东经180度", "北纬210度");

需要注意的是,赋值号两边都要指明具体的数据类型,且要保持一致。下面的写法是错误的:

//赋值号两边的数据类型不一致
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); //用于在数组尾部添加一个元素vCArray & 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;elseptr = 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 是一个类型参数,它通过classtypename关键字指定。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 类中以成员函数的形式重载了[ ]运算符,并且返回值是数组元素的引用。如果直接返回数组元素的值,那么将无法给数组元素赋值。

非类型参数的限制

非类型参数的类型不能随意指定,它受到了严格的限制,只能是一个整数,或者是一个指向对象或函数的指针(也可以是引用)。引用和指针在本质上是一样的,我们已在《引用在本质上是什么,它和指针到底有什么区别》中讲到。

  1. 当非类型参数是一个整数时,传递给它的实参,或者由编译器推导出的实参必须是一个常量表达式,例如102 * 3018 + 23 - 4等,但不能是nn + 10n + 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,是一个变量,而不是常量。

  1. 当非类型参数是一个指针(引用)时,绑定到该指针的实参必须具有静态的生存期;换句话说,实参必须存储在虚拟地址空间中的静态数据区。局部变量位于栈区,动态创建的对象位于堆区,它们都不能用作实参。

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命令)对应的头文件即可。

编译是针对单个源文件的,只要有函数声明,编译器就能知道函数调用是否正确;而将函数调用和函数定义对应起来的过程,可以延迟到链接时期。正是有了链接器的存在,函数声明和函数定义的分离才得以实现。

将类应用于多文件编程也是类似的道理,我们可以将类的声明和类的实现分别放到头文件和源文件中。类的声明已经包含了所有成员变量的定义和所有成员函数的声明(也可以是 inline 形式的定义),这样就知道如何创建对象了,也知道如何调用成员函数了,只是还不能将函数调用与函数实现对应起来,但是这又有什么关系呢,反正链接器可以帮助我们完成这项工作。

总起来说,不管是函数还是类,声明和定义(实现)的分离其实是一回事,都是将函数定义放到其他文件中,最终要解决的问题也只有一个,就是把函数调用和函数定义对应起来(找到函数定义的地址,并填充到函数调用处),而保证完成这项工作的就是链接器。

基于传统的编程思维,初学者往往也会将模板(函数模板和类模板)的声明和定义分散到不同的文件中,以期达到「模块化编程」的目的。但事实证明这种做法是不对的,程序员惯用的做法是将模板的声明和定义都放到头文件中。

模板并不是真正的函数或类,它仅仅是用来生成函数或类的一张“图纸”,在这个生成过程中有三点需要明确:

  • 模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码;
  • 模板的实例化是由编译器完成的,而不是由链接器完成的;
  • 在实例化过程中需要知道模板的所有细节,包含声明和定义。

将函数模板的声明和定义分散到不同的文件

为了更加深入地说明问题,现在有一个反面教材,它将函数模板的声明和实现分别放到了头文件和源文件。

func.cpp 源码:

//交换两个数的值
template<typename T> void Swap(T &a, T &b){T temp = a;a = b;b = temp;
}
//冒泡排序算法
void bubble_sort(int arr[], int n){for(int i=0; i<n-1; i++){bool isSorted = true;for(int j=0; j<n-1-i; j++){if(arr[j] > arr[j+1]){isSorted = false;Swap(arr[j], arr[j+1]);  //调用Swap()函数}}if(isSorted) break;}
}

func.h 源码:

#ifndef _FUNC_H
#define _FUNC_H
template<typename T> void Swap(T &a, T &b);
void bubble_sort(int arr[], int n);
#endif

main.cpp 源码

#include <iostream>
#include "func.h"
using namespace std;
int main(){int n1 = 10, n2 = 20;Swap(n1, n2);double f1 = 23.8, f2 = 92.6;Swap(f1, f2);return 0;
}

该工程包含了两个源文件和一个头文件,func.cpp中定义了两个函数,func.h中对函数进行了声明,main.cpp中对函数进行了调用,这是典型的将函数的声明和实现分离的编程模式。

运行上面的程序,会产生一个链接错误,意思是无法找到void Swap<double>(double &, double &)这个函数。主函数 main() 中共调用了两个版本的 Swap() 函数,它们的原型分别是:

void Swap(int &, int &);
void Swap(double &, double &);

为什么针对 int 的版本能够找到定义,而针对 double 的版本就找不到呢?

我们先来说针对 double 的版本为什么找不到定义。当编译器编译main.cpp时,发现使用到了 double 版本的 Swap() 函数,于是尝试生成一个 double 版本的实例,但是由于只有声明没有定义,所以生成失败。不过这个时候编译器不会报错,而是对该函数的调用做一个记录,希望等到链接程序时在其他目标文件(.obj 文件或 .o 文件)中找到该函数的定义。很明显,本例需要到func.obj中寻找。但是遗憾的是,func.cpp中没有调用 double 版本的 Swap() 函数,编译器不会生成 double 版本的实例,所以链接器最终也找不到 double 版本的函数定义,只能抛出一个链接错误,让程序员修改代码。

那么,针对 int 的版本为什么能够找到定义呢?请读者注意bubble_sort()函数,该函数用来对数组元素进行排序,在排序过程中需要频繁的交换两个元素的值,所以调用了 Swap() 函数,这样做的结果是:编译生成的func.obj中会有一个 int 版本的 Swap() 函数定义。编译器在编译main.cpp时虽然找不到 int 版本的实例,但是等到链接程序时,链接器在func.obj中找到了,所以针对 int 版本的调用就不会出错。

将类模板的声明和实现分散到不同的文件

我们再看一个类模板的反面教材,它将类模板的声明和实现分别放到了头文件和源文件。

point.h 源码:

#ifndef _POINT_H
#define _POINT_H
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;
};
#endif

point.cpp 源码:

#include <iostream>
#include "point.h"
using namespace std;
template<class T1, class T2>
void Point<T1, T2>::display() const{cout<<"x="<<m_x<<", y="<<m_y<<endl;
}

main.cpp 源码:

#include <iostream>
#include "point.h"
using namespace std;
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;
}

该工程包含了两个源文件和一个头文件,point.h中声明了类模板,point.cpp中对类模板进行了实现,main.cpp中通过类模板创建了对象,并调用了成员函数,这是典型的将类的声明和实现分离的编程模式。

运行上面的程序,会产生一个链接错误,意思是无法通过 p2 调用Point<char*, char*>::display() const这个函数。

类模板声明位于point.h中,它包含了所有成员变量的定义以及构造函数、get 函数、set 函数的定义,这些信息足够创建出一个完整的对象了,并且可以通过对象调用 get 函数和 set 函数,所以main.cpp的前 11 行代码都不会报错。而第 12 行代码调用了 display() 函数,该函数的定义位于point.cpp文件中,并且point.cpp中也没有生成对应的实例,所以会在链接期间抛出错误。

总结

通过上面的两个反面教材可以总结出,「不能将模板的声明和定义分散到多个文件中」的根本原因是:模板的实例化是由编译器完成的,而不是由链接器完成的,这可能会导致在链接期间找不到对应的实例。

修复上面两个项目的方法也很简单,就是将 func.cpp、point.cpp 的模板定义(实现)部分分别合并到 func.h、point.h 中。由于篇幅限制,这里不再给出具体的代码了,请读者到百度网盘下载正确的代码:

  • 下载地址:https://pan.baidu.com/s/1eRO1AKE
  • 提取密码:yp3u

C++模板的显式实例化

前面讲到的模板的实例化是在调用函数或者创建对象时由编译器自动完成的,不需要程序员引导,因此称为隐式实例化。相对应的,我们也可以通过代码明确地告诉编译器需要针对哪个类型进行实例化,这称为显式实例化。

编译器在实例化的过程中需要知道模板的所有细节:对于函数模板,也就是函数定义;对于类模板,需要同时知道类声明和类定义。我们必须将显式实例化的代码放在包含了模板定义的源文件中,而不是仅仅包含了模板声明的头文件中。

显式实例化的一个好处是,可以将模板的声明和定义(实现)分散到不同的文件中了。

函数模板的显式实例化

以上节讲到的 compare() 函数为例,针对 double 类型的显式实例化代码为:

template void Swap(double &a, double &b);

这条语言由两部分组成,前边是一个template关键字(后面不带< >),后面是一个普通的函数原型,组合在一起的意思是:将模板实例化成和函数原型对应的一个具体版本。

将该代码放到 func.cpp 文件的最后,再运行程序就不会出错了。

另外,还可以在包含了函数调用的源文件(main.cpp)中再增加下面的一条语句:

extern template void Swap(double &a, double &b);

该语句在前面增加了extern关键字,它的作用是明确地告诉编译器,该版本的函数实例在其他文件中,请在链接期间查找。不过这条语句是多余的,即使不写,编译器发现当前文件中没有对应的模板定义,也会自动去其他文件中查找。

上节我们展示了一个反面教材,告诉大家不能把函数模板的声明和定义分散到不同的文件中,但是现在有了显式实例化,这一点就可以做到了,下面就对上节的代码进行修复。

func.cpp 源码:

//交换两个数的值
template<typename T> void Swap(T &a, T &b){T temp = a;a = b;b = temp;
}
//冒泡排序算法
void bubble_sort(int arr[], int n){for(int i=0; i<n-1; i++){bool isSorted = true;for(int j=0; j<n-1-i; j++){if(arr[j] > arr[j+1]){isSorted = false;Swap(arr[j], arr[j+1]);  //调用Swap()函数}}if(isSorted) break;}
}
template void Swap(double &a, double &b);  //显式实例化定义

func.h 源码:

#ifndef _FUNC_H
#define _FUNC_H
template<typename T> void Swap(T &a, T &b);
void bubble_sort(int arr[], int n);
#endif

main.cpp 源码:

#include <iostream>
#include "func.h"
using namespace std;
//显示实例化声明(也可以不写)
extern template void Swap(double &a, double &b);
extern template void Swap(int &a, int &b);
int main(){int n1 = 10, n2 = 20;Swap(n1, n2);double f1 = 23.8, f2 = 92.6;Swap(f1, f2);return 0;
}

显式实例化也包括声明和定义,定义要放在模板定义(实现)所在的源文件,声明要放在模板声明所在的头文件(当然也可以不写)。

类模板的显式实例化

类模板的显式实例化和函数模板类似。以上节的 Point 类为例,针对char*类型的显式实例化(定义形式)代码为:

template class Point<char*, char*>;

相应地,它的声明形式为:

extern template class Point<char*, char*>;

不管是声明还是定义,都要带上class关键字,以表明这是针对类模板的。

另外需要注意的是,显式实例化一个类模板时,会一次性实例化该类的所有成员,包括成员变量和成员函数。

有了类模板的显式实例化,就可以将类模板的声明和定义分散到不同的文件中了,下面我们就来修复上节的代码。

point.cpp 源文件:

#include <iostream>
#include "point.h"
using namespace std;
template<class T1, class T2>
void Point<T1, T2>::display() const{cout<<"x="<<m_x<<", y="<<m_y<<endl;
}
//显式实例化定义
template class Point<char*, char*>;
template class Point<int, int>;

point.h 源码:

#ifndef _POINT_H
#define _POINT_H
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;
};
#endif

main.cpp 源码:

#include <iostream>
#include "point.h"
using namespace std;
//显式实例化声明(也可以不写)
extern template class Point<char*, char*>;
extern template class Point<int, int>;
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;
}

总结

函数模板和类模板的实例化语法是类似的,我们不妨对它们做一下总结:

extern template declaration; //实例化声明
template declaration; //实例化定义

对于函数模板来说,declaration 就是一个函数原型;对于类模板来说,declaration 就是一个类声明。

显式实例化的缺陷

C++ 支持显式实例化的目的是为「模块化编程」提供一种解决方案,这种方案虽然有效,但是也有明显的缺陷:程序员必须要在模板的定义文件(实现文件)中对所有使用到的类型进行实例化。这就意味着,每次更改了模板使用文件(调用函数模板的文件,或者通过类模板创建对象的文件),也要相应地更改模板定义文件,以增加对新类型的实例化,或者删除无用类型的实例化。

一个模板可能会在多个文件中使用到,要保持这些文件的同步更新是非常困难的。而对于库的开发者来说,他不能提前假设用户会使用哪些类型,所以根本就无法使用显式实例化,只能将模板的声明和定义(实现)全部放到头文件中;C++ 标准库几乎都是用模板来实现的,这些模板的代码也都位于头文件中。

总起来说,如果我们开发的模板只有我们自己使用,那也可以勉强使用显式实例化;如果希望让其他人使用(例如库、组件等),那只能将模板的声明和定义都放到头文件中了。

C++类模板中的静态成员

类模板中可以定义静态成员,从该类模板实例化得到的所有类都包含同样的静态成员。

程序示例如下:

#include <iostream>
using namespace std;
template <class T>
class A
{
private:static int count;
public:A() { count ++; }~A() { count -- ; };A(A &) { count ++ ; }static void PrintCount() { cout << count << endl; }
};
template<> int A<int>::count = 0;
template<> int A<double>::count = 0;
int main()
{A<int> ia;A<double> da;ia.PrintCount();da.PrintCount();return 0;
}

程序的输出结果是:
1
1

第 14 行和第 15 行,对静态成员变量在类外部加以声明是必需的。在 Visual Studio 2008 中,这两行也可以简单地写成:

int A::count = 0;
int A::count = 0;

A 和 A 是两个不同的类。虽然它们都有静态成员变量 count,但是显然,A 的对象 ia 和 A 的对象 da 不会共享一份 count。

【C++学习笔记】C++模板相关推荐

  1. OpenCV 学习笔记(模板匹配)

    OpenCV 学习笔记(模板匹配) 模板匹配是在一幅图像中寻找一个特定目标的方法之一.这种方法的原理非常简单,遍历图像中的每一个可能的位置,比较各处与模板是否"相似",当相似度足够 ...

  2. D3D9学习笔记之模板

    学习目标: 理解模板缓存的工作原理,如何创建模板缓存以及如何对模板缓存进行控制 了解如何实现镜面效果,以及如何使用模板缓存阻止物体映像在非镜面区域中的绘制 掌握如何绘制阴影,以及如何借助模板缓存阻止& ...

  3. C++学习笔记【模板】

    主要内容来源:https://www.bilibili.com/video/BV1et411b73Z?spm_id_from=333.999.0.0 一.函数模板 1.函数模板的概念 template ...

  4. Python学习笔记——Flask模板

    目录 3.1 模板 3.2 过滤器: 3.3 Web表单: 3.4 控制语句 3.5 宏.继承.包含 3.6 Flask中的特殊变量和方法: 3.1 模板 在前面的示例中,视图函数的主要作用是生成请求 ...

  5. 黑马ajax学习笔记02--art-template模板,自动提示,防抖,三级联动,fromData传参及传文件,同源,jsonp,天气预报,CROS,服务器桥接,withCredential跨域登录

    1.模板引擎概述 作用:使用模板引擎提供的模板语法,可以将数据和HTML拼接起来 实际上是实现在客户端做数据拼接 art-template模板引擎 官网:http://aui.github.io/ar ...

  6. C++学习笔记:模板参数

    本章节主要学习一下模板形参的基本知识. 模板参数有三种类型:类型模板参数.模板的模板参数(以模板作为模板的参数).非类型模板参数. 类型模板参数 类型模板参数是我们使用模板的主要目的.也就是普通的类型 ...

  7. Django学习笔记之模板渲染、模板语言、simple_tag、母版子版、静态配置文件

    一.首先我们用PyCharm来创建一个Django项目 终端命令:django-admin startproject sitename 图形创建: 这样一个Django项目就创建完成了,上面可以看到项 ...

  8. Symfony2 学习笔记之模板使用

    我们知道,controller负责处理每一个进入Symfony2应用程序的请求.实际上,controller把大部分的繁重工作都委托给了其它地方,以使代码能够被测试和重用.当一个controller需 ...

  9. C++ Primer 学习笔记_75_模板与泛型编程 --模板定义

    模板与泛型编程 --模板定义 引言: 所谓泛型程序就是以独立于不论什么特定类型的方式编写代码.使用泛型程序时,我们须要提供详细程序实例所操作的类型或值. 模板是泛型编程的基础.使用模板时能够无须了解模 ...

  10. c++学习笔记之类模板

    类是对象的抽象,类模板是类的抽象. 比较两个数(不同类型)的大小 在类模板内定义成员函数 #include<iostream> using namespace std; template& ...

最新文章

  1. 如何评估两张图片的差异
  2. linux-----shell高级编程----grep应用
  3. excel调用python编程-使用python集合进行EXCEL数据分析
  4. matlab的message函数,matlab用于数字调制,几个函数的使用问题
  5. Python3之打印出map结果的print(list())用法
  6. Android开源之BaseRecyclerViewAdapterHelper(持续更新!)
  7. 周例会会议、汇报框架
  8. SwiftUI 3.0调用SDWebImageSwiftUI 第三方框架
  9. mysql数据库表复用_MySQL 数据库之表操作
  10. linux 批量创建用户和删除用户
  11. 二分查找的平均查找长度详解【转】
  12. 【Mac】一些软件的图片和视频位置 QQ 微信
  13. 【QT】QT从零入门教程(九):QT常用控件 [QSlider、QSpinBox、QComboBox、QRadioButton]
  14. python实例26[计算MD5]
  15. Oracle 性能诊断艺术 第四章 笔记
  16. hustoj搭建教程
  17. bulma css 中文,Bulma CSS - CSS类
  18. 浅析2017年医疗类APP开发前景
  19. Python练手----字符串的密钥加密
  20. 全加器——Verilog HDL语言

热门文章

  1. SQL2012清除数据库日志
  2. PADS安装教程(超详细)
  3. 你不好奇 CPU 是如何执行任务的吗?
  4. 智能手势交互全攻略,知识地图
  5. iOS中头条新闻滑动效果
  6. Megatron-LM GPT2
  7. 2009 SAP全球技术研发大会圆满落幕
  8. 软碟通制作U盘启动盘,安装window10系统2020-02-07
  9. R语言导入excel表格数据——Please provide a sheet name OR a sheet index.
  10. RPG游戏自动打怪之朝向判断