17.x — 第十七章总结与测验(to do)

章节回顾

固定大小数组Fixed-size arrays(或固定长度数组fixed-length arrays)要求在实例化时必须已知数组长度,且该长度不可在后续修改。C 风格数组和 std::array 均属于固定大小数组。动态数组可在运行时调整大小,std::vector 即为动态数组。

std::array的长度必须是常量表达式。通常情况下,长度参数会采用整数字面量、constexpr变量或无作用域枚举器。

std::array属于聚合类型。这意味着它不提供构造函数,而是通过聚合初始化进行初始化。

尽可能将 std::array 定义为 constexpr。若无法实现 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() 成员函数查询 std::array 对象的长度(该函数返回长度值,类型为 unsigned size_type)。
  • 在C++17中,可使用非成员函数std::size()(该函数对std::array调用size()成员函数,因此返回长度时仍为unsigned size_type类型)。
  • 在 C++20 中,可使用非成员函数 std::ssize(),该函数返回长度时采用大型有符号整数类型(通常为 std::ptrdiff_t)。

上述三种函数均会返回长度作为 constexpr 值,但通过引用传递 std::array 时除外。此缺陷已在 C++23 的 P2280 提案中修复。

访问 std::array 的索引:

  • 使用下标运算符 (operator[])。此方式不进行边界检查,传递无效索引将导致未定义行为。
  • 使用 at() 成员函数进行带运行时边界检查的索引操作。我们建议避免使用该函数,因为通常需要在索引前进行边界检查,或需要编译时边界检查。
  • 使用 std::get() 函数模板,该模板将索引作为非类型模板参数,并执行编译时边界检查。

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

按值返回 std::array 会复制数组及所有元素,但若数组较小且元素复制成本不高,这种方式尚可接受。某些场景下使用 out 参数可能是更优选择。

当使用结构体、类或数组初始化 std::array 时,若未为每个初始化项指定元素类型,则需额外添加一对大括号以便编译器正确解析初始化内容。这是聚合初始化的特性,其他使用列表构造函数的标准库容器类型在此类情况下无需双重大括号。

C++聚合体支持“括号省略brace elision”机制,该机制规定了多组括号可省略的条件。通常在以下情形可省略括号:使用标量(单值)初始化std::array时;或使用类类型/数组初始化且每个元素类型均显式指定时。

无法创建引用数组,但可创建 std::reference_wrapper 数组,其行为类似可修改的左值引用。

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

  • 赋值运算符 = 会重新定位 std::reference_wrapper(改变被引用的对象)。
  • std::reference_wrapper 会隐式转换为 T&。
  • 通过 get() 成员函数可获取 T&。当需要更新被引对象的值时此特性尤为实用。

std::ref() 和 std::cref() 函数作为快捷方式,用于创建 std::reference_wrapper 和 const std::reference_wrapper 包装对象。

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

C 风格数组继承自 C 语言,是 C++ 核心语言的内置特性。由于属于核心语言,C 风格数组拥有专属的声明语法。在C风格数组声明中,使用方括号([])告知编译器该对象为C风格数组。方括号内可选地提供数组长度,该长度为std::size_t类型的整数值,用于告知编译器数组包含多少个元素。C风格数组的长度必须是常量表达式。

C 风格数组属于聚合体,因此可通过聚合初始化进行初始化。使用初始化列表为 C 风格数组所有元素赋值时,建议省略长度参数,由编译器自动计算数组长度。

C 风格数组可通过 [] 运算符进行索引。其索引值可以是带符号或无符号整数,也可为无作用域枚举类型。这意味着 C 风格数组不会遇到标准库容器类所面临的所有符号转换索引问题!

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

要获取 C 样式数组的长度:

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

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

指针算术Pointer arithmetic是一项功能,允许我们对指针应用某些整数算术运算符(加法、减法、递增或递减)以生成新的内存地址。给定一些指针 ptr,ptr + 1 返回内存中下一个对象的地址(基于所指向的类型)。

从数组开头(元素 0)开始索引时使用下标,以便数组索引与元素对齐。

从给定元素进行相对定位时使用指针算术。

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

数组的维数dimension是选择元素所需的索引数。

仅包含单个维度的数组称为单维数组single-dimensional array一维数组one-dimensional array(有时缩写为1d数组1d array)。数组的数组称为二维数组two-dimensional array(有时缩写为 2d 数组2d array),因为它有两个下标。超过一维的数组称为多维数组multidimensional arrays展平Flattening数组是减少数组维数(通常减少到一维)的过程。

