16-x 第十六章总结与测验(todo)

鼓励的话

这一章内容颇具挑战性。我们涵盖了大量知识点,并揭示了C++的一些缺陷。恭喜你坚持学到了这里!

数组是释放C++程序强大功能的关键之一。

章节回顾

容器container是一种数据类型,用于存储一组未命名的对象(称为元素elements)。当需要处理相关联的值集合时,我们通常会使用容器。

容器中的元素数量常被称为长度length(有时也称计数count)。在C++中,“大小size”一词也常用于表示容器元素数量。大多数编程语言(包括C++)中的容器都是同构的homogenous,即容器元素必须具有相同类型。

容器库Containers library作为C++标准库的一部分,包含多种实现常见容器类型的类。实现容器功能的类有时被称为容器类container class

数组array是一种容器数据类型,用于连续contiguously存储值序列(即每个元素位于相邻内存位置,无间隔)。数组支持对任意元素的快速直接访问。

C++包含三种主要数组类型:(C风格)数组、std::vector容器类和std::array容器类。

std::vector 是 C++ 标准容器库中实现数组功能的容器类之一。它在 头文件中定义为类模板,通过模板类型参数指定元素类型。例如 std::vector 即声明元素类型为 int 的 std::vector。

容器通常具有名为列表构造函数list constructor的特殊构造函数,允许我们使用初始化列表构造容器实例。通过使用包含值的初始化列表进行列表初始化,即可构造包含这些元素值的容器。

在 C++ 中,访问数组元素最常见的方式是使用数组名配合下标运算符(operator[])。要选择特定元素,需在下标运算符的方括号内提供整数值以标识目标元素。该整数值称为下标subscript(或非正式地称为索引index)。第一个元素通过索引0访问,第二个通过索引1访问,依此类推。由于索引从0而非1开始,我们称C++数组为零基数组zero-based

operator[]运算符不执行任何边界检查bounds checking,即不会验证索引值是否在0到N-1(含)范围内。向operator[]运算符传递无效索引将导致未定义行为。

数组是少数支持随机访问random access的容器类型之一,这意味着无论容器中元素数量多少,每个元素都能以相同速度直接访问。

构造类类型对象时,匹配的列表构造函数优先于其他匹配构造函数。若需构造容器(或任何具有列表构造函数的类型),且初始化值非元素值时,请使用直接初始化。

std::vector v1 { 5 }; // defines a 1 element vector containing value `5`.
std::vector v2 ( 5 ); // defines a 5 element vector where elements are value-initialized.

std::vector 可以被声明为 const,但不能声明为 constexpr。

标准库中的每个容器类都定义了一个名为 size_type 的嵌套 typedef 成员(有时写作 T::size_type),该类型是容器长度(以及若支持索引则包括索引)所用类型的别名。size_type 几乎总是 std::size_t 的别名,但在极少数情况下可被重写为其他类型。我们可合理假设 size_type 是 std::size_t 的别名。

访问容器类的size_type成员时,必须使用容器类的完整模板名称进行作用域限定,例如std::vector::size_type。

可通过size()成员函数获取容器对象的长度,该函数返回无符号size_type类型的长度值。在C++17中,也可使用非成员函数std::size()。

在 C++20 中,非成员函数 std::ssize() 返回长度时采用大型有符号整数类型(通常为 std::ptrdiff_t,该类型常作为 std::size_t 的有符号对应类型使用)。

使用 at() 成员函数访问数组元素时会进行运行时边界检查(若超出边界则抛出 std::out_of_range 异常)。若异常未被捕获,应用程序将终止运行。

operator[] 和 at() 成员函数均支持非 const 索引操作,但两者均要求索引类型为 size_type(无符号整数类型)。当索引为 non-constexpr 时,这将引发符号转换问题。

std::vector 类型的对象可像其他对象一样传递给函数。这意味着若按值传递 std::vector,将产生耗费资源的复制操作。因此通常采用按(const)引用传递方式避免此类复制。

可使用函数模板实现将任意元素类型的 std::vector 传递至函数的功能。可使用 assert() 确保传入向量的长度正确。

