注:本文的例子全部都是c语言中文网上的c++教程,自己只不过是运行了一遍而已。个别地方添加了点东西。

17.1模板函数教程

1.自己的第一个运行的程序(c语言中文网上源代码)

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
//#include "stdafx.h"
#include <string>
#include <iostream>
using namespace std;
template<typename T> void swap(T * p1, T  *p2)
{T temp = *p1;*p1 = *p2;*p2 = temp;
}int main()
{int n1 = 100, n2 = 29;swap(&n1, &n2);cout << n1 <<"  "<< n2 << endl;char c1 = 'm', c2 = 'n';swap(c1,c2);cout << c1 << " " << c2 << endl;double d1 = 1.23, d2 = 2.34;swap(d1,d2);cout << d1 << "  " << d2 << endl;return 0;
}

运行结果:

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

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

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

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

为了加深模板的理解,下面再看一个例子:

// 求三个数的最大的.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <string>
#include <iostream>
#include <memory>
using namespace std;//模板的声明和普通函数声明也是类似的(我自己理解是:模板函数体去掉,剩下的就是声明了。)
template <typename T> T max(T &, T &, T &);int _tmain(int argc, _TCHAR* argv[])
{int a = 12, b = 189, c = 2002;auto re = max(a,b,c);cout << re << endl;return 0;
}template <typename T>  T max(T &a, T &b, T &c)
{T temp;temp = (a > b ? a : b);temp = (temp > c ? temp : c);return temp;
}
函数模板也可以提前声明,不过声明时需要带上模板头,并且模板头和函数定义(声明)是一个不可分割的整体,它们可以换行,但中间不能有分号。

7.2 类模板教程

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度"

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

// 第一个例子.cpp : 定义控制台应用程序的入口点。
//#include "stdafx.h"
#include <string>
#include <iostream>
using namespace std;template <typename T1,typename T2>
class Point
{
public:Point(const T1 & t1, const T2 &t2) :m_x(t1), m_y(t2){}
public:T1 getX()const; //获取x坐标void setX(T1 x); //设置x坐标T2 getY()const; //获取y坐标void setY(T2 y);//设置y坐标private:T1 m_x;T2 m_y;
};int _tmain(int argc, _TCHAR* argv[])
{
Point<int, int> p1(10,20);Point<int, float> p2(10,12.5);Point<float, char*> p3(12.5,"东经180度");Point<int,int>* p4 = new Point<int, int>(10, 20);Point<int,double>* p5 = new Point<int, double>(10,15.123);cout << p1.getX() << endl;cout << p1.getY() << endl;cout << p2.getX() << endl;cout << p2.getY() << endl;cout << p3.getX() << endl;cout << p3.getY() << endl;cout << p4->getX() << endl;cout << p4->getY() << endl;cout << p5->getX() << endl;cout << p5->getY() << endl;return 0;
}

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)
{return m_y;
}

使用类模板创建对象

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

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

与函数模板不同的是

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

注意:类外定义的成员函数首先要加模板头,接着要在类名后面的尖括号内添加具体的参数类型。

所以类内定义的成员函数和类外定义的成员函数是有区别的。

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

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

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

  1. //赋值号两边的数据类型不一致
  2. Point<float, float> *p = new Point<float, int>(10.6, 109);
  3. //赋值号右边没有指明数据类型
  4. Point<float, float> *p = new Point(10.6, 109);

7.3  C++函数模板的重载

注意标准库也有一个函数叫swap,所以有时候调用会和标准库的swap冲突,这时候就需要加作用域运算符。

#include <string>
#include <iostream>
#include <memory>
using namespace std;template <typename T,unsigned LEN> void swap(T a[LEN],T b[LEN])
{
for (int i = 0; i !=LEN; ++ i){T temp =  a[i];a[i] = b[i];b[i] = temp;   }
}template<typename T> void swap(T a[],T b[],int len)
{
for(int i = 0;i != len; ++i){T temp = a[i];a[i] = b[i];b[i] = temp;  }}template <typename T>
void swap(T & a ,T & b)
{T temp = a;a = b;b = temp;cout << "call void swap(T &,T &)." << endl;
}int main()
{
int a[6] = {1,2,3,4,5,6};
int b[] = {11,22,33,44,55,66};
swap(a,b);
for(int i = 0;i != sizeof(a) / sizeof(a[0]); ++ i)cout << a[i] << endl;int x = 1.1,y = 1234.2;
::swap(x,y);   //这里加::,为了调用全局作用域定义的swap,加了全局作用域运算符::
cout << x << y << endl;
int l = sizeof(a) / sizeof(a[0]);
swap(a,b,l);
for(int i = 0;i != l ;++ i){cout << a[i] << endl;}return 0;
}