在 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;
}

显示答案

for循环存在偏移量差一错误,试图访问不存在的第4个数组元素。

解决方案:for循环中的条件语句应使用<而非<=。

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

显示答案

当数组传递给printArray()时,会衰变为指针。基于范围的for循环无法处理数组指针,因为数组的大小未知。

解决方案:改用std::array,它不会发生衰变。

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

显示答案

length 不是常量表达式,不能用于定义 std::array 的长度。

解决方案:改用 std::vector。


问题 #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 数组用于存储药水价格(potion costs),一个 std::string_view 数组用于存储药水(potion)名称。

同时编写一个名为 shop() 的函数,该函数遍历药水(Potions)列表并打印其编号、名称和价格。

程序应输出以下内容:

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

显示提示

提示:我们在第17.6节——std::array与枚举类型中,展示了如何使用基于范围的for循环遍历枚举类型。

显示解决方案

#include <array>
#include <iostream>
#include <string_view>

namespace Potion
{
    enum Type
    {
    healing,
    mana,
    speed,
    invisibility,
    max_potions
    };

    constexpr std::array types { healing, mana, speed, invisibility }; // An array of our enumerators

    // We could put these in a struct, but since we only have two attributes we'll keep them separate for now
    // We will explicitly define the element type so we don't have to use the sv suffix
    constexpr std::array<std::string_view, max_potions> name { "healing", "mana", "speed", "invisibility" };
    constexpr std::array cost { 20, 30, 12, 50 };

    static_assert(std::size(types) == max_potions);  // ensure 'all' contains the correct number of enumerators
    static_assert(std::size(cost) == max_potions);
    static_assert(std::size(name) == max_potions);
}

void shop()
{
    std::cout << "Here is our selection for today:\n";

    for (auto p: Potion::types)
        std::cout << p << ") " << Potion::name[p] << " costs " << Potion::cost[p] << '\n';
}

int main()
{
    shop();

    return 0;
}

步骤 #2

创建一个玩家类(Player)来存储玩家名称、药水库存和金币。添加罗斯科商店的欢迎语和告别语。获取玩家名称并随机生成其金币数量。

使用第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!

显示解决方案

#include <array>
#include <iostream>
#include <string_view>
#include "Random.h"

namespace Potion
{
    enum Type
    {
        healing,
        mana,
        speed,
        invisibility,
        max_potions
    };

    constexpr std::array types { healing, mana, speed, invisibility }; // An array of our enumerators

    // We could put these in a struct, but since we only have two attributes we'll keep them separate for now
    // We will explicitly define the element type so we don't have to use the sv suffix
    constexpr std::array<std::string_view, max_potions> name { "healing", "mana", "speed", "invisibility" };
    constexpr std::array cost { 20, 30, 12, 50 };

    static_assert(std::size(types) == max_potions);  // ensure 'all' contains the correct number of enumerators
    static_assert(std::size(cost) == max_potions);
    static_assert(std::size(name) == max_potions);
}

class Player
{
private:
    static constexpr int s_minStartingGold { 80 };
    static constexpr int s_maxStartingGold { 120 };

    std::string m_name {};
    int m_gold {};
    std::array<int, Potion::max_potions> m_inventory { };

public:
    explicit Player(std::string_view name) :
        m_name { name },
        m_gold { Random::get(s_minStartingGold, s_maxStartingGold) }
    {
    }

    int gold() const { return m_gold; }
    int inventory(Potion::Type p) const { return m_inventory[p]; }
};

void shop()
{
    std::cout << "Here is our selection for today:\n";

    for (auto p: Potion::types)
        std::cout << p << ") " << Potion::name[p] << " costs " << Potion::cost[p] << '\n';
}

int main()
{
    std::cout << "Welcome to Roscoe's potion emporium!\n";
    std::cout << "Enter your name: ";

    std::string name{};
    std::getline(std::cin >> std::ws, name); // read a full line of text into name

    Player player { name };

    std::cout << "Hello, " << name << ", you have " << player.gold() << " gold.\n\n";

    shop();

    std::cout << "\nThanks for shopping at Roscoe's potion emporium!\n";

    return 0;
}

步骤 #3

添加购买药水功能,处理无效输入(将任何多余输入视为失败)。玩家离开后打印其物品栏。完成此步骤后程序应已完成。