复制语义copy semantics指决定对象复制方式的规则。当提及调用复制语义时,意味着我们执行了会复制对象的操作。

当数据所有权从一个对象转移到另一个对象时,我们称数据已被移动moved

移动语义Move semantics则指决定数据如何从一个对象转移到另一个对象的规则。当调用移动语义时,所有可移动的数据成员将被移动,而不可移动的数据成员则被复制。通过移动而非复制数据,移动语义通常比复制语义更高效,尤其当昂贵的复制操作能被低成本的移动操作替代时。

通常,当对象使用同类型对象进行初始化或赋值时,会采用复制语义(假设复制操作未被省略)。但当目标对象类型支持移动语义,且初始化表达式或赋值来源对象为右值时,系统将自动启用移动语义替代复制操作。

我们可以按值返回可移动类型(如 std::vector 和 std::string)。这类类型会以低成本移动其值,而非进行高成本的复制。

按特定顺序访问容器中每个元素的过程称为遍历traversing容器。遍历有时也被称为迭代iterating through容器。

循环常用于遍历数组,此时循环变量充当索引。需警惕偏移量错误——即循环体执行次数过多或过少的情况。

基于范围的 for 循环range-based for loop(有时也称为 for-each 循环)可实现无需显式索引的容器遍历。遍历容器时,应优先选择基于范围的 for 循环而非常规 for 循环。

在基于范围的 for 循环中使用类型推断(auto),让编译器推断数组元素类型。当需要通过 (const) 引用传递元素类型时,元素声明应使用 (const) 引用。除非需要操作副本,否则建议始终使用 const auto&。这可确保即使后续修改元素类型,也不会生成副本。

无作用域枚举可作为索引使用,并能提供索引含义的相关信息。

当需要表示数组长度的枚举项时,添加额外的“count”枚举项非常有用。可通过断言或static_assert确保数组长度等于count枚举项,从而保证数组初始化时具有预期数量的初始化项。

在实例化时必须定义长度且不可更改的数组称为固定大小数组fixed-size arrays 固定长度数组fixed-size arrays动态数组dynamic array(也称可调整大小数组resizable array)是指实例化后仍可更改大小的数组。这种可调整大小的能力正是 std::vector 的独特之处。

std::vector 实例化后可通过调用 resize() 成员函数并传入新长度来调整大小。

在 std::vector 中,容量capacity指已分配存储空间的元素数量,长度length指当前实际使用的元素数量。可通过 capacity() 成员函数获取容量。

当 std::vector 改变其管理的存储量时,该过程称为重新分配reallocation。由于重新分配通常需要复制数组中的每个元素,因此成本较高。因此我们应尽可能避免重新分配。

下标运算符(operator[])和at()成员函数的有效索引基于向量的长度,而非容量。

std::vector提供名为shrink_to_fit()的成员函数,用于请求向量缩减容量以匹配其长度。此请求不具有强制性。

栈中元素的添加与移除遵循后进先出(LIFOlast-in, first-out)原则。最后添加到栈中的板材将最先被移除。在编程中,stack是一种容器数据类型,其元素的插入与移除均按后进先出方式进行。这通常通过名为pushpop的两个操作实现。

std::vector的成员函数push_back()和emplace_back()会增加向量长度,若容量不足则触发内存重新分配。当插入操作引发重新分配时,std::vector通常会预留额外容量,以便后续添加元素时避免再次触发分配。

resize()成员函数同时修改向量长度与容量(若需调整)。
reserve()成员函数仅调整容量(若需调整)。

增加std::vector元素数量的方法:

  • 通过索引访问向量时使用resize()。此操作改变向量长度,确保索引有效性。
  • 当通过栈操作访问向量时,使用 reserve()。该操作仅增加容量而不改变向量长度。

push_back() 和 emplace_back() 均将元素压入栈中。若待压入对象已存在,两者效果等同。但在为向量压入临时对象时,emplace_back() 效率更高。当需创建临时对象添加至容器或明确调用构造函数时,优先选用 emplace_back();其他情况则推荐 push_back()。

