C++函数模板与类模板的区别

类模板:

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


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

 
  1. template<typename T1, typename T2> //这里不能有分号
  2. class Point{
  3. public:
  4. Point(T1 x, T2 y): m_x(x), m_y(y){ }
  5. public:
  6. T1 getX() const; //获取x坐标
  7. void setX(T1 x); //设置x坐标
  8. T2 getY() const; //获取y坐标
  9. void setY(T2 y); //设置y坐标
  10. private:
  11. T1 m_x; //x坐标
  12. T2 m_y; //y坐标
  13. };

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

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

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

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

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

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

 
  1. template<typename T1, typename T2> //模板头
  2. T1 Point<T1, T2>::getX() const /*函数头*/ {
  3. return m_x;
  4. }
  5.  
  6. template<typename T1, typename T2>
  7. void Point<T1, T2>::setX(T1 x){
  8. m_x = x;
  9. }
  10.  
  11. template<typename T1, typename T2>
  12. T2 Point<T1, T2>::getY() const{
  13. return m_y;
  14. }
  15.  
  16. template<typename T1, typename T2>
  17. void Point<T1, T2>::setY(T2 y){
  18. m_y = y;
  19. }

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

使用类模板创建对象

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

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

综合示例

将上面的类定义和类实例化的代码整合起来,构成一个完整的示例,如下所示:

 
  1. #include <iostream>
  2. using namespace std;
  3.  
  4. template<class T1, class T2> //这里不能有分号
  5. class Point{
  6. public:
  7. Point(T1 x, T2 y): m_x(x), m_y(y){ }
  8. public:
  9. T1 getX() const; //获取x坐标
  10. void setX(T1 x); //设置x坐标
  11. T2 getY() const; //获取y坐标
  12. void setY(T2 y); //设置y坐标
  13. private:
  14. T1 m_x; //x坐标
  15. T2 m_y; //y坐标
  16. };
  17.  
  18. template<class T1, class T2> //模板头
  19. T1 Point<T1, T2>::getX() const /*函数头*/ {
  20. return m_x;
  21. }
  22.  
  23. template<class T1, class T2>
  24. void Point<T1, T2>::setX(T1 x){
  25. m_x = x;
  26. }
  27.  
  28. template<class T1, class T2>
  29. T2 Point<T1, T2>::getY() const{
  30. return m_y;
  31. }
  32.  
  33. template<class T1, class T2>
  34. void Point<T1, T2>::setY(T2 y){
  35. m_y = y;
  36. }
  37.  
  38. int main(){
  39. Point<int, int> p1(10, 20);
  40. cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
  41.  
  42. Point<int, char*> p2(10, "东京180度");
  43. cout<<"x="<<p2.getX()<<", y="<<p2.getY()<<endl;
  44.  
  45. Point<char*, char*> *p3 = new Point<char*, char*>("东京180度", "北纬210度");
  46. cout<<"x="<<p3->getX()<<", y="<<p3->getY()<<endl;
  47.  
  48. return 0;
  49. }

运行结果:
x=10, y=20
x=10, y=东京180度
x=东京180度, y=北纬210度

在定义类型参数时我们使用了 class,而不是 typename,这样做的目的是让读者对两种写法都熟悉。

函数模板:

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

 
  1. //交换 int 变量的值
  2. void Swap(int *a, int *b){
  3. int temp = *a;
  4. *a = *b;
  5. *b = temp;
  6. }
  7.  
  8. //交换 float 变量的值
  9. void Swap(float *a, float *b){
  10. float temp = *a;
  11. *a = *b;
  12. *b = temp;
  13. }
  14.  
  15. //交换 char 变量的值
  16. void Swap(char *a, char *b){
  17. char temp = *a;
  18. *a = *b;
  19. *b = temp;
  20. }
  21.  
  22. //交换 bool 变量的值
  23. void Swap(bool *a, bool *b){
  24. char temp = *a;
  25. *a = *b;
  26. *b = temp;
  27. }

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

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

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

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

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

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

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

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

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

 
  1. #include <iostream>
  2. using namespace std;
  3.  
  4. template<typename T> void Swap(T *a, T *b){
  5. T temp = *a;
  6. *a = *b;
  7. *b = temp;
  8. }
  9.  
  10. int main(){
  11. //交换 int 变量的值
  12. int n1 = 100, n2 = 200;
  13. Swap(&n1, &n2);
  14. cout<<n1<<", "<<n2<<endl;
  15.  
  16. //交换 float 变量的值
  17. float f1 = 12.5, f2 = 56.93;
  18. Swap(&f1, &f2);
  19. cout<<f1<<", "<<f2<<endl;
  20.  
  21. //交换 char 变量的值
  22. char c1 = 'A', c2 = 'B';
  23. Swap(&c1, &c2);
  24. cout<<c1<<", "<<c2<<endl;
  25.  
  26. //交换 bool 变量的值
  27. bool b1 = false, b2 = true;
  28. Swap(&b1, &b2);
  29. cout<<b1<<", "<<b2<<endl;
  30.  
  31. return 0;
  32. }

