17.x — 第 17 章总结和测验

章节回顾

固定大小数组(或定长数组)要求数组的长度在实例化时已知,并且该长度之后不能更改。C 风格数组和 std::array 都是固定大小数组。动态数组可以在运行时调整大小。std::vector 是一个动态数组。

std::array 的长度必须是一个常量表达式。通常,为长度提供的值将是整数文字、constexpr 变量或无作用域枚举器。

std::array 是一个聚合体。这意味着它没有构造函数,而是使用聚合初始化进行初始化。

尽可能将 std::array 定义为 constexpr。如果您的 std::array 不是 constexpr,请考虑改用 std::vector

使用类模板参数推导 (CTAD) 让编译器从其初始化器推导 std::array 的类型和长度。

std::array 被实现为一个模板结构体,其声明如下:

template<typename T, std::size_t N> // N is a non-type template parameter
struct array;

表示数组长度的非类型模板参数 (N) 的类型为 std::size_t

获取 std::array 的长度

  • 我们可以使用 size() 成员函数(返回长度为无符号 size_type)询问 std::array 对象的长度。
  • 在 C++17 中,我们可以使用 std::size() 非成员函数(对于 std::array,它只是调用 size() 成员函数,因此返回长度为无符号 size_type)。
  • 在 C++20 中,我们可以使用 std::ssize() 非成员函数,它返回长度作为一个大的有符号整型(通常是 std::ptrdiff_t)。

所有这三个函数都将返回长度作为一个 constexpr 值,除非在通过引用传递的 std::array 上调用。此缺陷已在 C++23 中通过 P2280 解决。

索引 std::array

  • 使用下标运算符(operator[])。在这种情况下不进行边界检查,传入无效索引将导致未定义行为。
  • 使用 at() 成员函数,它在运行时进行下标操作并进行边界检查。我们建议避免使用此函数,因为我们通常希望在索引之前进行边界检查,或者我们希望进行编译时边界检查。
  • 使用 std::get() 函数模板,它将索引作为非类型模板参数,并进行编译时边界检查。

您可以使用模板参数声明 template <typename T, std::size_t N> 将具有不同元素类型和长度的 std::array 传递给函数。或者在 C++20 中,使用 template <typename T, auto N>

按值返回 std::array 将复制数组和所有元素,但如果数组很小且元素复制成本不高,这可能是可以接受的。在某些情况下,使用输出参数可能是一个更好的选择。

当使用结构体、类或数组初始化 std::array 并且不为每个初始化器提供元素类型时,您需要额外的一对大括号,以便编译器能够正确解释要初始化什么。这是聚合初始化的一个产物,其他标准库容器类型(使用列表构造函数)在这种情况下不需要双大括号。

C++ 中的聚合体支持一种称为大括号省略的概念,它规定了一些何时可以省略多个大括号的规则。通常,当用标量(单个)值初始化 std::array,或当用类类型或数组初始化时,并且每个元素都明确指定了类型时,可以省略大括号。

不能有引用数组,但可以有 std::reference_wrapper 数组,其行为类似于可修改的左值引用。

关于 std::reference_wrapper,有几点值得注意:

  • Operator= 将重新设置 std::reference_wrapper(更改引用的对象)。
  • std::reference_wrapper<T> 将隐式转换为 T&
  • get() 成员函数可用于获取 T&。当我们想要更新所引用对象的值时,这很有用。

提供了 std::ref()std::cref() 函数作为创建 std::reference_wrapperconst std::reference_wrapper 包装对象的快捷方式。

尽可能使用 static_assert 来确保使用 CTAD 的 constexpr std::array 具有正确数量的初始化器。

C 风格数组继承自 C 语言,并且是 C++ 核心语言的内置部分。因为它们是核心语言的一部分,C 风格数组有自己特殊的声明语法。在 C 风格数组声明中,我们使用方括号 ([]) 告诉编译器声明的对象是一个 C 风格数组。在方括号内,我们可以选择提供数组的长度,它是一个 std::size_t 类型的整数值,告诉编译器数组中有多少个元素。C 风格数组的长度必须是一个常量表达式。

C 风格数组是聚合体,这意味着它们可以使用聚合初始化进行初始化。当使用初始化列表初始化 C 风格数组的所有元素时,最好省略长度,让编译器计算数组的长度。

C 风格数组可以通过 operator[] 进行索引。C 风格数组的索引可以是带符号或无符号整数,也可以是无作用域枚举。这意味着 C 风格数组不受标准库容器类所有符号转换索引问题的影响!

C 风格数组可以是 const 或 constexpr。