请确保测试以下情况:

用户输入无效药水编号(如'd')

用户输入有效药水编号但包含多余内容(如2d, 25)

第9.5课已讲解无效输入处理——std::cin与无效输入处理。

显示提示

提示:用户可输入数字或字母‘q’,因此需将用户输入转换为字符类型。

要将ASCII数字字符转换为整数(例如将'5'转换为5),可使用以下方法:
int charNumToInt(char c)
{
    return c - '0';
}

显示提示

提示:编写一个函数来处理用户输入。该函数应返回用户选择的药水类型(Potion::Type)。若用户选择退出,函数可返回默认值 Potion::max_potions。你需要将用户的输入静态转换为 Potion::Type 类型。

显示解决方案

#include <array>
#include <iostream>
#include <limits> // for std::numeric_limits
#include <string_view>
#include "Random.h"

namespace Potion
{
    enum Type
    {
        healing,
        mana,
        speed,
        invisibility,
        max_potions
    };

    constexpr std::array types { healing, mana, speed, invisibility }; // An array of our enumerators

    // We could put these in a struct, but since we only have two attributes we'll keep them separate for now
    // We will explicitly define the element type so we don't have to use the sv suffix
    constexpr std::array<std::string_view, max_potions> name { "healing", "mana", "speed", "invisibility" };
    constexpr std::array cost { 20, 30, 12, 50 };

    static_assert(std::size(types) == max_potions);  // ensure 'all' contains the correct number of enumerators
    static_assert(std::size(cost) == max_potions);
    static_assert(std::size(name) == max_potions);
}

class Player
{
private:
    static constexpr int s_minStartingGold { 80 };
    static constexpr int s_maxStartingGold { 120 };

    std::string m_name {};
    int m_gold {};
    std::array<int, Potion::max_potions> m_inventory { };

public:
    explicit Player(std::string_view name) :
        m_name { name },
        m_gold { Random::get(s_minStartingGold, s_maxStartingGold) }
    {
    }

    // returns false if can't afford, true if purchased
    bool buy(Potion::Type type)
    {
        if (m_gold < Potion::cost[type])
            return false;

        m_gold -= Potion::cost[type];
        ++m_inventory[type];
        return true;
    }

    int gold() const { return m_gold; }
    int inventory(Potion::Type p) const { return m_inventory[p]; }
};

