C++模板编程


808 浏览 5 years, 11 months

2.1 编写类模板

版权声明: 转载请注明出处 http://www.codingsoho.com/

编写类模板

假设您想要一个通用的棋盘类,可将其用作象棋棋盘、跳棋棋盘、井字游戏棋盘或其他任何二维的棋盘。为了让这个棋盘通用,这个棋盘应该能保存象棋棋子、跳棋棋子、井字游戏棋子或其他任何游戏类型的棋子。

1、编写不使用模板的代码

如果不使用模板,编写通用棋盘最好的方法是采用多态技术,保存通用的GamePiece对象。然后,可以从GamePiece类子类化每一种游戏的棋子。

例如,在象棋游戏中,ChessPiece可以是GamePiece的子类。通过多态技术,能够保存GamePiece的GameBoard也能保存ChessPiece。 这个类的定义应该类似于前面的Spreadsheet类,这个类采用了动态分配的二维数组作为底层的网格数据结构:

class GameBoard
{
public:
    // The general-purpose GameBoard allows the user to specify its dimensions
    GameBoard(size_t inWidth = kDefaultWidth, size_t inHeight = kDefaultHeight);
    GameBoard(const GameBoard& src); // copy constructor
    virtual ~GameBoard();
    GameBoard& operator=(const GameBoard& rhs); // assignment operator
    //
    void setPieceAt(size_t x, size_t y, const GamePiece* inPiece);
    GamePiece* getPieceAt(size_t x, size_t y);
    const GamePiece* getPieceAt(size_t x, size_t y) const;
    //
    size_t getHeight() const { return mHeight; }
    size_t getWidth() const { return mWidth; }
    static const size_t kDefaultWidth = 10;
    static const size_t kDefaultHeight = 10;

protected:
    void copyFrom(const GameBoard& src);
    void initializeCells();
    void cleanupCells();
    // objects dynamically allocate space for the game pieces.
    GamePiece*** mCells;
    size_t mWidth, mHeight;
};

下面是方法的定义。这个实现几乎与前面的Spreadsheet类是一致的。当然,生产系统中的代码应该在setPieceAt()和getPieceAt()进行边界检查。这里略去了边界检查的代码,因为这不是本章讨论的重点:

GameBoard::GameBoard(size_t inWidth, size_t inHeight) :
  mWidth(inWidth), mHeight(inHeight)
{
  initializeCells();
}

GameBoard::GameBoard(const GameBoard& src)
{
  copyFrom(src);
}

GameBoard::~GameBoard()
{
  // free the old memory
  cleanupCells();
}

void GameBoard::copyFrom(const GameBoard& src)
{
  mWidth = src.mWidth;
  mHeight = src.mHeight;

  initializeCells();

  for (size_t i = 0; i < mWidth; i++) {
    for (size_t j = 0; j < mHeight; j++) {
      if (src.mCells[i][j])
        mCells[i][j] = src.mCells[i][j]->Clone();
    }
  }
}

void GameBoard::initializeCells()
{
  mCells = new GamePiece** [mWidth];
  for (size_t i = 0; i < mWidth; i++) {
    mCells[i] = new GamePiece*[mHeight];
    for (size_t j = 0; j < mHeight; j++) {
      mCells[i][j] = nullptr;
    }
  }
}

void GameBoard::cleanupCells()
{
  for (size_t i = 0; i < mWidth; i++) {
    for (size_t j = 0; j < mHeight; j++) {
      delete mCells[i][j];
    }
    delete [] mCells[i];
  }

  delete [] mCells;
  mCells = nullptr;
}

GameBoard& GameBoard::operator=(const GameBoard& rhs)
{
  // check for self-assignment
  if (this == &rhs) {
    return *this;
  }

  // free the old memory
  cleanupCells();

  // copy the new memory
  copyFrom(rhs);

  return *this;
}

void GameBoard::setPieceAt(size_t x, size_t y, const GamePiece* inElem)
{
  if (inElem)
    mCells[x][y] = inElem->Clone();
}

GamePiece* GameBoard::getPieceAt(size_t x, size_t y)
{
    return mCells[x][y];
}

const GamePiece* GameBoard::getPieceAt(size_t x, size_t y) const
{
  return mCells[x][y];
}

GameBoard类可以很好地完成任务。假设您编写了一个ChessPiece类,那么可以这样创建GameBoard对象并使用这些对象:

GameBoard chessBoard(8, 8);
ChessPiece pawn;
chessBoard.setPieceAt(0, 0, &pawn);
2.模板Grid类

