C++ 模板


模板是泛型编程的基础,泛型编程及以一种独立于任何特定类型的方式编写代码。

模板是创建泛型类或函数的蓝图或公式。库容器:比如迭代器,都是泛型编程的例子,他们都使用类模板的概念, 每个容器都有一个单一的定义,比如向量,我们可以定义许多不同类型的向量,比如vector <int>或 vector <string>。

模板是一种对类型进行参数化的工具,通常有两种形式:函数模板和类模板;

(1) 函数模板针对仅参数类型不同的函数;
(2) 类模板针对仅数据成员和成员函数类型不同的类。

使用模板让程序员能够编写与类型无关的代码。比如:编写一个交换两个int型的swap函数,这个函数就只能实现int型,对double,字符这些类型无法实现,要实现这些类型的交换就要重新写另一个swap函数。使用模板的目的就是要让这程序的实现与类型无关,比如一个swap模板函数,即可以实现int型,又可以实现double型的交换。

注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。

一、函数模板

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

//交换 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;
}

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

数据的值可以通过函数参数传递,在函数定义时数据的值是未知的,只有等到函数调用时接收了实参才能确定其值。这就是值的参数化。
在C++中,数据的类型也可以通过参数传递,在函数定义时可以不指明具体的数据类型,当发生函数调用时,编译器可以根据传入的实参自动推断数据类型。这就是类型的参数化。

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

函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。
这个通用函数就称为函数模板。在函数模板中,数据的值和类型都被参数化了,发生函数调用时编译器会根据传入的实参来推演形参的值和类型。换个角度说,函数模板除了支持值的参数化,还支持类型的参数化。

一但定义了函数模板,就可以将类型参数用于函数定义和函数声明了。说得直白一点,原来使用 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;

    return 0;
}

定义模板函数的语法:

template <typename 类型参数1 , typename 类型参数2 , ...>
返回值类型 函数名(形参列表)
{
    //在函数体中可以使用类型参数
}
// 类型参数可以有多个,它们之间以逗号,分隔。类型参数列表以< >包围,形式参数列表以( )包围。

注意:模板中typename和class关键字的区别?

template<typename T> class Widget; // uses "typename"
template<class T> class Widget; // uses "class"

   typename关键字也可以使用class关键字替代。C++早期对模板的支持并不严谨,没有引入新的关键字,而是用class来指明类型参数,但是class关键字本来已经用在类的定义中了,这样做显得不太友好,所以后来C++又引入了一个新的关键字typename,专门用来定义类型参数。不过至今仍然有很多代码在使用class关键字,包括C++标准库、一些开源程序等。
   但是,《Effective C++》中建议我们尽量使用typename而不是class来声明一个模板。因为class给人的感觉总像是T应该是一个类,或者说是用户自定义的类型,而typename则更像是表明T可以是任意一种类型。虽然在C++编译器看来,这两种声明方式没有任何的区别,其意义是完全一样的。

不过,typename关键字在模板中还有另一种用法: 使用嵌套依赖类型

class MyArray
{
public:
    typedef int LengthType;
    .....
}

template<class T>
void MyMethod(T myarr)
{ 
    typedef typename T::LengthType LengthType; 
    LengthType length = myarr.GetLength; 
}

这个时候typename的作用就是告诉c++编译器,typename后面的字符串为一个类型名称,而不是成员函数或者成员变量,这个时候如果前面没有typename,编译器没有任何办法知道T::LengthType是一个类型还是一个成员名称(静态数据成员或者静态函数),所以编译不能够通过。

二、类模板

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

一但声明了类模板,就可以将类型参数用于类的成员函数和成员变量了。换句话说,原来使用 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;
}

使用类模板创建对象

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

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度

【实例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] << " ";
    cout << endl;
    return 0;
}
// 0 1 2 3 4 

posted on 2020-04-17 10:48  JJ_S  阅读(980)  评论(0编辑  收藏  举报