void ignoreLine()
{
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

int charNumToInt(char c)
{
    return c - '0';
}

Potion::Type whichPotion()
{
    std::cout << "Enter the number of the potion you'd like to buy, or 'q' to quit: ";
    char input{};
    while (true)
    {
        std::cin >> input;
        if (!std::cin)
        {
            std::cin.clear(); // put us back in 'normal' operation mode
            ignoreLine(); // and remove the bad input
            continue;
        }

        // If there is extraneous input, treat as failure case
        if (!std::cin.eof() && std::cin.peek() != '\n')
        {
            std::cout << "I didn't understand what you said.  Try again: ";
            ignoreLine(); // ignore any extraneous input
            continue;
        }

        if (input == 'q')
            return Potion::max_potions;

        // Convert the char to a number and see if it's a valid potion selection
        int val { charNumToInt(input) };
        if (val >= 0 && val < Potion::max_potions)
            return static_cast<Potion::Type>(val);

        // It wasn't a valid potion selection
        std::cout << "I didn't understand what you said.  Try again: ";
        ignoreLine();
    }
}

void shop(Player &player)
{
    while (true)
    {
        std::cout << "Here is our selection for today:\n";

        for (auto p: Potion::types)
            std::cout << p << ") " << Potion::name[p] << " costs " << Potion::cost[p] << '\n';

        Potion::Type which { whichPotion() };
        if (which == Potion::max_potions)
            return;

        bool success { player.buy(which) };
        if (!success)
            std::cout << "You can not afford that.\n\n";
        else
            std::cout << "You purchased a potion of " << Potion::name[which] << ".  You have " << player.gold() << " gold left.\n\n";
    }
}

void printInventory(Player& player)
{
    std::cout << "Your inventory contains: \n";

    for (auto p: Potion::types)
    {
        if (player.inventory(p) > 0)
            std::cout << player.inventory(p) << "x potion of " << Potion::name[p] << '\n';
    }

    std::cout << "You escaped with " << player.gold() << " gold remaining.\n";
}

int main()
{
    std::cout << "Welcome to Roscoe's potion emporium!\n";
    std::cout << "Enter your name: ";

    std::string name{};
    std::cin >> name;

    Player player { name };

    std::cout << "Hello, " << name << ", you have " << player.gold() << " gold.\n\n";

    shop(player);

    std::cout << '\n';

    printInventory(player);

    std::cout << "\nThanks for shopping at Roscoe's potion emporium!\n";

    return 0;
}


问题 #3

假设我们要编写一款使用标准扑克牌的纸牌游戏。为此需要建立牌张与牌组的表示机制,让我们实现该功能。

后续测验题将运用此功能实现实际游戏。

步骤 #1

一副牌包含52张独特卡片(13个点数×4种花色)。创建点数枚举(A、2、3、4、5、6、7、8、9、10、J、Q、K)和花色枚举(梅花、方块、红心、黑桃)。

显示解决方案

// Because identifiers can't start with a number, we'll use a "rank_" prefix for these
enum Rank
{
    rank_ace,
    rank_2,
    rank_3,
    rank_4,
    rank_5,
    rank_6,
    rank_7,
    rank_8,
    rank_9,
    rank_10,
    rank_jack,
    rank_queen,
    rank_king,

    max_ranks
};

// We'll also prefix these for consistency
enum Suit
{
    suit_club,
    suit_diamond,
    suit_heart,
    suit_spade,

    max_suits
};

步骤 #2

每张牌将由名为Card的结构体表示,包含点数和花色成员变量。创建该结构体并将枚举类型移入其中。

显示解决方案

struct Card
{
    enum Rank
    {
        rank_ace,
        rank_2,
        rank_3,
        rank_4,
        rank_5,
        rank_6,
        rank_7,
        rank_8,
        rank_9,
        rank_10,
        rank_jack,
        rank_queen,
        rank_king,

        max_ranks
    };

    enum Suit
    {
        suit_club,
        suit_diamond,
        suit_heart,
        suit_spade,

        max_suits
    };

    Rank rank{};
    Suit suit{};
};

步骤 #3

接下来为Card结构体添加实用函数。首先重载<<运算符,以双字母代码形式输出点数和花色(例如黑桃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;
    }
};

其次,添加一个返回卡牌点数值的函数。将A视为11点。最后,添加一个包含所有点数(命名为allRanks)和所有花色(命名为allSuits)的std::array数组以便遍历。由于这些属于结构体(而非命名空间),请将其设为静态变量,确保仅在初始化时创建一次(而非随每个对象创建)。

以下代码应能编译通过:

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 

显示解决方案

#include <array>
#include <iostream>

struct Card
{
    enum Rank
    {
        rank_ace,
        rank_2,
        rank_3,
        rank_4,
        rank_5,
        rank_6,
        rank_7,
        rank_8,
        rank_9,
        rank_10,
        rank_jack,
        rank_queen,
        rank_king,

        max_ranks
    };

    enum Suit
    {
        suit_club,
        suit_diamond,
        suit_heart,
        suit_spade,

        max_suits
    };

    // These need to be static so they are only created once per program, not once per Card
    static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
    static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };

    Rank rank{};
    Suit suit{};

    friend std::ostream& operator<<(std::ostream& out, const Card &card)
    {
        static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
        static constexpr std::array suits { 'C', 'D', 'H', 'S' };

        out << ranks[card.rank] << suits[card.suit];
        return out;
    }

    int value() const
    {
        static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
        return rankValues[rank];
    }
};

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

步骤 #4

接下来,创建我们的扑克牌组。创建名为Deck的类,其中包含一个Cards类型的std::array。可假设一副牌有52张。

Deck类应包含三个函数:

首先,默认构造函数需初始化牌组数组。可使用类似前例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

显示答案:

#include <algorithm> // for std::shuffle
#include <array>
#include <cassert>
#include <iostream>
#include "Random.h"

struct Card
{
    enum Rank
    {
        rank_ace,
        rank_2,
        rank_3,
        rank_4,
        rank_5,
        rank_6,
        rank_7,
        rank_8,
        rank_9,
        rank_10,
        rank_jack,
        rank_queen,
        rank_king,

        max_ranks
    };

    enum Suit
    {
        suit_club,
        suit_diamond,
        suit_heart,
        suit_spade,

        max_suits
    };