前一节中定义的GameBoard类很好,但仍不够强大。例如,这个类和Spreadsheet类非常类似,但是要将这个类用作电子表格的时候,唯一的方法是把SpreadsheetCell类定义为GamePiece的子类。这样做并不合理,因为不符合继承的“是一个”原理:一个SpreadsheetCell并不是一个GamePiece。

如果能编写一个通用的Grid类,既能用作一个Spreadsheet,又能用作一个ChessBoard就好了。在C++中,可以通过编写类模板来实现这一点,编写类模板可以避免编写需要指定一种或多种类型的类。客户通过指定想要使用的类型对模板进行实例化。

Grid类定义

为了理解类模板,最好首先看一下类模板的语法。下面的例子展示了如何对GameBoard类做小小的修改得到一个模板化的Grid类。不要被语法吓到了,代码之后会对所有语法进行解释。

注意,类名从GameBoard变为Grid,setPieceAt()和getPieceAt()也变为setElementAt()和getElementAt()以反映这个类的通用本质:

template <typename T>
class Grid
{
public:
    Grid(size_t inWidth = kDefaultWidth, size_t inHeight = kDefaultHeight);
    Grid(const Grid<T>& src);
    virtual ~Grid();
    Grid<T>& operator=(const Grid<T>& rhs);
    //
    void setElementAt(size_t x, size_t y, const T& inElem);
    T& getElementAt(size_t x, size_t y);
    const T& getElementAt(size_t x, size_t y) const;
    //
    size_t getHeight() const { return mHeight; }
    size_t getWidth() const { return mWidth; }
    static const size_t kDefaultWidth = 10;
    static const size_t kDefaultHeight = 10;

protected:
    void copyFrom(const Grid<T>& src);
    T** mCells;
    size_t mWidth, mHeight;
};

现在已经展示了完整的类定义,下面在逐行分析这些代码:

template <typename T>

第一行表示下面的类定义是一个基于一个类型的模板。template和typename都是C++的关键字。
根据前面的描述,模板采用函数对值进行“参数化”的方式对类型进行“参数化”。

就像在函数中通过参数名称表示调用者将要传入的参数一样,在模板中使用类型名称(例如T)表示调用者要指定的类型。

名称T没有什么特别之处,您可以使用任何想用的名称。按照惯例,只是用一个类型的时候,将这个类型称为T,但这只是一个历史约定,就像把索引数组的整数命名为i或j一样。

基于历史原因,指定模板类型参数的时候,可以使用关键字class而不是typename。因此,很多书籍和现有的程序使用了这样的语法:template <class T>。不过,在这个上下文中使用class这个词会产生一些误解,因为这个词暗示这个类型必须是一个类,而实际上并不要求这样。这个类型可以是一个类、一个struct、一个union、一个语言的原始类型(例如int或double等)。

这个模板说明符应用于整个语句,在这里即整个类定义。

以下几行是复制构造函数:

Grid(const Grid<T>& src);

从中可以看出,src参数的类型不再是const Grid&了,而是一个const Grid<T>&。编写一个类模板的时候,您过去认为是类名的那个名称(Grid)现在实际上是模板名称。

当您讨论实际的Grid类或类型的时候,实际上讨论的是Grid类模板对某一个特定类型实例化的结果,例如int、SpreadsheetCell或ChessPiece。

在定义模板的时候没有指定这个模板要实例化为的类型,因此必须使用一个占位的模板参数T,这个T表示未来可能使用的任何类型。因此,当需要表示一个Grid对象的类型作为方法的传入参数或返回值的时候,必须使用Grid。在赋值运算符的传入参数和返回的值,以及copyFrom()方法的参数中也可以看到这个变化。

在一个类定义中,编译器会根据需要将Grid解释为Grid<T>。然而,最好养成显式指定Grid的习惯,因为在这个类的外面要表示这个模板产生的类型的时候需要使用这种语法。只有构造函数和析构函数应该使用Grid而不是Grid<T>。

这个类的最后一个变化是例如setElementAt()和getElementAt()这类方法现在接受的参数和返回的值的类型都是T,而不是GamePiece。

    void setElementAt(size_t x, size_t y, const T& inElem);
    T& getElementAt(size_t x, size_t y);
    const T& getElementAt(size_t x, size_t y) const;

这个类型T是一个占位符,表示用户指定的任意类型。mCells现在是一个T**而不是GamePiece***,因为mCells指向一个类型为T的动态分配的二维数组,类型T是用户指定的。

Grid类的方法定义

template <typenameT>说明符必须在Grid模板的每一个方法定义前面。构造函数如下所示:

template <typename T>
Grid<T>::Grid(size_t inWidth, size_t inHeight) : mWidth(inWidth), mHeight(inHeight)
{
 mCells = new T* [mWidth];
 for (size_t i = 0; i < mWidth; i++) {
  mCells[i] = new T[mHeight];
 }
}