std::vector 采用特殊实现,通过将 8 个布尔值压缩至 1 个字节,可显著提升布尔值存储效率。

std::vector 并非传统向量(其内存无需连续),也不存储布尔值(实际存储位集合),且不符合 C++ 对容器的定义。尽管 std::vector 在多数情况下表现得像向量,但它与标准库其余部分并不完全兼容。适用于其他元素类型的代码可能无法在 std::vector 上运行。因此,通常应避免使用 std::vector


测验时间

问题 #1

为下列内容编写定义。尽可能使用 CTAD(13.14 -- 类模板参数推导(CTAD)与推导指南)。

a) 用前六个偶数初始化的 std::vector。

显示答案

std::vector evens { 2, 4, 6, 8, 10, 12 };

b) 初始化值为 1.2, 3.4, 5.6, 7.8 的常量 std::vector。

image

显示答案

const std::vector d { 1.2, 3.4, 5.6, 7.8 }; // reminder: std::vector can't be constexpr

c) 初始化为 “Alex”, “Brad”, ‘Charles’, “Dave” 的常量 std::vector_of_std::string_view。

显示解决方案

using namespace std::literals::string_view_literals; // for sv suffix
const std::vector names { "Alex"sv, "Brad"sv, "Charles"sv, "Dave"sv }; // sv suffix needed for CTAD to infer std::string_view

d) 包含单个元素值 12 的 std::vector。

显示解决方案

std::vector v { 12 };

e) 包含 12 个 int 元素的 std::vector,初始化为默认值。

显示提示

提示:请考虑CTAD在此情况下是否适用。

显示解决方案

std::vector<int> v( 12 );

初始化具有初始长度的 std::vector 时,必须使用直接初始化。同时必须显式指定类型模板参数,因为没有初始化器可用于推断元素类型。


问题 #2

假设你正在开发一款游戏,玩家可携带三类物品:生命药水(health potions)、火把(torches)和箭矢(arrows)。


步骤 #1

在命名空间中定义一个无作用域枚举,用于标识不同类型的物品。定义一个std::vector来存储玩家携带的每种物品数量。玩家初始应携带1瓶生命药水、5支火把和10支箭。通过断言确保数组初始化数量正确。

提示:定义一个计数枚举器并在断言中使用它。

程序应输出以下内容:

You have 16 total items

显示解决方案

#include <cassert>
#include <iostream>
#include <vector>

namespace Items
{
    enum Type
    {
        health_potion,
        torch,
        arrow,
        max_items
    };
}

// Inventory items should have integral quantities, so we don't need a function template here
int countTotalItems(const std::vector<int>& inventory)
{
    int sum { 0 };
    for (auto e: inventory)
        sum += e;
    return sum;
}

int main()
{
    std::vector inventory { 1, 5, 10 };
    assert(std::size(inventory) == Items::max_items); // make sure our inventory has the correct number of initializers

    std::cout << "You have " << countTotalItems(inventory) << " total items\n";

    return 0;
}

步骤 #2

修改上一步的程序,使其现在输出:

You have 1 health potion
You have 5 torches
You have 10 arrows
You have 16 total items

使用循环打印每个库存项的数量及其名称。确保名称的复数形式正确。

显示解决方案

#include <cassert>
#include <iostream>
#include <string_view>
#include <type_traits> // for std::is_integral and std::is_enum
#include <vector>

namespace Items
{
    enum Type: int
    {
        health_potion,
        torch,
        arrow,
        max_items
    };
}

std::string_view getItemNamePlural(Items::Type type)
{
    switch (type)
    {
        case Items::health_potion:  return "health potions";
        case Items::torch:          return "torches";
        case Items::arrow:          return "arrows";

        default:                    return "???";
    }
}

std::string_view getItemNameSingular(Items::Type type)
{
    switch (type)
    {
        case Items::health_potion:  return "health potion";
        case Items::torch:          return "torch";
        case Items::arrow:          return "arrow";

        default:                    return "???";
    }
}