在第二个函数模板中,最后一个参数的类型为具体类型(int),而不是泛型。并不是所有的模板参数都必须被泛型化。

7.6 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)

函数模板和类模板都可以显示具体化,下面我们先讲解函数模板的显示具体化,再讲解类模板的显示具体化。

函数模板的显式具体化

定义:让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的显示具体化(Explicit Specialization)

在讲解函数模板的显示具体化语法之前,我们先来看一个显示具体化的例子:

#include <string>
#include <iostream>
using namespace std;typedef struct{string name;int age;float score;
}STU;//function template
template<class T> const T& Max(const T& a,const T & b);
//function explicit specialization:具体方法是把模板参数列表中的参数清空,然后后面的
//1.函数用显式具体化,也就是函数名称后面加<>里面放具体的类型,
//2.再者就是函数参数列表中也用具体的类型
template<> const STU& Max<STU>(const STU& a,const STU &b);
//overload
ostream & operator<<(ostream &,const STU &);int main()
{
STU s1 = {"liming",12,92.2},s2 = {"wanglin",16,100};
cout << Max(s1,s2) << 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;
}

运行结果:

wanglin , 16 , 100

本例中,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);

也就是说,函数模板显式具体化有2个步骤:

1.将函数名后面加<>,里面按照模板参数列表逐个实例化类型参数,模板参数列表置空,因为不需要推断类型了,有了具体的实例化类型后。

2.将函数中返回类型(函数参数列表,以及局部变量中)的T类型变量都实例化成STU

3.函数名后面的尖括号和里面的实例化类型可以省略。

类模板的显式具体化

除了函数模板,类模板也可以显示具体化,并且它们的语法是类似的。

 为什么类模板参数具体化呢?有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以创建显示具体化。当具体化模板和通用模板都与实例化请求匹配时,编译器将选择具体化版本。

具体化类模板定义格式为:template<>class 类名<具体类型>

注意:在上面的显式具体化例子中,我们为所有的类型参数都提供了实参,所以最后的模板头为空,也即template<>。另外 C++ 还允许只为一部分类型参数提供实参,这称为部分显式具体化。

部分显式具体化只能用于类模板,不能用于函数模板。

仍然以 Point 为例,假设我现在希望“只要横坐标 x 是字符串类型”就以|来分隔输出结果,而不管纵坐标 y 是什么类型,这种要求就可以使用部分显式具体化技术来满足。请看下面的代码:
具体化是将通用模板具体化,因此必须要有通用模板存在才可以使用具体化,否则单独使用会报错。下面先来一个具体的例子:

#include <string>
#include <iostream>
using namespace std;
template <class T1,class T2>
class AA{public:void print();};
template<class T1,class T2>
//类外定义的模板成员函数类名后面要加尖括号,里面放具体的类型T1,T2
void AA<T1,T2>::print()
{cout << "template<class T1,class T2>" << std::endl;
}//下面将上面的类模板具体实例化,在原来模板的2个地方做修改即可:
//1.模板参数列表置为空
//2.类名后面加上<>,里面放上具体的参数名字。
//3.在类外定义具体实例化的类的成员函数时,不用加类模板了
//4.不过,不论是类模板也好,类模板具体实例化的类也罢,类外定义成员函数都需要
//类名后面加具体的类型的。比如下面的AA<int,int>::,类名后面就有<int,int>template<> class AA<int,int>
{public:void print();
};
void AA<int,int>::print()
{cout << "template<>" << endl;
}int main()
{
AA<int,int> a1;
a1.print();
AA<double,double> a2;
a2.print();return 0;
}

运行结果:

template<>
template<class T1,class T2>

在《C++类模板》一节中我们定义了一个 Point 类,用来输出不同类型的坐标。在输出结果中,横坐标 x 和纵坐标 y 是以逗号,为分隔的,但是由于个人审美的不同,我希望当 x 和 y 都是字符串时以|为分隔,是数字或者其中一个是数字时才以逗号,为分隔。为了满足我这种奇葩的要求,可以使用显示具体化技术对字符串类型的坐标做特殊处理。

下面的例子演示了如何对 Point 类进行显示具体化:

#include <string>
#include <iostream>
#include <memory>
using namespace std;//先定义一个类,方式是:
//(1)先来一个模板头和模板参数列表
//(2)
template<typename T1,typename T2>
class Point{public:Point(T1 t1,T2 t2):m_x(t1),m_y(t2){}T1 getX()const{return m_x;}T2 getY()const{return m_y;}void setX(T1 a){m_x = a;}void setY(T2 b){m_y = b;}void display()const;private:T1 m_x;T2 m_y;
};template <typename T1,typename T2>
void Point<T1,T2>::display()const
{cout << "x=" << m_x << "m_y:"<< m_y << endl;
}//类模板的显式具体化:针对字符串类型的显式具体化
//类模板显式具体化和函数模板显式具体化步骤类似:
//1.在类名后面加尖括号<>,里面放置具体的类型
//2.类中使用的类型参数全部用具体类型的实际参数对应替代。
//3.类作用域template<> class Point<char*,char*>
{public:
Point(char* t1,char* t2):m_x(t1),m_y(t2){}
char* getX(){return m_x;}
char* getY(){return m_y;}
void setX(char *a){m_x = a;}
void setY(char *b){m_y = b;}void display()const;private:char *m_x;char *m_y;
};
//因为这个类已经实例化了,有模板参数有具体的参数类型了(char* char*),所以
//具体化的类不能带模板头了
void Point<char*,char*>::display()const
{cout << "x=" << m_x << " | y=" << m_y << endl;
}int main()
{Point<int,int> p1(123,123);
p1.display();
Point<char*,char*> p2("aaa","bbb");
p2.display();return 0;
}

类模板也可以部分参数具体实例化,如下即是:

#include <string>
#include <iostream>
using namespace std;template<typename T1,typename T2>
class AA{public:void print();
};template<typename T1,typename T2>
void AA<T1,T2>::print()
{cout << "template<class T1,class T2>" << endl;
}template<class T1>
class AA<T1,int>
{public:void print();
};template<class T1>
void AA<T1,int>::print()
{cout << "template<class T1,int>" << endl;
}int main()
{
AA<double,int> p1;
p1.print();
AA<double,double> p2;
p2.print();
AA<int,int> p3;
p3.print();return 0;
}

也可以如下定义:

#include <string>
#include <iostream>
using namespace std;template<typename T1,typename T2>
class AA{public:void print();
};template<typename T1,typename T2>
void AA<T1,T2>::print()
{cout << "template<class T1,class T2>" << endl;
}template<class T2>
//部分实例化的时候,当类名后面放<>以及内部的2个参数的时候,必须按照原来的类的顺序放置。
class AA<int,T2>
{public:void print();
};template<class T2>
void AA<int,T2>::print()
{cout << "template<class int,T2>" << endl;
}int main()
{
AA<double,int> p1;
p1.print();
AA<double,double> p2;
p2.print();
AA<int,int> p3;
p3.print();return 0;
}

注意:在部分具体实例化的类的定义中:

1.模板头中仅仅只放类型参数(类型未知的参数)。

2.类名后面的尖括号内放所有参数,包括类型已知的和未知的。

类名后面之所以要列出所有的类型参数,是为了让编译器确认“到底是第几个类型参数被具体化了”,如果写作template<typename T2> class Point<char*>,编译器就不知道char*代表的是第一个类型参数,还是第二个类型参数。

题外话:如果编译函数的时候g++ 遇到类似下面的结果,...

undefined reference to some_function...,那一般的错误就是没有给函数提供定义。这就是一个连接期的错误,有的读者可能说:我提供定义了呀,那么就该检查下自己定义的函数和声明的函数是不是同一个(一般是参数类型或者返回类型出现错误)。

r@r:~/coml/c++/review$ g++ 4.cc -o 123
/usr/bin/ld: /tmp/ccIzfftp.o: in function `main':
4.cc:(.text+0x83): undefined reference to `void Swap<int*>(int*, int*, int)'
collect2: error: ld returned 1 exit status

7.7 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<typename T> void Swap(T a[], T b[], int len);

形参 len 用来指明要交换的数组的长度,调用 Swap() 函数之前必须先通过sizeof求得数组长度再传递给它。(除非用数组的引用传递数组名字,就不用转换成指针了)

有读者可能会疑惑,为什么在函数内部不能求得数组长度,一定要通过形参把数组长度传递进去呢?这是因为数组在作为函数参数时会自动转换为数组指针,而sizeof只能通过数组名求得数组长度,不能通过数组指针求得数组长度,这在《数组和指针绝不等价,数组是另外一种类型》《数组到底在什么时候会转换为指针》两节中已经作了详细讲解。