模板要求将方法的实现也放在头文件本身中,因为编译器在创建模板的实例之前需要知道完整的定义,包括方法的定义。本章后面会对此进行详述。

注意::之前的类名是Grid<T>,而不是Grid。必须在所有的方法和静态数据成员定义中将Grid<T>指定为类名。构造函数的函数体类似于GameBoard构造函数的函数体,区别在于类型占位符T替换了GamePiece类型。

其他方法定义也类似于GameBoard类中对应的方法定义,只是适当地改变了的template和Grid<T>语法:

template <typename T>
Grid<T>::Grid(const Grid<T>& src)
{
 copyFrom(src);
}

template <typename T>
Grid<T>::~Grid()
{

 for (size_t i = 0; i < mWidth; i++) {
  delete [] mCells[i];
 }
 delete [] mCells;
 mCells = nullptr;
}

template <typename T>
void Grid<T>::copyFrom(const Grid<T>& src)
{
 mWidth = src.mWidth;
 mHeight = src.mHeight;

 mCells = new T* [mWidth];
 for (size_t i = 0; i < mWidth; i++) {
  mCells[i] = new T[mHeight];
 }

 for (size_t i = 0; i < mWidth; i++) {
  for (size_t j = 0; j < mHeight; j++) {
   mCells[i][j] = src.mCells[i][j];
  }
 }
}

template <typename T>
Grid<T>& Grid<T>::operator=(const Grid<T>& rhs)
{

 if (this == &rhs) {
  return *this;
 }

 for (size_t i = 0; i < mWidth; i++) {
  delete [] mCells[i];
 }
 delete [] mCells;
 mCells = nullptr;


 copyFrom(rhs);

 return *this;
}

template <typename T>
void Grid<T>::setElementAt(size_t x, size_t y, const T& inElem)
{
 mCells[x][y] = inElem;
}

template <typename T>
T& Grid<T>::getElementAt(size_t x, size_t y)
{
 return mCells[x][y];
}

template <typename T>
const T& Grid<T>::getElementAt(size_t x, size_t y) const
{
 return mCells[x][y];
}
3.使用Grid模板

创建网格对象的时候,不能单独使用Grid作为类型;必须指定这个Grid中保存的元素的类型。为某一个特定的类型创建一个模板类的对象的过程称为模板的实例化(instantiate)。下面列举一个示例:

    Grid<int> myIntGrid; // declares a grid that stores ints, using default parameters for the constructor
    Grid<double> myDoubleGrid(11, 11); // declares an 11x11 Grid of doubles
    myIntGrid.setElementAt(0, 0, 10);
    int x = myIntGrid.getElementAt(0, 0);
        //
    Grid<int> grid2(myIntGrid);
    Grid<int> anotherIntGrid = grid2;

注意 myIntGrid, grid2和anotherlntGrid的类型为Grid<int>。不能将SpreadsheetCell或ChessPiece保存在这些网格中;如果试图那样做编译器会生成错误消息。

类型规范非常重要,下面两行代码都无法成功编译:

    //Grid test;   // WILL NOT COMPILE
    //Grid<> test; // WILL NOT COMPILE

编译器对第一行代码会给出类似这样的错误:“类模板的使用要求提供模板参数列表”。编译器对第二行代码会给出类似这样的错误:“模板参数数目错误”。

如果要声明一个接受Grid对象的函数或方法,必须在Grid类型中指定保存在那个网格中的类型。

void processIntGrid(Grid<int>& inGrid)
{
    // body omitted for brevity
}

为了避免每次都编写完整的Grid类型名称,例如Grid<int>,可以通过typedef赋予一个更简单的名称:

typedef Grid<int> IntGrid

现在可编写以下代码:

void processIntGrid(IntGrid& inGrid) { }

Grid模板能保存的数据不只是int。例如,可实例化一个保存SpreadsheetCell的Grid:

    Grid<SpreadsheetCell> mySpreadsheet;
    SpreadsheetCell myCell;
    mySpreadsheet.setElementAt(3, 4, myCell);

还可以保存指针类型:

    Grid<char*> myStringGrid;
    myStringGrid.setElementAt(2, 2, "hello");

指定的类型甚至可以是另一个模板类型。下面的例子使用了标准模板库中的vector模板:

    Grid<vector<int>> gridOfVectors;
    vector<int> myVector;
    gridOfVectors.setElementAt(5, 6, myVector);

还可在堆上动态分配Grid模板实例:

    Grid<int>* myGridp = new Grid<int>(); // creates Grid with default width/height
    myGridp->setElementAt(0, 0, 10);
    x = myGridp->getElementAt(0, 0);
    delete myGridp;