// Helper function to convert `value` into an object of type std::size_t
// UZ is the suffix for literals of type std::size_t.
template <typename T>
constexpr std::size_t toUZ(T value)
{
    // make sure T is an integral type
    static_assert(std::is_integral<T>() || std::is_enum<T>());

    return static_cast<std::size_t>(value);
}


void printInventoryItem(const std::vector<int>& inventory, Items::Type type)
{
    bool plural { inventory[toUZ(type)] != 1 };
    std::cout << "You have " << inventory[toUZ(type)] << ' ';
    std::cout << (plural ? getItemNamePlural(type) : getItemNameSingular(type)) << '\n';
}

// Inventory items should have integral quantities, so we don't need a function template here
int countTotalItems(const std::vector<int>& inventory)
{
    int sum { 0 };
    for (auto e: inventory)
        sum += e;
    return sum;
}

int main()
{
    std::vector inventory { 1, 5, 10 };
    assert(std::size(inventory) == Items::max_items); // make sure our inventory has the correct number of initializers

    // Since we can't iterate over an enumerated type using a ranged-for, we'll need to use a traditional for-loop here
    for (int i=0; i < Items::max_items; ++i)
    {
        auto item { static_cast<Items::Type>(i) };
        printInventoryItem(inventory, item);
    }

    std::cout << "You have " << countTotalItems(inventory) << " total items\n";

    return 0;
}

问题 #3

编写一个函数,该函数接受一个 std::vector,返回一个 std::pair,其中包含数组中最小值和最大值元素的索引。std::pair 的文档可在此处查阅。请对以下两个向量调用该函数:

std::vector v1 { 3, 8, 2, 5, 7, 8, 3 };
std::vector v2 { 5.5, 2.7, 3.3, 7.6, 1.2, 8.8, 6.6 };

该程序应输出以下内容:

With array ( 3, 8, 2, 5, 7, 8, 3 ):
The min element has index 2 and value 2
The max element has index 1 and value 8

With array ( 5.5, 2.7, 3.3, 7.6, 1.2, 8.8, 6.6 ):
The min element has index 4 and value 1.2
The max element has index 5 and value 8.8

显示答案

#include <iostream>
#include <vector>

template <typename T>
std::pair<std::size_t, std::size_t> findMinMaxIndices(const std::vector<T>& v)
{
    // Assume element 0 is the minimum and the maximum
    std::size_t minIndex { 0 };
    std::size_t maxIndex { 0 };

    // Look through the remaining elements to see if we can find a smaller or larger element
    for (std::size_t index { 1 }; index < v.size(); ++index)
    {
        if (v[index] < v[minIndex])
            minIndex = index;
        if (v[index] > v[maxIndex])
            maxIndex = index;
    }

    return { minIndex, maxIndex };
}

template <typename T>
void printArray(const std::vector<T>& v)
{
    bool comma { false };
    std::cout << "With array ( ";
    for (const auto& e: v)
    {
        if (comma)
            std::cout << ", ";

        std::cout << e;
        comma = true;
    }
    std::cout << " ):\n";
}

int main()
{
    std::vector v1 { 3, 8, 2, 5, 7, 8, 3 };
    printArray(v1);

    auto m1 { findMinMaxIndices(v1) };
    std::cout << "The min element has index " << m1.first << " and value " << v1[m1.first] << '\n';
    std::cout << "The max element has index " << m1.second << " and value " << v1[m1.second] << '\n';

    std::cout << '\n';

    std::vector v2 { 5.5, 2.7, 3.3, 7.6, 1.2, 8.8, 6.6 };
    printArray(v2);

    auto m2 { findMinMaxIndices(v2) };
    std::cout << "The min element has index " << m2.first << " and value " << v2[m2.first] << '\n';
    std::cout << "The max element has index " << m2.second << " and value " << v2[m2.second] << '\n';

    return 0;
}

问题 #4

修改原程序,使用户能够输入任意数量的整数。当用户输入 -1 时停止接受输入。

打印向量并找出最小和最大元素。