    static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
    static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };

    Rank rank{};
    Suit suit{};

    friend std::ostream& operator<<(std::ostream& out, const Card &card)
    {
        static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
        static constexpr std::array suits { 'C', 'D', 'H', 'S' };

        out << ranks[card.rank] << suits[card.suit];
        return out;
    }

    int value() const
    {
        static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
        return rankValues[rank];
    }
};

class Deck
{
private:
    std::array<Card, 52> m_cards {};
    std::size_t m_nextCardIndex { 0 };

public:
    Deck()
    {
        std::size_t count { 0 };
        for (auto suit: Card::allSuits)
            for (auto rank: Card::allRanks)
                m_cards[count++] = Card{rank, suit};
    }

    void shuffle()
    {
        std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);
        m_nextCardIndex = 0;
    }

    Card dealCard()
    {
        assert(m_nextCardIndex != 52 && "Deck::dealCard ran out of cards");
        return m_cards[m_nextCardIndex++];
    }
};

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


问题 #4

好,现在让我们用卡牌和牌组实现一个简化版的二十一点!如果你还不熟悉二十一点,维基百科的二十一点条目中有个概述。

以下是我们版本的二十一点规则:

  1. 庄家起手一张牌(现实中庄家起手两张,但其中一张是面朝下的,所以此刻无关紧要)。
  2. 玩家起手两张牌。
  3. 玩家先行动。
  4. 玩家可反复选择“要牌”或“停牌”。
  5. 若玩家“停牌”,则结束回合,根据当前持牌计算得分。
  6. 若玩家“要牌”,则抽取新牌并将该牌点数加至总分。
  7. A通常计为1点或11点(取更有利的点数)。为简化规则,此处统一计为11点。
  8. 若玩家点数超过21点即爆牌,当场输掉。
  9. 玩家行动结束后,轮到庄家行动。
  10. 庄家持续要牌直至点数达到17点或以上,此时必须停止要牌。
  11. 若庄家点数超过21点则爆牌,玩家立即获胜。
  12. 否则,若玩家点数高于庄家则玩家获胜,反之玩家失败(为简化处理,平局视为庄家获胜)。

在本简化版二十一点中,我们不追踪玩家与庄家具体持有的牌面。仅记录玩家与庄家已获牌的点数总和,以保持流程简洁。

请基于前次测验的代码(或参考我们的标准解法)开始编写。


步骤 #1

创建名为 Player 的结构体,用于表示游戏参与者(庄家或玩家)。因本游戏仅关注玩家得分,该结构体只需一个成员变量。

编写函数实现(最终)完成一轮二十一点游戏。当前阶段该函数需为庄家随机发一张牌,为玩家随机发两张牌,并返回布尔值判定双方得分高低。

代码输出应如下所示:

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


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

显示答案

#include <algorithm> // for std::shuffle
#include <array>
#include <cassert>
#include <iostream>
#include "Random.h"

struct Card
{
    enum Rank
    {
        rank_ace,
        rank_2,
        rank_3,
        rank_4,
        rank_5,
        rank_6,
        rank_7,
        rank_8,
        rank_9,
        rank_10,
        rank_jack,
        rank_queen,
        rank_king,

        max_ranks
    };

    enum Suit
    {
        suit_club,
        suit_diamond,
        suit_heart,
        suit_spade,

        max_suits
    };

    static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
    static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };

    Rank rank{};
    Suit suit{};

    friend std::ostream& operator<<(std::ostream& out, const Card &card)
    {
        static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
        static constexpr std::array suits { 'C', 'D', 'H', 'S' };

        out << ranks[card.rank] << suits[card.suit];
        return out;
    }

    int value() const
    {
        static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
        return rankValues[rank];
    }
};

class Deck
{
private:
    std::array<Card, 52> m_cards {};
    std::size_t m_nextCardIndex { 0 };

public:
    Deck()
    {
        std::size_t count { 0 };
        for (auto suit: Card::allSuits)
            for (auto rank: Card::allRanks)
                m_cards[count++] = Card{rank, suit};
    }

    void shuffle()
    {
        std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);
        m_nextCardIndex = 0;
    }

    Card dealCard()
    {
        assert(m_nextCardIndex != 52 && "Deck::dealCard ran out of cards");
        return m_cards[m_nextCardIndex++];
    }
};

struct Player
{
    int score{};
};