获取 C 风格数组的长度

  • 在 C++17 中,我们可以使用 std::size() 非成员函数,它返回无符号的 std::size_t 类型长度。
  • 在 C++20 中,我们可以使用 std::ssize() 非成员函数,它返回长度作为一个大的有符号整型(通常是 std::ptrdiff_t)。

在大多数情况下,当 C 风格数组在表达式中使用时,数组将被隐式转换为指向元素类型的指针,并用第一个元素(索引为 0)的地址初始化。通俗地说,这被称为数组衰减(或简称衰减)。

指针算术是一种特性,允许我们对指针应用某些整数算术运算符(加、减、增、减)以产生新的内存地址。给定一些指针 ptrptr + 1 返回内存中下一个对象的地址(基于指向的类型)。

当从数组开头(元素 0)索引时使用下标,这样数组索引与元素对齐。
当从给定元素进行相对定位时,使用指针算术。

C 风格字符串只是元素类型为 charconst char 的 C 风格数组。因此,C 风格字符串会衰减。

数组的维数是选择一个元素所需的索引数量。

只包含一个维度的数组称为单维数组(有时缩写为 1d 数组)。数组的数组称为二维数组(有时缩写为 2d 数组),因为它有两个下标。具有多个维度的数组称为多维数组扁平化数组是减少数组维度的过程(通常减少到单个维度)。

在 C++23 中,std::mdspan 是一个视图,为连续的元素序列提供多维数组接口。

小测验时间

问题 #1

这些片段有什么问题,你将如何修复它们?

a)

#include <array>
#include <iostream>

int main()
{
    std::array arr { 0, 1, 2, 3 };

    for (std::size_t count{ 0 }; count <= std::size(arr); ++count)
    {
        std::cout << arr[count] << ' ';
    }

    std::cout << '\n';

    return 0;
}

显示答案

b)

#include <iostream>

void printArray(int array[])
{
    for (int element : array)
    {
        std::cout << element << ' ';
    }
}

int main()
{
    int array[] { 9, 7, 5, 3, 1 };

    printArray(array);

    std::cout << '\n';

    return 0;
}

显示答案

c)

#include <array>
#include <iostream>

int main()
{
    std::cout << "Enter the number of test scores: ";
    std::size_t length{};
    std::cin >> length;

    std::array<int, length> scores;

    for (std::size_t i { 0 } ; i < length; ++i)
    {
        std::cout << "Enter score " << i << ": ";
        std::cin >> scores[i];
    }
    return 0;
}

显示答案

问题 #2

本次测验,我们将实现罗斯科的药水店,这片土地上最好的药水店!这将是一个更大的挑战。

实现一个输出以下内容的程序

Welcome to Roscoe's potion emporium!
Enter your name: Alex
Hello, Alex, you have 85 gold.

Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Enter the number of the potion you'd like to buy, or 'q' to quit: a
That is an invalid input.  Try again: 3
You purchased a potion of invisibility.  You have 35 gold left.

Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Enter the number of the potion you'd like to buy, or 'q' to quit: 4
That is an invalid input.  Try again: 2
You purchased a potion of speed.  You have 23 gold left.

Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Enter the number of the potion you'd like to buy, or 'q' to quit: 2
You purchased a potion of speed.  You have 11 gold left.

Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Enter the number of the potion you'd like to buy, or 'q' to quit: 4
You can not afford that.

Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Enter the number of the potion you'd like to buy, or 'q' to quit: q

Your inventory contains: 
2x potion of speed
1x potion of invisibility
You escaped with 11 gold remaining.

Thanks for shopping at Roscoe's potion emporium!

玩家起始金币数量随机,介于 80 到 120 之间。

听起来有趣吗?那就动手吧!因为一次性实现所有功能会很困难,所以我们将分步开发。

> 步骤 #1

创建一个名为 Potion 的命名空间,其中包含一个名为 Type 的枚举,用于存储药水类型。创建两个 std::array:一个 int 数组用于存储药水成本,一个 std::string_view 数组用于存储药水名称。

还要编写一个名为 shop() 的函数,它遍历药水列表并打印它们的编号、名称和成本。

程序应输出以下内容

Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50

显示提示

显示答案

> 步骤 #2

创建一个 Player 类来存储玩家的姓名、药水库存和金币。添加 Roscoe 商店的介绍和告别文本。获取玩家姓名并随机化他们的金币。

使用课程 8.15 -- 全局随机数 (Random.h) 中的“Random.h”文件,使随机化变得容易。

程序应输出以下内容