当输入 3 8 5 2 3 7 -1 时,程序应输出如下结果:

Enter numbers to add (use -1 to stop): 3 8 5 2 3 7 -1
With array ( 3, 8, 5, 2, 3, 7 ):
The min element has index 3 and value 2
The max element has index 1 and value 8

当用户首次输入-1时,请采取合理处理措施。

显示解决方案

#include <iostream>
#include <limits>
#include <vector>

template <typename T>
std::pair<std::size_t, std::size_t> findMinMaxIndices(const std::vector<T>& v)
{
    // Assume element 0 is the minimum and the maximum
    std::size_t minIndex { 0 };
    std::size_t maxIndex { 0 };

    // Look through the remaining elements to see if we can find a smaller or larger element
    for (std::size_t index { 1 }; index < v.size(); ++index)
    {
        if (v[index] < v[minIndex])
            minIndex = index;
        if (v[index] > v[maxIndex])
            maxIndex = index;
    }

    return { minIndex, maxIndex };
}

template <typename T>
void printArray(const std::vector<T>& v)
{
    bool comma { false };
    std::cout << "With array ( ";
    for (const auto& e: v)
    {
        if (comma)
            std::cout << ", ";

        std::cout << e;
        comma = true;
    }
    std::cout << " ):\n";
}

int main()
{
    std::vector<int> v1 { };
    std::cout << "Enter numbers to add (use -1 to stop): ";

    while (true)
    {
        int input{};
        std::cin >> input;
        if (input == -1)
            break;

        if (!std::cin) // if the previous extraction failed
        {
            std::cin.clear(); // put us back in 'normal' operation mode
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // and remove the bad input
            continue;
        }

        v1.push_back(input);
    }

    // If the array is empty
    if (v1.size() == 0)
    {
        std::cout << "The array has no elements\n";
    }
    else
    {
        printArray(v1);

        auto m1 { findMinMaxIndices(v1) };
        std::cout << "The min element has index " << m1.first << " and value " << v1[m1.first] << '\n';
        std::cout << "The max element has index " << m1.second << " and value " << v1[m1.second] << '\n';
    }

    return 0;
}

问题 #5

让我们实现游戏 C++man(这将是我们版本的经典儿童猜字游戏 Hangman)。

若您从未玩过此游戏,以下是简化规则:

高阶规则:

  • 计算机将随机选取一个单词,并为该单词中的每个字母绘制一条下划线。
  • 玩家在猜错 X 次之前猜出单词所有字母即获胜 (X值可配置)。

每轮操作:

  • 玩家需猜测单个字母。
  • 若该字母已被猜中,则不计数,游戏继续。
  • 若任意下划线对应该字母,则该字母替换所有对应下划线,游戏继续。
  • 若无下划线对应该字母,则消耗一次错误猜测机会。

状态显示:

  • 玩家应知晓剩余错误猜测次数。
  • 玩家应知晓所有猜错的字母(按字母顺序排列)。

鉴于这是C++man,我们将用+符号表示剩余错误猜测次数。当+符号耗尽时即游戏失败。

以下是完成版游戏的示例输出:

Welcome to C++man (a variant of Hangman)
To win: guess the word.  To lose: run out of pluses.

The word: ________   Wrong guesses: ++++++
Enter your next letter: a
No, 'a' is not in the word!

The word: ________   Wrong guesses: +++++a
Enter your next letter: b
Yes, 'b' is in the word!

The word: b_______   Wrong guesses: +++++a
Enter your next letter: c
Yes, 'c' is in the word!

The word: b__cc___   Wrong guesses: +++++a
Enter your next letter: d
No, 'd' is not in the word!

The word: b__cc___   Wrong guesses: ++++ad
Enter your next letter: %
That wasn't a valid input.  Try again.

The word: b__cc___   Wrong guesses: ++++ad
Enter your next letter: d
You already guessed that.  Try again.

The word: b__cc___   Wrong guesses: ++++ad
Enter your next letter: e
No, 'e' is not in the word!