bool playBlackjack()
{
    Deck deck{};
    deck.shuffle();

    Player dealer{ deck.dealCard().value() };

    std::cout << "The dealer is showing: " << dealer.score << '\n';

    Player player { deck.dealCard().value() + deck.dealCard().value() };

    std::cout << "You have score: " << player.score << '\n';

    return (player.score > dealer.score);
}

int main()
{
    if (playBlackjack())
    {
        std::cout << "You win!\n";
    }
    else
    {
        std::cout << "You lose!\n";
    }

    return 0;
}

步骤 #2

添加一个包含两个常量的设置命名空间:玩家爆牌的临界值,以及庄家必须停止要牌的点数。

添加处理庄家回合的逻辑。庄家将持续要牌直至达到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!

显示解决方案

#include <algorithm> // for std::shuffle
#include <array>
#include <cassert>
#include <iostream>
#include "Random.h"

struct Card
{
    enum Rank
    {
        rank_ace,
        rank_2,
        rank_3,
        rank_4,
        rank_5,
        rank_6,
        rank_7,
        rank_8,
        rank_9,
        rank_10,
        rank_jack,
        rank_queen,
        rank_king,

        max_ranks
    };

    enum Suit
    {
        suit_club,
        suit_diamond,
        suit_heart,
        suit_spade,

        max_suits
    };

    static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
    static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };

    Rank rank{};
    Suit suit{};

    friend std::ostream& operator<<(std::ostream& out, const Card &card)
    {
        static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
        static constexpr std::array suits { 'C', 'D', 'H', 'S' };

        out << ranks[card.rank] << suits[card.suit];
        return out;
    }

    int value() const
    {
        static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
        return rankValues[rank];
    }
};

class Deck
{
private:
    std::array<Card, 52> m_cards {};
    std::size_t m_nextCardIndex { 0 };

public:
    Deck()
    {
        std::size_t count { 0 };
        for (auto suit: Card::allSuits)
            for (auto rank: Card::allRanks)
                m_cards[count++] = Card{rank, suit};
    }

    void shuffle()
    {
        std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);
        m_nextCardIndex = 0;
    }

    Card dealCard()
    {
        assert(m_nextCardIndex != 52 && "Deck::dealCard ran out of cards");
        return m_cards[m_nextCardIndex++];
    }

};

struct Player
{
    int score{};
};

namespace Settings
{
    // Maximum score before losing.
    constexpr int bust{ 21 };

    // Minium score that the dealer has to have.
    constexpr int dealerStopsAt{ 17 };
}

// Returns true if the dealer went bust. False otherwise.
bool dealerTurn(Deck& deck, Player& dealer)
{
    while (dealer.score < Settings::dealerStopsAt)
    {
        Card card { deck.dealCard() };
        dealer.score += card.value();
        std::cout << "The dealer flips a " << card << ".  They now have: " << dealer.score << '\n';
    }

    if (dealer.score > Settings::bust)
    {
        std::cout << "The dealer went bust!\n";
        return true;
    }

    return false;
}

bool playBlackjack()
{
    Deck deck{};
    deck.shuffle();

    Player dealer{ deck.dealCard().value() };

    std::cout << "The dealer is showing: " << dealer.score << '\n';

    Player player { deck.dealCard().value() + deck.dealCard().value() };

    std::cout << "You have score: " << player.score << '\n';

    if (dealerTurn(deck, dealer))
        return true;

    return (player.score > dealer.score);
}

int main()
{
    if (playBlackjack())
    {
        std::cout << "You win!\n";
    }
    else
    {
        std::cout << "You lose!\n";
    }

    return 0;
}

步骤 #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!

显示解答

#include <algorithm> // for std::shuffle
#include <array>
#include <cassert>
#include <iostream>
#include "Random.h"

struct Card
{
    enum Rank
    {
        rank_ace,
        rank_2,
        rank_3,
        rank_4,
        rank_5,
        rank_6,
        rank_7,
        rank_8,
        rank_9,
        rank_10,
        rank_jack,
        rank_queen,
        rank_king,

        max_ranks
    };

    enum Suit
    {
        suit_club,
        suit_diamond,
        suit_heart,
        suit_spade,

        max_suits
    };

    static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
    static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };

    Rank rank{};
    Suit suit{};

    friend std::ostream& operator<<(std::ostream& out, const Card &card)
    {
        static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
        static constexpr std::array suits { 'C', 'D', 'H', 'S' };

        out << ranks[card.rank] << suits[card.suit];
        return out;
    }

    int value() const
    {
        static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
        return rankValues[rank];
    }
};