Welcome to Roscoe's potion emporium!
Enter your name: Alex
Hello, Alex, you have 84 gold.

Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50

Thanks for shopping at Roscoe's potion emporium!

显示答案

> 步骤 #3

添加购买药水的功能,处理无效输入(将任何冗余输入视为失败)。玩家离开后打印玩家的库存。此步骤完成后,程序应完整。

确保测试以下情况

  • 用户输入无效药水编号(例如 'd')
  • 用户输入有效的药水编号但带有冗余输入(例如 2d, 25

我们在课程 9.5 -- std::cin 和处理无效输入 中介绍了无效输入处理。

显示提示

显示提示

显示答案

问题 #3

假设我们要编写一个使用标准扑克牌组的纸牌游戏。为此,我们需要一种方法来表示这些牌和牌组。让我们构建这个功能。

我们将在下一个测验问题中实际实现一个游戏。

> 步骤 #1

一副牌有 52 张独特的牌(4 种花色,每种花色 13 种牌)。为牌的等级(A、2、3、4、5、6、7、8、9、10、J、Q、K)和花色(梅花、方块、红心、黑桃)创建枚举。

显示答案

> 步骤 #2

每张牌都将由一个名为 Card 的结构体表示,该结构体包含一个等级和一个花色成员。创建该结构体并将枚举移入其中。

显示答案

> 步骤 #3

接下来,让我们为我们的 Card 结构体添加一些有用的函数。首先,重载 operator<< 以将牌的等级和花色打印为 2 字母代码(例如,黑桃 J 将打印为 JS)。您可以通过完成以下函数来做到这一点:

struct Card
{
    // Your other stuff here

    friend std::ostream& operator<<(std::ostream& out, const Card &card)
    {
        out << // print your card rank and suit here
        return out;
    }
};

其次,添加一个返回 Card 值的函数。将 Ace 视为值 11。最后,添加一个 Rank 和 Suit 的 std::array(分别命名为 allRanksallSuits),以便它们可以被迭代。因为这些是结构体的一部分(而不是命名空间),所以将它们设置为静态,以便它们只实例化一次(而不是每个对象实例化一次)。

以下代码应能编译:

int main()
{
    // Print one card
    Card card { Card::rank_5, Card::suit_heart };
    std::cout << card << '\n';

    // Print all cards
    for (auto suit : Card::allSuits)
        for (auto rank : Card::allRanks)
            std::cout << Card { rank, suit } << ' ';
    std::cout << '\n';

    return 0;
}

并产生以下输出:

5H
AC 2C 3C 4C 5C 6C 7C 8C 9C TC JC QC KC AD 2D 3D 4D 5D 6D 7D 8D 9D TD JD QD KD AH 2H 3H 4H 5H 6H 7H 8H 9H TH JH QH KH AS 2S 3S 4S 5S 6S 7S 8S 9S TS JS QS KS 

显示答案

> 第4步

接下来,我们来创建一副牌。创建一个名为 Deck 的类,其中包含一个 std::array 类型的 Card。您可以假定一副牌有 52 张。

甲板应该有三个功能

首先,默认构造函数应该初始化牌组数组。您可以使用类似于前面示例的 main() 函数中的范围 for 循环来遍历所有花色和牌面。

其次,添加一个 dealCard() 函数,该函数按值返回牌组中的下一张牌。由于 std::array 是一个固定大小的数组,请思考如何跟踪下一张牌的位置。当牌组中的所有牌都已发完时,调用此函数应断言失败。

第三,编写一个 shuffle() 成员函数来洗牌。为了简化,我们将借助 std::shuffle

#include <algorithm> // for std::shuffle
#include "Random.h"  // for Random::mt

    // Put this line in your shuffle function to shuffle m_cards using the Random::mt Mersenne Twister
    // This will rearrange all the Cards in the deck randomly
    std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);

shuffle() 函数也应该将你跟踪下一张牌位置的方式重置回牌组的开头。

以下程序应该运行

int main()
{
    Deck deck{};
    std::cout << deck.dealCard() << ' ' << deck.dealCard() << ' ' << deck.dealCard() << '\n';

    deck.shuffle();
    std::cout << deck.dealCard() << ' ' << deck.dealCard() << ' ' << deck.dealCard() << '\n';

    return 0;
}

并产生以下输出(最后 3 张牌应随机化)

AC 2C 3C
2H 7H 9C

显示答案

问题 #4

好的,现在让我们用我们的 Card 和 Deck 来实现一个简化版的二十一点!如果您还不熟悉二十一点,可以查看 二十一点 的维基百科文章。