The word: b__cc___   Wrong guesses: +++ade
Enter your next letter: f
No, 'f' is not in the word!

The word: b__cc___   Wrong guesses: ++adef
Enter your next letter: g
No, 'g' is not in the word!

The word: b__cc___   Wrong guesses: +adefg
Enter your next letter: h
No, 'h' is not in the word!

The word: b__cc___   Wrong guesses: adefgh
You lost!  The word was: broccoli

步骤 #1

目标:

任务:

  • 首先定义名为 WordList 的命名空间。初始词表包含:“mystery”、“broccoli”、“account”、“almost”、“spaghetti”、“opinion”、“beautiful”、‘distance’、“luggage”。可根据需要添加其他词汇。
  • 编写函数随机选取单词并显示结果。多次运行程序以验证选词的随机性。

此步骤的示例输出如下:

Welcome to C++man (a variant of Hangman)
To win: guess the word.  To lose: run out of pluses.

The word is: broccoli

显示答案

#include <iostream>
#include <vector>
#include "Random.h"

namespace WordList
{
    // Define your list of words here
    std::vector<std::string_view> words { "mystery", "broccoli" , "account", "almost", "spaghetti", "opinion", "beautiful", "distance", "luggage" };

    std::string_view getRandomWord()
    {
        return words[Random::get<std::size_t>(0, words.size()-1)];
    }
}

int main()
{
    std::cout << "Welcome to C++man (a variant of Hangman)\n";
    std::cout << "To win: guess the word.  To lose: run out of pluses.\n";

    std::cout << "The word is: " << WordList::getRandomWord();

    return 0;
}

步骤 #2

在开发复杂程序时,我们需要循序渐进地工作:每次只添加一两项功能,并确保其正常运行。接下来该添加什么功能呢?

目标:

  • 能够绘制游戏的基本状态,用下划线显示单词。
  • 接受用户输入的字母,并进行基础错误验证。

本步骤暂不记录用户已输入的字母。

此步骤的示例输出如下:

Welcome to C++man (a variant of Hangman)
To win: guess the word.  To lose: run out of pluses.

The word: ________
Enter your next letter: %
That wasn't a valid input.  Try again.
Enter your next letter: a
You entered: a

任务:

  • 创建名为 Session 的类,用于存储游戏会话中需要管理的全部数据。当前阶段只需获取随机生成的单词。
  • 创建函数以显示游戏基本状态,其中单词以下划线形式呈现。
  • 创建函数接受用户输入的字母。进行基础输入验证,过滤非字母或无效输入。

显示解决方案

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

namespace WordList
{
    // Define your list of words here
    std::vector<std::string_view> words { "mystery", "broccoli" , "account", "almost", "spaghetti", "opinion", "beautiful", "distance", "luggage" };

    std::string_view getRandomWord()
    {
        return words[Random::get<std::size_t>(0, words.size()-1)];
    }
}

class Session
{
private:
    // Game session data
    std::string_view m_word { WordList::getRandomWord() };

public:
    std::string_view getWord() const { return m_word; }
};

void draw(const Session& s)
{
    std::cout << '\n';

    std::cout << "The word: ";
    for ([[maybe_unused]] auto c: s.getWord()) // step through each letter of word
    {
        std::cout << '_';
    }

    std::cout << '\n';
}

char getGuess()
{
    while (true)
    {
        std::cout << "Enter your next letter: ";

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

        // If user did something bad, try again
        if (!std::cin)
        {
            // Fix it
            std::cin.clear();
            std::cout << "That wasn't a valid input.  Try again.\n";
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            continue;
        }

        // Clear out any extraneous input
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

        // If the user entered an invalid char, try again
        if (c < 'a' || c > 'z')
        {
            std::cout << "That wasn't a valid input.  Try again.\n";
            continue;
        }

        return c;
    }
}

int main()
{
    std::cout << "Welcome to C++man (a variant of Hangman)\n";
    std::cout << "To win: guess the word.  To lose: run out of pluses.\n";

    Session s{};

    draw(s);
    char c { getGuess() };
    std::cout << "You guessed: " << c << '\n';

    return 0;
}