#include <string>
#include <cstring>
#include <cstdlib>
#include <iostream>
//abs函数在 cmath 中
#include <cmath>
using namespace std;template<typename T,size_t N>
class Array{public:Array();Array(initializer_list<T> li);size_t size()const;size_t get_capacity()const;//把数组的大小扩容n个单位bool capacity(int n);T& operator[](int i);private:size_t m_length; //数组当前大小size_t m_capacity; //数组的容量T* m_ptr; //指向数组首元素地址};template<typename T,size_t N>
Array<T,N>::Array():m_length(N),m_capacity(N),m_ptr(new T[N]){}template<typename T,size_t N>
size_t Array<T,N>::size()const
{return m_length;
}template<typename T,size_t N>
size_t Array<T,N>::get_capacity()const
{return m_capacity;
}template<typename T,size_t N>
T& Array<T,N>::operator[](int i)
{if(i < 0 || i >= m_length){cout << "exception:too big index." << endl;static T temp;return temp;}return m_ptr[i];
}template<typename T,size_t 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 * new_ptr = new T[m_length + 2*n];if(new_ptr == NULL){cout << "Not space to allocate." << endl;return false;}else{//从内存地址m_ptr开始赋值m_length * sizeof(T)//个字节到new_ptr中memcpy(new_ptr,m_ptr,m_length * sizeof(T));//m_capacity equals  to total storage sizem_capacity = m_length + 2 * n;m_length = len;    delete [] m_ptr;m_ptr = new_ptr;return true;}      }}
//缩小数组
else{int len = m_length - abs(n);//缩小后的数组长度if(len < 0){cout << "too small Array." << endl;return false;}else{m_length = len;return true;     }}
}template<typename T,size_t N>
void print(const T (&p)[N])
{
size_t len = sizeof(p) / sizeof(p[0]);
for(auto i = 0;i != len; ++ i){if(i == N-1){cout << p[i] << endl;}else{cout << p[i] << ",";}    }
}int main()
{
Array<int,6> a;
a[0] = 111;
a[2]= 333;
a[1] = 222;
cout << a[0] << endl;
cout << a[1] << endl;
a[0] = 123;
cout << a[0] << endl;
cout << a.get_capacity() << endl;
cout << a.size() << endl;
cout <<"too big:"<< a[100] << endl;
a.capacity(100);
cout << a.size()<< endl;
cout << a.get_capacity();return 0;
}

运行结果:

111
222
123
6
6
too big:exception:too big index.
0
106
206

7.9将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_Htemplate<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<double>(int &, int &);
    void Swap<double>(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_Htemplate<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 中。

  • 7.10 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_Htemplate<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;}

显式实例化也包括声明和定义,定义要放在模板定义(实现)所在的源文件,声明要放在模板声明所在的头文件(当然也可以不写)。既然声明放在模板声明所在文件,那不是应该放在func.h中吗?

类模板的显式实例化

类模板的显式实例化和函数模板类似。以上节的 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.cpp的源码

    #ifndef _POINT_H#define _POINT_Htemplate<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 source code

    #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++ 支持显式实例化的目的是为「模块化编程」提供一种解决方案,这种方案虽然有效,但是也有明显的缺陷:程序员必须要在模板的定义文件(实现文件,也就是.cpp文件而非.h文件)中对所有使用到的类型进行实例化。这就意味着,每次更改了模板使用文件(调用函数模板的文件,或者通过类模板创建对象的文件),也要相应地更改模板定义文件,以增加对新类型的实例化,或者删除无用类型的实例化。
        一个模板可能会在多个文件中使用到,要保持这些文件的同步更新是非常困难的。而对于库的开发者来说,他不能提前假设用户会使用哪些类型,所以根本就无法使用显式实例化,只能将模板的声明和定义(实现)全部放到头文件中;C++ 标准库几乎都是用模板来实现的,这些模板的代码也都位于头文件中。

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

c++ 模板教程(c语言中文网) 自己运行实例相关推荐

  1. 菜鸟教程c语言题目,C 练习实例40

    参考方法: #include #include #include void reverse(int *numArray, int k); int main() { srand((int)time(0) ...

  2. Go语言实践[回顾]教程03--Go语言的编译与运行的命令行

    Go语言实践[回顾]教程03--Go语言的编译与运行的命令行 Go语言是编译型静态语言 如何编译Go语言的源文件 如何执行(运行)编译后的文件 开发中如何编译后立即执行 总结 Go语言是编译型静态语言 ...

  3. [c++][语言语法]函数模板和模板函数 及参数类型的运行时判断

    参考:http://blog.csdn.net/beyondhaven/article/details/4204345 参考:http://blog.csdn.net/joeblackzqq/arti ...

  4. 全国计算机等级考试二级教程——c++语言程序设计答案,全国计算机等级考试二级教程--C++语言程序设计(2018年版)...

    全国计算机等级考试二级教程--C++语言程序设计(2018年版) 下载 mobi epub pdf ☆☆☆☆☆ 教育部考试中心 著 下载链接在页面底部 发表于2021-05-17 类似图书 点击查看全 ...

  5. Swift教程Swift语言快速入门(内部资料)

    Swift语言快速入门(内部资料) 试读下载地址:http://pan.baidu.com/s/1eQCGRHw 前言Swift教程Swift语言快速入门(内部资料)Swift教程Swift语言快速入 ...

  6. Atitit.5gl 第五代语言编程语言 PROLOG教程  人工智能语言的标准 与实现

    Atitit.5gl 第五代语言编程语言 PROLOG教程  人工智能语言的标准 与实现 1. 第五代语言就是自然语言又被称为知识库语言或人工智能语言,1 2. 人工智能语言特点2 2.1. 试探法2 ...

  7. c语言中文网_在C语言中使用中文字符

    大部分C语言教材对中文字符的处理讳莫如深,甚至只字不提,导致很多初学者认为C语言只能处理英文,而不支持中文.其实C语言是一门全球化的编程语言,它支持世界上任何一个国家的语言文化,包括中文.日语.韩语等 ...

  8. C语言高级教程-C语言数组(六):变长数组

    C语言高级教程-C语言数组(六):变长数组 一.本文的编译环境 二.一维数组在执行期间确定长度 三.二维数组在执行期间确定长度 四.一维变长数组实例 五.完整程序 5.1 Main.h 文件程序 5. ...

  9. c语言中文网严长生,2.shell语法:shell变量定义和使用

    目录 1.shell变量:Shell变量的定义.删除变量.只读变量.变量类型 1.1 变量类型 1.2定义变量 1.3 使用变量 1.4只读变量和删除修改变量 1.5 变量作用域 1.shell变量: ...

最新文章

  1. 深度学习未来的三种方式
  2. 【设计模式】状态模式 ( 简介 | 适用场景 | 优缺点 | 代码示例 )
  3. 【运营】“顶级运营”最强速成攻略!目前国内不超过200人!
  4. boost::gregorian模块实现自出生以来的天数的测试程序
  5. 用ASP.NETCore构建可检测的高可用服务
  6. android 获取系统时间的时间戳 ,时间戳日期互转,计算日期时间差,获取明天日期,比较时间大小
  7. httprequest存储的是字符内容 而文本内容是以字节形式上传的;所以普通的取值方式无法从httprequest取到值...
  8. JAVA的抽象类和接口
  9. Ant Design Upload 文件上传功能
  10. [硬件选型] 工业镜头之参数和选型
  11. 运行Moblin Live映像 - for Mobin v2.1
  12. 图解 Spring 循环依赖,看过之后再也不怕面试被问到了!
  13. Kali Linux 工具使用中文说明书
  14. 云呐|机房监控服务平台,机房监控服务平台有哪些
  15. 做销售,如何开发陌生市场?
  16. 达人评测i5 1340p和i5 12500h差距 酷睿i51340p和i5 12500h选哪个
  17. 路飞学城简要需求分析
  18. 『CV学习笔记』图像处理透视变换(Python+Opencv)
  19. PHP输出文件的函数
  20. 人生还有四件大事不能糊涂

热门文章

  1. 正弦水波纹波动画 - SJWaveView
  2. 【模拟】Codeforces 705A Hulk
  3. HTML data属性简介以及低版本浏览器兼容算法
  4. ethercat主站控制软件TwinCAT的安装
  5. 一个Delphi写的DES算法, 翻译成C#
  6. 操作系统实验报告1:ucore Lab 1
  7. PyTorch tensorboard报错:TensorBoard logging requires TensorBoard version 1.15 or above
  8. 2019\Province_C_C++_B\试题G-完全二叉树的权值
  9. 2016年第七届蓝桥杯 - 国赛 - Java大学C组 - A. 平方末尾
  10. 信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言—— 1050:骑车与走路