class Deck
{
private:
    std::array<Card, 52> m_cards {};
    std::size_t m_nextCardIndex { 0 };

public:
    Deck()
    {
        std::size_t count { 0 };
        for (auto suit: Card::allSuits)
            for (auto rank: Card::allRanks)
                m_cards[count++] = Card{rank, suit};
    }

    void shuffle()
    {
        std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);
        m_nextCardIndex = 0;
    }

    Card dealCard()
    {
        assert(m_nextCardIndex != 52 && "Deck::dealCard ran out of cards");
        return m_cards[m_nextCardIndex++];
    }

};

struct Player
{
    int score{};
};

namespace Settings
{
    // Maximum score before losing.
    constexpr int bust{ 21 };

    // Minium score that the dealer has to have.
    constexpr int dealerStopsAt{ 17 };
}

bool playerWantsHit()
{
    while (true)
    {
        std::cout << "(h) to hit, or (s) to stand: ";

        char ch{};
        std::cin >> ch;

        switch (ch)
        {
            case 'h':
                return true;
            case 's':
                return false;
        }
    }
}

// Returns true if the player went bust. False otherwise.
bool playerTurn(Deck& deck, Player& player)
{
    while (player.score < Settings::bust && playerWantsHit())
    {
        Card card { deck.dealCard() };
        player.score += card.value();

        std::cout << "You were dealt " << card  << ". You now have: " << player.score << '\n';
    }

    if (player.score > Settings::bust)
    {
        std::cout << "You went bust!\n";
        return true;
    }

    return false;
}

// Returns true if the dealer went bust. False otherwise.
bool dealerTurn(Deck& deck, Player& dealer)
{
    while (dealer.score < Settings::dealerStopsAt)
    {
        Card card { deck.dealCard() };
        dealer.score += card.value();
        std::cout << "The dealer flips a " << card << ".  They now have: " << dealer.score << '\n';
    }

    if (dealer.score > Settings::bust)
    {
        std::cout << "The dealer went bust!\n";
        return true;
    }

    return false;
}

bool playBlackjack()
{
    Deck deck{};
    deck.shuffle();

    Player dealer{ deck.dealCard().value() };

    std::cout << "The dealer is showing: " << dealer.score << '\n';

    Player player { deck.dealCard().value() + deck.dealCard().value() };

    std::cout << "You have score: " << player.score << '\n';

    if (playerTurn(deck, player))
        return false;

    if (dealerTurn(deck, dealer))
        return true;

    return (player.score > dealer.score);
}

int main()
{
    if (playBlackjack())
    {
        std::cout << "You win!\n";
    }
    else
    {
        std::cout << "You lose!\n";
    }

    return 0;
}


问题 #5

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

需注意:程序仅记录牌面总和,不追踪用户具体持有哪些牌。

显示解答

一种方法是记录玩家和庄家获得的A牌数量(在Player结构体中以整数形式存储)。当玩家或庄家点数超过21且其A牌计数器大于零时,可将该方得分减去10分(将A牌从11点转换为1点),同时减少A牌计数器值。此操作可重复执行直至A牌计数器归零。

b) 实际二十一点规则中,若玩家与庄家点数相同(且玩家未爆牌),则判定平局。描述如何修改上述程序以实现此规则。

显示解决方案

我们当前的playBlackjack()函数返回一个布尔值,表示玩家是否获胜。需要更新该函数使其返回三种结果:庄家胜、玩家胜、平局。最佳实现方式是为这三种情况定义枚举类型,让函数返回对应的枚举值。

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!

显示答案

#include <algorithm> // for std::shuffle
#include <array>
#include <cassert>
#include <iostream>
#include "Random.h"

namespace Settings
{
    // Maximum score before losing.
    constexpr int bust{ 21 };

    // Minium score that the dealer has to have.
    constexpr int dealerStopsAt{ 17 };
}

struct Card
{
    enum Rank
    {
        rank_ace,
        rank_2,
        rank_3,
        rank_4,
        rank_5,
        rank_6,
        rank_7,
        rank_8,
        rank_9,
        rank_10,
        rank_jack,
        rank_queen,
        rank_king,

        max_ranks
    };

    enum Suit
    {
        suit_club,
        suit_diamond,
        suit_heart,
        suit_spade,