步骤 #3

既然我们已经能够显示游戏状态并获取用户输入,接下来就将用户输入整合到游戏中。

目标:

  • 记录用户已猜过的字母。
  • 显示正确猜中的字母。
  • 实现基础游戏循环。

任务:

  • 更新 Session 类以追踪当前已猜过的字母。
  • 修改游戏状态函数,同时显示下划线和正确猜中的字母。
  • 更新输入程序,拒绝已猜过的字母。
  • 编写循环,执行6次后退出(以便测试上述功能)。

本步骤中,我们不会告知用户所猜字母是否存在于单词中(但会在游戏状态显示中呈现该信息)。

此步骤的关键在于如何存储用户猜测过的字母信息。存在多种可行的实现方式。提示:字母数量固定,且该操作将频繁执行。

显示提示

提示:为每个字母使用布尔值比维护字母列表并搜索该列表来确定字母是否存在更简单快捷。

显示提示

提示:这里使用 std::vector<bool> 是可行的,因为我们不会使用标准库的其他任何功能。

显示提示

提示:可通过 (字母 % 32)-1 将字母转换为数组索引。此方法适用于大小写字母。

此步骤的示例输出如下:

Welcome to C++man (a variant of Hangman)
To win: guess the word.  To lose: run out of pluses.

The word: ________
Enter your next letter: a

The word: ____a___
Enter your next letter: a
You already guessed that.  Try again.
Enter your next letter: b

The word: ____a___
Enter your next letter: c

The word: ____a___
Enter your next letter: d

The word: d___a___
Enter your next letter: e

The word: d___a__e
Enter your next letter: f

The word: d___a__e
Enter your next letter: g

显示解决方案

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

namespace WordList
{
    // Define your list of words here
    std::vector<std::string_view> words { "mystery", "broccoli" , "account", "almost", "spaghetti", "opinion", "beautiful", "distance", "luggage" };

    std::string_view getRandomWord()
    {
        return words[Random::get<std::size_t>(0, words.size()-1)];
    }
}

class Session
{
private:
    // Game session data
    std::string_view m_word { WordList::getRandomWord() };
    std::vector<bool> m_letterGuessed { std::vector<bool>(26) };

    std::size_t toIndex(char c) const { return static_cast<std::size_t>((c % 32)-1); }

public:
    std::string_view getWord() const { return m_word; }

    bool isLetterGuessed(char c) const { return m_letterGuessed[toIndex(c)]; }
    void setLetterGuessed(char c) { m_letterGuessed[toIndex(c)] = true; }
};

void draw(const Session& s)
{
    std::cout << '\n';

    std::cout << "The word: ";
    for (auto c: s.getWord()) // step through each letter of word
    {
        if (s.isLetterGuessed(c))
            std::cout << c;
        else
            std::cout << '_';
    }

    std::cout << '\n';
}

char getGuess(const Session& s)
{
    while (true)
    {
        std::cout << "Enter your next letter: ";

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

        // If user did something bad, try again
        if (!std::cin)
        {
            // Fix it
            std::cin.clear();
            std::cout << "That wasn't a valid input.  Try again.\n";
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            continue;
        }

        // Clear out any extraneous input
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

        // If the user entered an invalid char, try again
        if (c < 'a' || c > 'z')
        {
            std::cout << "That wasn't a valid input.  Try again.\n";
            continue;
        }

        // If the letter was already guessed, try again
        if (s.isLetterGuessed(c))
        {
            std::cout << "You already guessed that.  Try again.\n";
            continue;
        }

        // If we got here, this must be a valid guess
        return c;
    }
}

int main()
{
    std::cout << "Welcome to C++man (a variant of Hangman)\n";
    std::cout << "To win: guess the word.  To lose: run out of pluses.\n";

    Session s {};

    int count { 6 };
    while (--count)
    {
        draw(s);
        char c { getGuess(s) };
        s.setLetterGuessed(c);
    }

    // Draw the final state of the game
    draw(s);

    return 0;
}

