网站负责人拍照,中国工程建设造价管理协会网站,北京装修公司四小龙,网站建设需要什么研究条件C模板基础及代码实战
C 模板概览
泛型编程的支持
C 不仅为面向对象编程提供了语言支持#xff0c;还支持泛型编程。正如第6章《设计可重用性》中讨论的#xff0c;泛型编程的目标是编写可重用的代码。C 中支持泛型编程的基本工具是模板。虽然模板不严格是面向对象的特性模板基础及代码实战
C 模板概览
泛型编程的支持
C 不仅为面向对象编程提供了语言支持还支持泛型编程。正如第6章《设计可重用性》中讨论的泛型编程的目标是编写可重用的代码。C 中支持泛型编程的基本工具是模板。虽然模板不严格是面向对象的特性但它们可以与面向对象编程结合产生强大的效果。
模板的核心
在过程化编程范式中主要编程单元是过程或函数。函数之所以有用主要是因为它们允许你编写与特定值无关的算法因此可以用于许多不同的值。例如C 中的 sqrt() 函数计算调用者提供的值的平方根。只计算一个数字如四的平方根的函数不会特别有用sqrt() 函数是针对一个参数编写的该参数是调用者传递的任何值的代表。对象导向编程范式增加了对象的概念对象将相关数据和行为组合在一起但它并没有改变函数和方法参数化值的方式。
模板的进阶参数化
模板将参数化概念进一步扩展允许你对类型以及值进行参数化。C 中的类型包括 int、double 等基本类型以及 SpreadsheetCell、CherryTree 等用户定义的类。有了模板你可以编写不仅与它将要给定的值无关而且与这些值的类型无关的代码。例如你可以编写一个堆栈类定义而不是编写用于存储 int、Cars 和 SpreadsheetCells 的单独堆栈类这个堆栈类定义可以用于任何这些类型。
模板的使用和重要性
尽管模板是一项惊人的语言特性但 C 中的模板在语法上可能令人困惑许多程序员避免自己编写模板。然而每个程序员至少需要知道如何使用模板因为它们被广泛用于库例如 C 标准库。本章教你如何在 C 中支持模板重点是在标准库中出现的方面。在此过程中你将了解一些除了使用标准库之外你可以在程序中运用的巧妙特性。
类模板
类模板的定义和应用
类模板定义了一个类其中一些变量的类型、方法的返回类型和/或方法的参数被指定为模板参数。类模板主要用于容器即存储对象的数据结构。这一节通过运行示例 Grid 容器来说明。为了保持示例的合理长度并足够简单以阐明特定要点本章的不同部分将为 Grid 容器添加不在后续部分使用的功能。
编写类模板
假设你想要一个通用的游戏棋盘类可以用作国际象棋棋盘、跳棋棋盘、井字棋棋盘或任何其他二维游戏棋盘。为了使其具有通用性你应该能够存储国际象棋棋子、跳棋棋子、井字棋棋子或任何类型的游戏棋子。
不使用模板编写代码
通过多态性建立通用游戏棋盘
在没有模板的情况下构建通用游戏棋盘的最佳方法是使用多态性来存储通用的 GamePiece 对象。然后你可以让每个游戏的棋子从 GamePiece 类继承。例如在国际象棋游戏中ChessPiece 将是 GamePiece 的派生类。通过多态性编写为存储 GamePiece 的 GameBoard 也可以存储 ChessPiece。因为可能需要复制 GameBoard所以 GameBoard 需要能够复制 GamePiece。这种实现使用多态性所以一种解决方案是在 GamePiece 基类中添加一个纯虚拟的 clone() 方法派生类必须实现它以返回具体 GamePiece 的副本。
这是基本的 GamePiece 接口
export class GamePiece {
public:virtual ~GamePiece() default;virtual std::unique_ptrGamePiece clone() const 0;
};GamePiece 是一个抽象基类。具体类如 ChessPiece从它派生并实现 clone() 方法
class ChessPiece : public GamePiece {
public:std::unique_ptrGamePiece clone() const override {// 调用复制构造函数来复制这个实例return std::make_uniqueChessPiece(*this);}
};GameBoard 的实现
GameBoard 的实现使用向量的向量和 unique_ptr 来存储 GamePieces
GameBoard::GameBoard(size_t width, size_t height) : m_width { width }, m_height { height } {m_cells.resize(m_width);for (auto column : m_cells) {column.resize(m_height);}
}GameBoard::GameBoard(const GameBoard src) : GameBoard { src.m_width, src.m_height } {// The ctor-initializer of this constructor delegates first to the// non-copy constructor to allocate the proper amount of memory.// The next step is to copy the data.for (size_t i { 0 }; i m_width; i) {for (size_t j { 0 }; j m_height; j) {if (src.m_cells[i][j]) {m_cells[i][j] src.m_cells[i][j]-clone();}}}
}void GameBoard::verifyCoordinate(size_t x, size_t y) const {if (x m_width) {throw out_of_range { format({} must be less than {}., x, m_width) };}if (y m_height) {throw out_of_range { format({} must be less than {}., y, m_height) };}
}void GameBoard::swap(GameBoard other) noexcept {std::swap(m_width, other.m_width);std::swap(m_height, other.m_height);std::swap(m_cells, other.m_cells);
}void swap(GameBoard first, GameBoard second) noexcept {first.swap(second);
}GameBoard GameBoard::operator(const GameBoard rhs) {// Copy-and-swap idiomGameBoard temp { rhs }; // Do all the work in a temporary instance.swap(temp); // Commit the work with only non-throwing operations.return *this;
}const unique_ptrGamePiece GameBoard::at(size_t x, size_t y) const {verifyCoordinate(x, y);return m_cells[x][y];
}unique_ptrGamePiece GameBoard::at(size_t x, size_t y) {return const_castunique_ptrGamePiece(as_const(*this).at(x, y));
}在这个实现中at() 返回指定位置的棋子的引用而不是棋子的副本。GameBoard 作为二维数组的抽象应该通过给出索引处的实际对象而不是对象的副本来提供数组访问语义。
注意事项
这个实现为 at() 提供了两个版本一个返回非常量引用另一个返回常量引用。使用复制和交换习语copy-and-swap idiom用于赋值运算符以及 Scott Meyer 的 const_cast() 模式来避免代码重复。
GameBoard 类的使用
GameBoard chessBoard { 8, 8 };
auto pawn { std::make_uniqueChessPiece() };
chessBoard.at(0, 0) std::move(pawn);
chessBoard.at(0, 1) std::make_uniqueChessPiece();
chessBoard.at(0, 1) nullptr;这个 GameBoard 类运行得相当好它可以用于国际象棋棋盘的创建和棋子的放置。
类模板实现的 Grid 类
GameBoard 的局限性
在上一节中的 GameBoard 类虽然实用但有其局限性。首先你无法使用 GameBoard 来按值存储元素它总是存储指针。更严重的问题与类型安全有关。GameBoard 中的每个单元格都存储一个 unique_ptrGamePiece。即使你存储的是 ChessPieces当你使用 at() 请求特定单元格时你将得到一个 unique_ptrGamePiece。这意味着你必须将检索到的 GamePiece 向下转型为 ChessPiece 才能使用 ChessPiece 的特定功能。GameBoard 的另一个缺点是它不能用于存储原始类型如 int 或 double因为单元格中存储的类型必须派生自 GamePiece。
实现通用 Grid 类
因此如果你能编写一个通用的 Grid 类来存储 ChessPieces、SpreadsheetCells、ints、doubles 等就很好了。在 C 中你可以通过编写类模板来实现这一点这允许你编写一个不指定一个或多个类型的类。然后客户端通过指定他们想要使用的类型来实例化模板。这就是所谓的泛型编程。
泛型编程的优势
泛型编程的最大优势是类型安全。类及其方法中使用的类型是具体类型而不是像上一节中多态解决方案那样的抽象基类类型。例如假设不仅有 ChessPiece还有 TicTacToePiece
class TicTacToePiece : public GamePiece {
public:std::unique_ptrGamePiece clone() const override {// 调用复制构造函数来复制这个实例return std::make_uniqueTicTacToePiece(*this);}
};使用上一节中的多态解决方案没有什么能阻止你在同一个棋盘上存储井字棋棋子和国际象棋棋子
GameBoard chessBoard { 8, 8 };
chessBoard.at(0, 0) std::make_uniqueChessPiece();
chessBoard.at(0, 1) std::make_uniqueTicTacToePiece();这样做的一个大问题是你需要记住一个单元格存储了什么以便在调用 at() 时执行正确的向下转型。
Grid 类模板的定义
类模板的语法
为了理解类模板检查其语法非常有帮助。以下示例展示了如何将 GameBoard 类修改为模板化的 Grid 类。代码后面会详细解释语法。请注意类名已从 GameBoard 改为 Grid。
使用值语义实现 Grid 类
与 GameBoard 实现中使用的多态指针语义相比我选择使用值语义而不是多态来实现这个解决方案。m_cells 容器存储实际对象而不是指针。与指针语义相比使用值语义的一个缺点是不能有真正的空单元格也就是说单元格必须始终包含某个值。使用指针语义时空单元格存储 nullptr。 std::optional 在这里提供了帮助。它允许你在仍然有表示空单元格的方法的同时使用值语义。
template typename T
class Grid {
public:explicit Grid(size_t width DefaultWidth, size_t height DefaultHeight);virtual ~Grid() default;// Explicitly default a copy constructor and assignment operator.Grid(const Grid src) default;Grid operator(const Grid rhs) default;// Explicitly default a move constructor and assignment operator.Grid(Grid src) default;Grid operator(Grid rhs) default;std::optionalT at(size_t x, size_t y);const std::optionalT at(size_t x, size_t y) const;size_t getHeight() const { return m_height; }size_t getWidth() const { return m_width; }static const size_t DefaultWidth { 10 };static const size_t DefaultHeight { 10 };private:void verifyCoordinate(size_t x, size_t y) const;std::vectorstd::vectorstd::optionalT m_cells;size_t m_width { 0 }, m_height { 0 };
};
类模板的详细解读
export template typename T这一行表示接下来的类定义是一个关于类型 T 的模板并且它正在从模块中导出。template 和 typename 是 C 中的关键字。如前所述模板在类型上“参数化”就像函数在值上“参数化”一样。使用模板类型参数名如 T来表示调用者将作为模板类型参数传递的类型。T 的名称没有特殊含义——你可以使用任何你想要的名称。
关于模板类型参数的注意事项
出于历史原因你可以使用关键字 class 而不是 typename 来指定模板类型参数。因此许多书籍和现有程序使用类似 template class T 的语法。然而在这种情况下使用 class 这个词是令人困惑的因为它暗示类型必须是一个类这实际上并不正确。类型可以是类、结构体、联合、语言的原始类型如 int 或 double 等。
Grid 类模板与 GameBoard 类的对比
数据成员的变化
在之前的 GameBoard 类中m_cells 数据成员是指针的向量的向量这需要特殊的复制代码因此需要拷贝构造函数和拷贝赋值操作符。在 Grid 类中m_cells 是可选值的向量的向量所以编译器生成的拷贝构造函数和赋值操作符是可以的。
显式默认构造函数和操作符
一旦你有了用户声明的析构函数就不推荐编译器隐式生成拷贝构造函数或拷贝赋值操作符因此 Grid 类模板显式地将它们默认化。它还显式默认化了移动构造函数和移动赋值操作符。以下是显式默认的拷贝赋值操作符
Grid operator(const Grid rhs) default;可以看到rhs 参数的类型不再是 const GameBoard而是 const Grid。在类定义内编译器会在需要时将 Grid 解释为 GridT但如果你愿意也可以显式地使用 GridT
GridT operator(const GridT rhs) default;然而在类定义外你必须使用 GridT。当你编写类模板时你过去认为的类名Grid实际上是模板名。当你想谈论实际的 Grid 类或类型时你必须使用模板 ID即 GridT这些是针对特定类型如 int、SpreadsheetCell 或 ChessPiece的 Grid 类模板的实例化。
at() 方法的更新
由于 m_cells 不再存储指针而是存储可选值at() 方法现在返回 std::optionalT 而不是 unique_ptrs即可以有类型 T 的值也可以为空的 optionals
std::optionalT at(size_t x, size_t y);
const std::optionalT at(size_t x, size_t y) const;Grid 类模板的方法定义
模板方法定义格式
每个 Grid 模板的方法定义都必须以 template typename T 说明符开头。构造函数如下所示
template typename T
GridT::Grid(size_t width, size_t height) : m_width { width }, m_height { height } {m_cells.resize(m_width);for (auto column : m_cells) {column.resize(m_height);}
}注意类模板的方法定义需要对使用该类模板的任何客户端代码可见。这对方法定义的位置施加了一些限制。通常它们直接放在类模板定义本身的同一文件中。本章后面讨论了绕过这一限制的一些方法。 类名和方法定义
请注意:: 前的类名是 GridT而不是 Grid。在所有方法和静态数据成员定义中你必须指定 GridT 作为类名。构造函数的主体与 GameBoard 构造函数相同。其他方法定义也类似于 GameBoard 类中的对应方法但有适当的模板和 GridT 语法变化
template typename T
void GridT::verifyCoordinate(size_t x, size_t y) const {if (x m_width) {throw std::out_of_range { std::format({} must be less than {}., x, m_width) };}if (y m_height) {throw std::out_of_range { std::format({} must be less than {}., y, m_height) };}
}template typename T
const std::optionalT GridT::at(size_t x, size_t y) const {verifyCoordinate(x, y);return m_cells[x][y];
}template typename T
std::optionalT GridT::at(size_t x, size_t y) {return const_caststd::optionalT(std::as_const(*this).at(x, y));
}类模板方法的默认值 注意如果类模板方法的实现需要某个模板类型参数的默认值例如 T则可以使用 T{} 语法。T{} 调用对象的默认构造函数如果 T 是类类型或生成零如果 T 是基本类型。这种语法称为零初始化语法。它是为尚不知道类型的变量提供合理默认值的好方法。 使用 Grid 类模板
模板实例化
当你想要创建 Grid 对象时不能单独使用 Grid 作为类型你必须指定将存储在该 Grid 中的类型。为特定类型创建类模板对象称为实例化模板。以下是一个示例
Gridint myIntGrid; // 声明一个存储 int 的网格使用构造函数的默认参数。
Griddouble myDoubleGrid { 11, 11 }; // 声明一个 11x11 的 double 类型网格。
myIntGrid.at(0, 0) 10;
int x { myIntGrid.at(0, 0).value_or(0) };
Gridint grid2 { myIntGrid }; // 拷贝构造函数
Gridint anotherIntGrid;
anotherIntGrid grid2; // 赋值操作符请注意 myIntGrid、grid2 和 anotherIntGrid 的类型是 Gridint。你不能在这些网格中存储 SpreadsheetCells 或 ChessPieces如果尝试这样做编译器将生成错误。
使用 value_or()
还要注意 value_or() 的使用。at() 方法返回一个可选引用可能包含值也可能不包含。value_or() 方法在可选项中有值时返回该值否则它返回给 value_or() 的参数。
模板类型的重要性
模板类型的指定非常重要以下两行都无法编译
Grid test; // 无法编译
Grid test; // 无法编译如果你想声明一个接受 Grid 对象的函数或方法你必须在 Grid 类型中指定存储在网格中的类型
void processIntGrid(Gridint grid) { /* 省略正文以简洁 */ }或者你可以使用本章后面讨论的函数模板编写一个模板化的函数该函数根据网格中元素的类型进行模板化。 注意你可以使用类型别名来简化完整的 Grid 类型的重复书写例如 Gridint using IntGrid Gridint;
void processIntGrid(IntGrid grid) { /* 正文 */ }Grid 类模板的多样性
Grid 类模板可以存储的不仅仅是 int。例如你可以实例化一个存储 SpreadsheetCells 的 Grid
GridSpreadsheetCell mySpreadsheet;
SpreadsheetCell myCell { 1.234 };
mySpreadsheet.at(3, 4) myCell;你也可以存储指针类型
Gridconst char* myStringGrid;
myStringGrid.at(2, 2) hello;指定的类型甚至可以是另一个模板类型
Gridvectorint gridOfVectors;
vectorint myVector { 1, 2, 3, 4 };
gridOfVectors.at(5, 6) myVector;你还可以在自由存储区动态分配 Grid 模板实例
auto myGridOnFreeStore { make_uniqueGridint(2, 2) }; // 自由存储区上的 2x2 网格。
myGridOnFreeStore-at(0, 0) 10;
int x { myGridOnFreeStore-at(0, 0).value_or(0) };