运行结果:
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() 这个函数模板:

 
  1. #include <iostream>
  2. using namespace std;
  3.  
  4. template<typename T> void Swap(T &a, T &b){
  5. T temp = a;
  6. a = b;
  7. b = temp;
  8. }
  9.  
  10. int main(){
  11. //交换 int 变量的值
  12. int n1 = 100, n2 = 200;
  13. Swap(n1, n2);
  14. cout<<n1<<", "<<n2<<endl;
  15.  
  16. //交换 float 变量的值
  17. float f1 = 12.5, f2 = 56.93;
  18. Swap(f1, f2);
  19. cout<<f1<<", "<<f2<<endl;
  20.  
  21. //交换 char 变量的值
  22. char c1 = 'A', c2 = 'B';
  23. Swap(c1, c2);
  24. cout<<c1<<", "<<c2<<endl;
  25.  
  26. //交换 bool 变量的值
  27. bool b1 = false, b2 = true;
  28. Swap(b1, b2);
  29. cout<<b1<<", "<<b2<<endl;
  30.  
  31. return 0;
  32. }

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

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

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

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

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

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

 
  1. template<class T> void Swap(T &a, T &b){
  2. T temp = a;
  3. a = b;
  4. b = temp;
  5. }

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

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

 
  1. #include <iostream>
  2. using namespace std;
  3.  
  4. //声明函数模板
  5. template<typename T> T max(T a, T b, T c);
  6.  
  7. int main( ){
  8. //求三个整数的最大值
  9. int i1, i2, i3, i_max;
  10. cin >> i1 >> i2 >> i3;
  11. i_max = max(i1,i2,i3);
  12. cout << "i_max=" << i_max << endl;
  13.  
  14. //求三个浮点数的最大值
  15. double d1, d2, d3, d_max;
  16. cin >> d1 >> d2 >> d3;
  17. d_max = max(d1,d2,d3);
  18. cout << "d_max=" << d_max << endl;
  19.  
  20. //求三个长整型数的最大值
  21. long g1, g2, g3, g_max;
  22. cin >> g1 >> g2 >> g3;
  23. g_max = max(g1,g2,g3);
  24. cout << "g_max=" << g_max << endl;
  25.  
  26. return 0;
  27. }
  28.  
  29. //定义函数模板
  30. template<typename T> //模板头,这里不能有分号
  31. T max(T a, T b, T c){ //函数头
  32. T max_num = a;
  33. if(b > max_num) max_num = b;
  34. if(c > max_num) max_num = c;
  35. return max_num;
  36. }

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

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

两者的区别:

函数模板与类模板有什么区别?答:函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化 必须由程序员在程序中显式地指定。

 

即函数模板允许隐式调用和显式调用而类模板只能显示调用这期间有涉及到函数模板与模板函数,类模板与模板类的概念(类似于类与类对象的区别)请看下面例子

注意:模板类的函数声明和实现必须都在头文件中完成,不能像普通类那样声明在.h文件中实现在.cpp文件中,原因可以看链接http://hi.baidu.com/cn_rigel/blog/item/6cf6fc083723e2286a60fb53.html

 

#include "stdafx.h"
#include <iostream>
using namespace std;

//使用模板创建一个返回最大值的函数
//这是一个函数模板
template <class Type>
Type MaxValue(Type a,Type b)
{
    if ( a > b)
    {
        return a;
    }
    else
        return b;
}

//创建一个堆栈模板类
//这是一个类模板
template <class T>
class Stack
{
public:
    Stack(){ m_nPos = 0;}
    ~Stack(){}

    void Push(T value);
    T Pop();