第4步

目标:完成游戏。

任务:

  • 添加剩余错误猜测次数的显示
  • 添加错误字母猜测的显示
  • 添加胜负条件及胜负提示文本。

显示解决方案

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

namespace Settings
{
    constexpr int wrongGuessesAllowed { 6 };
}

namespace WordList
{
    // Define your list of words here
    std::vector<std::string_view> words { "mystery", "broccoli" , "account", "almost", "spaghetti", "opinion", "beautiful", "distance", "luggage" };

    std::string_view getRandomWord()
    {
        return words[Random::get<std::size_t>(0, words.size()-1)];
    }
}

class Session
{
private:
    // Game session data
    std::string_view m_word { WordList::getRandomWord() };
    int m_wrongGuessesLeft { Settings::wrongGuessesAllowed };
    std::vector<bool> m_letterGuessed { std::vector<bool>(26) };

    std::size_t toIndex(char c) const { return static_cast<std::size_t>((c % 32)-1); }

public:
    std::string_view getWord() const { return m_word; }

    int wrongGuessesLeft() const { return m_wrongGuessesLeft; }
    void removeGuess() { --m_wrongGuessesLeft; }

    bool isLetterGuessed(char c) const { return m_letterGuessed[toIndex(c)]; }
    void setLetterGuessed(char c) { m_letterGuessed[toIndex(c)] = true; }

    bool isLetterInWord(char c) const
    {
        for (auto ch: m_word) // step through each letter of word
        {
            if (ch == c)
                return true;
        }

        return false;
    }

    bool won()
    {
        for (auto c: m_word) // step through each letter of word
        {
            if (!isLetterGuessed(c))
                return false;
        }

        return true;
    }
};

void draw(const Session& s)
{
    std::cout << '\n';

    std::cout << "The word: ";
    for (auto c: s.getWord()) // step through each letter of word
    {
        if (s.isLetterGuessed(c))
            std::cout << c;
        else
            std::cout << '_';
    }

    std::cout << "   Wrong guesses: ";
    for (int i=0; i < s.wrongGuessesLeft(); ++i)
        std::cout << '+';


    for (char c='a'; c <= 'z'; ++c)
        if (s.isLetterGuessed(c) && !s.isLetterInWord(c))
            std::cout << c;

    std::cout << '\n';
}

char getGuess(const Session& s)
{
    while (true)
    {
        std::cout << "Enter your next letter: ";

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

        // If user did something bad, try again
        if (!std::cin)
        {
            // Fix it
            std::cin.clear();
            std::cout << "That wasn't a valid input.  Try again.\n";
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            continue;
        }

        // Clear out any extraneous input
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

        // If the user entered an invalid char, try again
        if (c < 'a' || c > 'z')
        {
            std::cout << "That wasn't a valid input.  Try again.\n";
            continue;
        }

        // If the letter was already guessed, try again
        if (s.isLetterGuessed(c))
        {
            std::cout << "You already guessed that.  Try again.\n";
            continue;
        }

        // If we got here, this must be a valid guess
        return c;
    }
}

void handleGuess(Session &s, char c)
{
    s.setLetterGuessed(c);

    if (s.isLetterInWord(c))
    {
        std::cout << "Yes, '" << c << "' is in the word!\n";
        return;
    }

    std::cout << "No, '" << c << "' is not in the word!\n";
    s.removeGuess();
}

int main()
{
    std::cout << "Welcome to C++man (a variant of Hangman)\n";
    std::cout << "To win: guess the word.  To lose: run out of pluses.\n";

    Session s{};

    while (s.wrongGuessesLeft() && !s.won())
    {
        draw(s);
        char c { getGuess(s) };
        handleGuess(s, c);
    }

    // Draw the final state of the game
    draw(s);

    if (!s.wrongGuessesLeft())
        std::cout << "You lost!  The word was: " << s.getWord() << '\n';
    else
        std::cout << "You won!\n";

    return 0;
}
posted @ 2026-01-08 19:15  游翔  阅读(6)  评论(0)    收藏  举报