这是我们版本的二十一点规则

  • 庄家先发一张牌(现实中庄家发两张,但其中一张是面朝下,所以目前无关紧要)。
  • 玩家先发两张牌。
  • 玩家先行。
  • 玩家可以反复“要牌”或“停牌”。
  • 如果玩家“停牌”,则其回合结束,并根据所发牌计算其分数。
  • 如果玩家“要牌”,他们会得到另一张牌,该牌的价值会加到他们的总分中。
  • 一张 A 通常计为 1 或 11(以对总分更有利者为准)。为简单起见,这里我们将其计为 11。
  • 如果玩家分数超过 21,则爆牌并立即输掉。
  • 玩家完成后,轮到庄家。
  • 庄家重复抽牌,直到分数达到 17 或以上,此时他们必须停止抽牌。
  • 如果庄家分数超过 21,则爆牌,玩家立即获胜。
  • 否则,如果玩家分数高于庄家,则玩家获胜。否则,玩家输(为简化起见,我们将平局视为庄家获胜)。

在我们简化版的二十一点中,我们不会跟踪玩家和庄家具体发到了哪些牌。我们只跟踪玩家和庄家发到的牌面总和。这样可以保持简单。

从您在之前的测验中编写的代码开始(或使用我们的参考解决方案)。

> 步骤 #1

创建一个名为 Player 的结构体,它将代表我们游戏中的参与者(庄家或玩家)。由于在这个游戏中我们只关心玩家的分数,所以这个结构体只需要一个成员。

编写一个函数,它将(最终)进行一轮二十一点游戏。目前,这个函数应该为庄家随机抽一张牌,为玩家随机抽两张牌。它应该返回一个布尔值,表示谁的分数更高。

代码应输出以下内容

The dealer is showing: 10
You have score: 13
You win!
The dealer is showing: 10
You have score: 8
You lose!

显示答案

> 步骤 #2

添加一个 Settings 命名空间,其中包含两个常量:玩家爆牌的分数,以及庄家必须停止抽牌的分数。

添加处理庄家回合的逻辑。庄家将抽牌直到达到 17 点,然后他们必须停止。如果他们爆牌,玩家获胜。

以下是一些示例输出

The dealer is showing: 8
You have score: 9
The dealer flips a 4D.  They now have: 12
The dealer flips a JS.  They now have: 22
The dealer went bust!
You win!
The dealer is showing: 6
You have score: 13
The dealer flips a 3D.  They now have: 9
The dealer flips a 3H.  They now have: 12
The dealer flips a 9S.  They now have: 21
You lose!
The dealer is showing: 7
You have score: 21
The dealer flips a JC.  They now have: 17
You win!

显示答案

> 步骤 #3

最后,添加玩家回合的逻辑。这将完成游戏。

以下是一些示例输出

The dealer is showing: 2
You have score: 14
(h) to hit, or (s) to stand: h
You were dealt KH.  You now have: 24
You went bust!
You lose!
The dealer is showing: 10
You have score: 9
(h) to hit, or (s) to stand: h
You were dealt TH.  You now have: 19
(h) to hit, or (s) to stand: s
The dealer flips a 3D.  They now have: 13
The dealer flips a 7H.  They now have: 20
You lose!
The dealer is showing: 7
You have score: 12
(h) to hit, or (s) to stand: h
You were dealt 7S.  You now have: 19
(h) to hit, or (s) to stand: h
You were dealt 2D.  You now have: 21
(h) to hit, or (s) to stand: s
The dealer flips a 6H.  They now have: 13
The dealer flips a QC.  They now have: 23
The dealer went bust!
You win!

显示答案

问题 #5

a) 描述如何修改上述程序以处理 A 可以等于 1 或 11 的情况。

重要的是要注意,我们只跟踪牌的总和,而不是用户具体有哪些牌。

显示答案

b) 在实际的二十一点游戏中,如果玩家和庄家得分相同(且玩家未爆牌),结果是平局,双方均不赢。描述你将如何修改上述程序来处理这种情况。

显示答案

c) 额外奖励:将上述两个想法实现到你的二十一点游戏中。请注意,你需要显示庄家的初始牌和玩家的初始两张牌,以便他们知道自己是否有 A。

以下是示例输出:

The dealer is showing JH (10)
You are showing AH 7D (18)
(h) to hit, or (s) to stand: h
You were dealt JD.  You now have: 18
(h) to hit, or (s) to stand: s
The dealer flips a 6C.  They now have: 16
The dealer flips a AD.  They now have: 17
You win!

显示答案

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
1.6K 评论
最新
最早 最多投票
内联反馈
查看所有评论