        max_suits
    };

    static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
    static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };

    Rank rank{};
    Suit suit{};

    friend std::ostream& operator<<(std::ostream& out, const Card &card)
    {
        static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
        static constexpr std::array suits { 'C', 'D', 'H', 'S' };

        out << ranks[card.rank] << suits[card.suit];
        return out;
    }

    int value() const
    {
        static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
        return rankValues[rank];
    }
};

class Deck
{
private:
    std::array<Card, 52> m_cards {};
    std::size_t m_nextCardIndex { 0 };

public:
    Deck()
    {
        std::size_t count { 0 };
        for (auto suit: Card::allSuits)
            for (auto rank: Card::allRanks)
                m_cards[count++] = Card{rank, suit};
    }

    void shuffle()
    {
        std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);
        m_nextCardIndex = 0;
    }

    Card dealCard()
    {
        assert(m_nextCardIndex != 52 && "Deck::dealCard ran out of cards");
        return m_cards[m_nextCardIndex++];
    }

};

class Player
{
private:
    int m_score{ };
    int m_ace11Count { 0 }; // how many aces worth 11 points the player has

public:
    // We'll use a function to add the card to the player's score
    // Since we now need to count aces
    void addToScore(Card card)
    {
        m_score += card.value();
        if (card.rank == Card::rank_ace)
            ++m_ace11Count; // aces start at 11 points
        consumeAces();
    }

    // Decrease aceCount by 1 and
    void consumeAces()
    {
        // If the player would bust, see if we can switch aces from 11 points to 1
        while (m_score > Settings::bust && m_ace11Count > 0)
        {
            m_score -= 10;
            --m_ace11Count;
        }
    }

    int score() { return m_score; }
};

bool playerWantsHit()
{
    while (true)
    {
        std::cout << "(h) to hit, or (s) to stand: ";

        char ch{};
        std::cin >> ch;

        switch (ch)
        {
            case 'h':
                return true;
            case 's':
                return false;
        }
    }
}

// Returns true if the player went bust. False otherwise.
bool playerTurn(Deck& deck, Player& player)
{
    while (player.score() < Settings::bust && playerWantsHit())
    {
        Card card { deck.dealCard() };
        player.addToScore(card);

        std::cout << "You were dealt " << card  << ". You now have: " << player.score() << '\n';
    }

    if (player.score() > Settings::bust)
    {
        std::cout << "You went bust!\n";
        return true;
    }

    return false;
}


// Returns true if the dealer went bust. False otherwise.
bool dealerTurn(Deck& deck, Player& dealer)
{
    while (dealer.score() < Settings::dealerStopsAt)
    {
        Card card { deck.dealCard() };
        dealer.addToScore(card);

        std::cout << "The dealer flips a " << card << ".  They now have: " << dealer.score() << '\n';
    }

    if (dealer.score() > Settings::bust)
    {
        std::cout << "The dealer went bust!\n";
        return true;
    }

    return false;
}

enum class GameResult
{
    playerWon,
    dealerWon,
    tie
};

GameResult playBlackjack()
{
    Deck deck{};
    deck.shuffle();

    Player dealer{};
    Card card1 { deck.dealCard() };
    dealer.addToScore(card1);
    std::cout << "The dealer is showing " << card1 << " (" << dealer.score() << ")\n";

    Player player{};
    Card card2 { deck.dealCard() };
    Card card3 { deck.dealCard() };
    player.addToScore(card2);
    player.addToScore(card3);
    std::cout << "You are showing " << card2 << ' ' << card3 << " (" << player.score() << ")\n";

    if (playerTurn(deck, player)) // if player busted
        return GameResult::dealerWon;

    if (dealerTurn(deck, dealer)) // if dealer busted
        return GameResult::playerWon;

    if (player.score() == dealer.score())
        return GameResult::tie;

    return (player.score() > dealer.score() ? GameResult::playerWon : GameResult::dealerWon);
}

int main()
{
    switch (playBlackjack())
    {
    case GameResult::playerWon:
        std::cout << "You win!\n";
        return 0;
    case GameResult::dealerWon:
        std::cout << "You lose!\n";
        return 0;
    case GameResult::tie:
        std::cout << "It's a tie.\n";
        return 0;
    }

    return 0;
}
posted @ 2026-01-14 00:13  游翔  阅读(21)  评论(0)    收藏  举报