    bool IsEmpty()
    {
        return m_nPos == 0;
    }
    bool HasElement()
    {
        return !IsEmpty();
    }
    bool IsFull()
    {
        return m_nPos == STATCK_SIZE;
    }

private:
    int m_nPos;
    //使用常量表示堆栈的大小

    const static int STATCK_SIZE = 100;
    T m_Data[STATCK_SIZE];
};
//模板类的成员函数实现

template <class T>
void Stack<T> ::Push(T value)
{
    //使用后置递增操作符

    m_Data[m_nPos++] = value;
}
template <class T>
T Stack<T>::Pop()
{
    //使用前置递减操作符

    return m_Data[--m_nPos];
}

void TestMaxValue()
{
    //隐式调用
 
//函数模板的实例化在程序调用时自动完成
   cout << MaxValue(100, 204)<< endl;//MaxValue(100, 204)这是一个模板函数
    cout << MaxValue(2.5002,30.003) << endl;//MaxValue(2.5002,30.003)这也是一个模板函数
//当然由程序员自己指定也可以
    //显示调用

    cout << MaxValue<int>(10,20) << endl;
    cout << MaxValue<double>(2.5002,30.003) << endl;
}

void TestStack()
{
    //测试模板类(整数)

    Stack <int> intStack;//类模板的实例化由程序员显示的指定
    intStack.Push(10);
    intStack.Push(20);
    intStack.Push(30);

    while (intStack.HasElement())
    {
        cout << intStack.Pop() << endl;
    }

    //测试模板类(浮点)

    Stack <float> floatStack;//类模板的实例化由程序员显示的指定
    floatStack.Push(1.001);
    floatStack.Push(2.002);
    floatStack.Push(3.003);

    while (floatStack.HasElement())
    {
        cout << floatStack.Pop() << endl;
    }

    //测试动态创建对象

    //Stack创建的指针必须指明类型

    Stack<int>* pInt = new Stack<int>();类模板的实例化由程序员显示的指定
    pInt->Push(10);


    pInt->Push(20);
    pInt->Push(30);

    while (pInt->HasElement())
    {
        cout << pInt->Pop() << endl;
    }
    if ( pInt != NULL)
    {
        delete pInt;
        pInt = NULL;
    }
}

区别2:

在C++中有好几个这样的术语,但是我们很多时候用的并不正确,几乎是互相替换混淆使用。下面我想彻底辨清几个术语,这样就可以避免很多概念上的混淆和使用上的错误。

这几个词是:

函数指针——指针函数

数组指针——指针数组

类模板——模板类

函数模板——模板函数

最终在使用中,我们就可以让它们实至名归,名正言顺。

1.函数指针——指针函数

函数指针的重点是指针。表示的是一个指针,它指向的是一个函数,例子:

int   (*pf)();

指针函数的重点是函数。表示的是一个函数,它的返回值是指针。例子:

int*   fun();

 
2.数组指针——指针数组

数组指针的重点是指针。表示的是一个指针,它指向的是一个数组,例子:

int   (*pa)[8];

指针数组的重点是数组。表示的是一个数组,它包含的元素是指针。例子;

int*   ap[8];

 

3.类模板——模板类(class   template——template   class)

类模板的重点是模板。表示的是一个模板,专门用于产生类的模子。例子:

template   <typename   T>

class   Vector

{

            …

};

使用这个Vector模板就可以产生很多的class(类),Vector <int> 、Vector <char> 、Vector <   Vector <int>   > 、Vector <Shape*> ……。

模板类的重点是类。表示的是由一个模板生成而来的类。例子:

上面的Vector <int> 、Vector <char> 、……全是模板类。

这两个词很容易混淆,我看到很多文章都将其用错,甚至一些英文文章也是这样。将他们区分开是很重要的,你也就可以理解为什么在定义模板的头文件.h时,模板的成员函数实现也必须写在头文件.h中,而不能像普通的类(class)那样,class的声明(declaration)写在.h文件中,class的定义(definition)写在.cpp文件中。请参照Marshall   Cline的《C++   FAQ   Lite》中的[34]   Container   classes   and   templates中的[34.12]   Why   can 't   I   separate   the   definition   of   my   templates   class   from   it 's   declaration   and   put   it   inside   a   .cpp   file?   URL地址是http://www.parashift.com/c++-faq-lite/containers-and-templates.html#faq-34.12

我将几句关键的段落摘录如下,英文很好理解:

In   order   for   the   compiler   to   generate   the   code,   it   must   see   both   the   template   definition   (not   just   declaration)   and   the   specific   types/whatever   used   to   "fill   in "   the   template.   For   example,   if   you 're   trying   to   use   a   Foo <int> ,   the   compiler   must   see   both   the   Foo   template   and   the   fact   that   you 're   trying   to   make   a   specific   Foo <int> .  

Suppose   you   have   a   template   Foo   defined   like   this:  

  template <class   T>
  class   Foo   {
  public:
      Foo();
      void   someMethod(T   x);
  private:
      T   x;
  };  

Along   with   similar   definitions   for   the   member   functions:  

  template <class   T>
  Foo <T> ::Foo()
  {
      ...
  }
 
  template <class   T>
  void   Foo <T> ::someMethod(T   x)
  {
      ...
  }  

Now   suppose   you   have   some   code   in   file   Bar.cpp   that   uses   Foo <int> :  

  //   Bar.cpp
 
  void   blah_blah_blah()
  {
      ...
      Foo <int>   f;
      f.someMethod(5);
      ...
  }  

Clearly   somebody   somewhere   is   going   to   have   to   use   the   "pattern "   for   the   constructor   definition   and   for   the   someMethod()   definition   and   instantiate   those   when   T   is   actually   int.   But   if   you   had   put   the   definition   of   the   constructor   and   someMethod()   into   file   Foo.cpp,   the   compiler   would   see   the   template   code   when   it   compiled   Foo.cpp   and   it   would   see   Foo <int>   when   it   compiled   Bar.cpp,   but   there   would   never   be   a   time   when   it   saw   both   the   template   code   and   Foo <int> .   So   by   rule   above,   it   could   never   generate   the   code   for   Foo <int> ::someMethod().  

关于一个缺省模板参数的例子:

template   <typename   T   =   int>

class   Array

{

            …

};

第一次我定义这个模板并使用它的时候,是这样用的:

Array   books;//我认为有缺省模板参数,这就相当于Array <int>   books

上面的用法是错误的,编译不会通过,原因是Array不是一个类。正确的用法是Array <>   books;

这里Array <> 就是一个用于缺省模板参数的类模板所生成的一个具体类。

 

4.函数模板——模板函数(function   template——template   function)

函数模板的重点是模板。表示的是一个模板,专门用来生产函数。例子:

template   <typename   T>

void   fun(T   a)

{

            …

}

在运用的时候,可以显式(explicitly)生产模板函数,fun <int> 、fun <double> 、fun <Shape*> ……。

也可以在使用的过程中由编译器进行模板参数推导,帮你隐式(implicitly)生成。

fun(6);//隐式生成fun <int>

fun(8.9);//隐式生成fun <double>

fun(‘a’);//   隐式生成fun <char>

Shape*   ps   =   new   Cirlcle;

fun(ps);//隐式生成fun <Shape*>

 

模板函数的重点是函数。表示的是由一个模板生成而来的函数。例子:

上面显式(explicitly)或者隐式(implicitly)生成的fun <int> 、fun <Shape*> ……都是模板函数。

关于模板本身,是一个非常庞大的主题,要把它讲清楚,需要的不是一篇文章,而是一本书,幸运的是,这本书已经有了:David   Vandevoorde,   Nicolai   M.   Josuttis写的《C++   Templates:   The   Complete   Guide》。可惜在大陆买不到纸版,不过有一个电子版在网上流传。

 

模板本身的使用是很受限制的,一般来说,它们就只是一个产生类和函数的模子。除此之外,运用的领域非常少了,所以不可能有什么模板指针存在的,即指向模板的指针,这是因为在C++中,模板就是一个代码的代码生产工具,在最终的代码中,根本就没有模板本身存在,只有模板具现出来的具体类和具体函数的代码存在。

但是类模板(class   template)还可以作为模板的模板参数(template   template   parameter)使用,在Andrei   Alexandrescu的《Modern   C++   Design》中的基于策略的设计(Policy   based   Design)中大量的用到。

template <   typename   T,   template <typename   U>   class   Y>

class   Foo

{

            …

};

 

从文章的讨论中,可以看到,名字是非常重要的,如果对名字的使用不恰当的话,会引起很多的麻烦和误解。我们在实际的程序中各种标识符的命名也是一门学问,为了清晰易懂,有时候还是需要付出一定的代价。
 

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页