探索 C++20(一)

原文:Exploring C++20

协议:CC BY-NC-SA 4.0

一、打磨你的工具

在开始探索 C++ 环境之前,您必须收集一些基本的工具:文本编辑器、C++ 编译器、链接器和调试器。您可以单独或捆绑购买这些工具,可能作为集成开发环境(IDE)的一揽子交易。无论您的平台、操作系统和预算如何,都有很多选择。

如果你正在上课,老师会提供工具或指示使用哪些工具。如果你在一个已经使用 C++ 的组织中工作,你可能想使用它的工具,这样你就可以熟悉它们和它们的正确用法。如果你必须获得自己的工具,检查这本书的网站, https://cpphelp.com/exploring/ 。工具版本和质量变化太快,无法以印刷形式提供详细信息,因此您可以在网站上找到最新的建议。以下部分给出了一些一般性建议。

C++ 版本

这本书涵盖了 C++ 20,这是标准化委员会在 2020 年批准的 C++ 标准的重大更新。C++ 20 引入了几个主要特性,所有编译器实现这些特性都需要时间。本书中的大多数代码清单只能用最新的 C++ 20 编译器编译,所以请确保您使用的是所有工具的最新版本。即使这样,您也可能无法编译所有的示例。事实上,你可能一个也编译不出来。

其中一个主要的特性,模块,影响着每一个程序。如果您的环境不完全支持这个特性,您可能无法编译任何代码清单。为了帮助你,这本书的网站提供了所有代码示例的转换副本,以避免使用模块,但所有其他 C++ 20 特性保持不变。

雷的建议

C++ 是世界上使用最广泛的编程语言之一(取决于你如何衡量“广泛使用”)。因此,C++ 工具大量存在于许多硬件和软件环境中,价格也各不相同。

您可以选择命令行工具,这在 UNIX 和类 UNIX 环境中特别流行,或者您可以选择 IDE,它将所有工具捆绑到一个单一的图形用户界面(GUI)中。选择你觉得最舒服的风格。你的程序不会关心你用什么工具来编辑、编译和链接它们。

Clang 和 LLVM

Clang 是一个 C++ 编译器(以及其他语言),它在幕后使用 LLVM 来编译和优化程序。(不,LLVM 不代表任何东西。)macOS 使用 clang 作为默认编译器,很多 Linux 开发者也喜欢使用 clang。你甚至可以为微软 Windows 下载 clang 和 LLVM。

一些 Linux 发行版已经包含了 clang 和 LLVM。对于其他发行版,通常可以从发行版的中央存储库或者直接从 LLVM 网站下载。链接见 cpphelp.com/exploring

GNU 编译器集合

最广泛使用的 C++ 编译器是 GNU 编译器集合(GCC)的一部分。GNU C++ 编译器通常被称为 g++。它通常是 Linux 发行版的默认 C++ 编译器,也可用于 macOS 和 Microsoft Windows。

微软视窗软件

大多数使用 Microsoft Windows 的 C++ 开发人员使用微软自己的编译器,这些编译器包含在他们的 Visual Studio 产品中,可以免费下载。Visual Studio 在一个保护伞下积累了许多工具,可能相当复杂,所以一定要下载 C++ 编译器,并且只在标准 C++ 模式下使用,而不是 C++/CLI,这是一种不同的语言。

在 Microsoft Windows 上使用 clang 时,还需要 GnuWin32 用于一些相关的实用程序。Cygwin 和 MinGW 项目包括 GCC。

其他工具

微软提供了 Visual Studio 代码,这是一个运行在所有流行平台上的 IDE。它可以与您平台上的首选编译器集成。其他流行的 ide 包括 Eclipse 和 NetBeans。

C++ 需要编译器和标准库。大多数 C++ 产品都包括这两种库,但有时,利基编译器希望您使用不同产品的库。例如,你可以为他们的硬件下载英特尔的编译器。编译器的优化器是一流的,但是您还需要一个库,比如 g++ 附带的 libstdc++。

作者的网站(cpphelp.com/exploring)有安装和使用这些工具的有用提示链接。

书中的大多数代码清单和代码片段都有相关的测试。您需要 Python 3 来运行测试。代码中包含了CMakeLists.txt文件,因此您可以使用 cmake 构建和测试每个代码样本,cmake 是一个用于构建软件的跨平台工具。

阅读文档

现在您已经有了工具,请花些时间阅读产品文档——尤其是入门部分。真的,我是认真的。查找教程和其他快速介绍,帮助您快速掌握工具。如果您正在使用 IDE,您尤其需要知道如何创建简单的命令行项目。

ide 通常要求您在实际编写 C++ 程序之前,创建一个项目、工作区或其他一些信封或包装。你一定知道怎么做,我帮不了你,因为每个 IDE 都不一样。如果您可以选择项目模板,请选择“控制台”、“命令行”、“终端”、“C++ 工具”或一些具有类似名称的项目。

阅读编译器和其他工具的文档花了你多长时间?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

时间是太多了,还是太少了,还是刚刚好?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

C++ 语言遵循国际标准。每个编译器(或多或少)都遵循那个标准,但也加入了一些非标准的额外内容。对于某些项目来说,这些额外的东西可能是有用的,甚至是必要的,但是对于本书来说,你必须确保你只使用标准的 C++。大多数编译器可以关闭它们的扩展。即使您以前没有阅读过该文档,现在也要阅读,以了解您需要哪些选项来使您能够编译标准 C++ 和只编译标准 C++。

记下选项,以备将来参考。




您可能错过了一些选项;它们可能很模糊。为了帮助您,表 1-1 列出了 Microsoft Visual C++、g++ 和 clang 所需的命令行编译器选项。这本书的网站为其他一些流行的编译器提供了建议。如果您使用的是 IDE,请查看项目选项或属性以找到等效项。

表 1-1。

标准 C++ 的编译器选项

|

编译程序

|

选择

|
| — | — |
| Visual Studio 命令行 | /EHsc /Za |
| 成开发环境 | 启用 C++ 异常,禁用语言扩展 |
| g++ | -pedantic -std=c++20 |
| clang/llvm | -pedantic -std=c++20 |

你的第一个程序

现在你有了工具,是时候开始了。启动您最喜欢的文本编辑器或 C++ IDE,开始您的第一个项目或创建一个新文件。将这个文件命名为list0101.cpp,是列表 1-1 的简称。几种不同的文件扩展名在 C++ 程序中很流行。我喜欢用.cpp,这里的 p 表示“加”。其他常见的扩展名有.cxx.cc。有些编译器会将.C(大写 C )识别为 C++ 文件扩展名,但我不建议使用它,因为它太容易与 C 程序的默认扩展名.c(小写 c )混淆。许多桌面环境不区分大小写文件名,这进一步加剧了问题。挑选你最喜欢的,坚持下去。键入清单 1-1 中包含的文本。(除了一个例外,你可以从本书的网站下载所有代码清单。清单 1-1 是个例外。我希望你习惯于在你的文本编辑器中输入 C++ 代码。)

/// This program examines features of the C++ library
/// to deduce and print the C++ version.

#include <algorithm>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <ostream>
#include <string>
#include <vector>

template<std::size_t N>
struct array
{
    char array[N];
    enum { size = N };
};

template<int I>
struct value_of
{};

template<>
struct value_of<1>
{
    enum { value = true };
};

template<>
struct value_of<2>
{
    enum { value = false };
};

void* erase(...);

struct is_cpp20
{
    static array<1> deduce_type(std::vector<int>::size_type);
    static array<2> deduce_type(...);
    static std::vector<int> v;
    static int i;
    enum { value = value_of<sizeof(deduce_type(erase(v, i)))>::value };
};

struct is_cpp17
{
    static array<1> deduce_type(char*);
    static array<2> deduce_type(const char*);
    static std::string s;
    enum { value = value_of<sizeof(deduce_type(s.data()))>::value };
};

int cbegin(...);

struct is_cpp14
{
    static array<1> deduce_type(std::string::const_iterator);
    static array<2> deduce_type(int);
    enum { value = value_of<sizeof(deduce_type(cbegin(std::string())))>::value };
};

int move(...);

struct is_cpp11
{
    template<class T>
    static array<1> deduce_type(T);
    static array<2> deduce_type(int);
    static std::string s;
    enum { value = value_of<sizeof(deduce_type(move(s)))>::value };
};

enum { cpp_year =
        is_cpp20::value ? 2020 :
        is_cpp17::value ? 2017 :
        is_cpp14::value ? 2014 :
        is_cpp11::value ? 2011 :
        2003
    };

int main()
{
    std::cout << "C++ " << std::setfill('0') << std::setw(2) << cpp_year%100 << '\n';
    std::cout << "C++ " << std::setw(2) << (__cplusplus / 100) % 100 << '\n';
}

Listing 1-1.Your First C++ Program

毫无疑问,这些代码的一部分或全部对你来说是胡言乱语。没关系。这个练习的目的不是理解 C++,而是确保你能正确地使用你的工具。我可以从一个简单的“Hello,world”类型的程序开始,但这只是语言和库的一小部分。这个程序寻找在不同版本的 C++ 标准中引入的标准库的特性,以确定你使用的是哪个版本。

现在回去仔细检查你的源代码。确保你输入的一切都是正确的。

你真的仔细检查过这个程序了吗?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

你有没有发现任何需要改正的错别字?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

犯错是人之常情,排印错误并不可耻。我们都会犯错。回去重新检查你的程序。

现在编译你的程序。如果您使用的是 IDE,请找到“编译”或“构建”按钮或菜单项。如果你使用命令行工具,一定要链接程序。出于历史(或歇斯底里)的原因,UNIX 工具如 g++ 通常会产生一个名为a.out的可执行程序。您应该将其重命名为更有用的名称,或者使用-o选项来命名输出文件。表 1-2 显示了用于 Visual C++、g++ 和 clang 的示例命令行。

表 1-2。

编译器 list0101.cpp 的示例命令行

|

编译程序

|

命令行

|
| — | — |
| Visual C++ | cl /EHsc /Za list0101.cpp |
| g++ | g++ -o list0101 -pedantic -std=c++20 list0101.cpp |
| Clang | clang++ -o list0101 -pedantic -std=c++20 list0101.cpp |

如果你从编译器那里收到任何错误,那就意味着你在输入源代码时犯了一个错误;编译器、链接器或 C++ 库安装不正确。或者编译器、链接器或库不符合 C++ 标准,因此不适合在本书中使用。再三检查你输入的文本是否正确。如果您确信错误出在工具上,而不是您,请检查发布日期。如果工具早于 2020 年,它们早于标准。因此,根据定义,它们不能符合标准。编译器供应商努力确保他们的工具符合最新标准,但这需要时间。在全球疫情中,我们可能要等很长时间才能看到真正实现足够有用的 C++ 20 标准的编译器。

如果其他方法都失败了,尝试不同的工具。下载 GCC 或 Visual Studio 的当前版本。你可能不得不为这本书使用这些工具,即使你必须为你的工作使用一些粗糙、生锈的旧工具。

成功的编译是一回事,成功的执行是另一回事。如何调用程序取决于操作系统。在 GUI 环境中,您需要一个控制台或终端窗口来输入命令行。您可能需要键入可执行文件的完整路径或仅键入程序名,这同样取决于您的操作系统。当您运行程序时,它从标准输入流中读取文本,这意味着无论您键入什么,程序都会读取。然后,你必须通知程序你已经完成了,通过按下魔法键来表示文件结束。在大多数类似 UNIX 的操作系统上,按 Ctrl+D。在 Windows 上,按 Ctrl+Z。

从 IDE 中运行控制台应用程序有时很棘手。如果您不小心,IDE 可能会在您有机会看到程序的任何输出之前就关闭程序的窗口。您必须确保窗口保持可见。有些 ide(如 Visual Studio 和 KDevelop)会自动为您完成这项工作,要求您在它关闭窗口之前按下最后一个回车键。

如果 IDE 没有自动保持窗口打开,并且您找不到任何选项或设置来保持窗口打开,您可以通过在程序的右大括号或调试器允许您设置断点的最近语句上设置断点来强制解决该问题。

你如何测试 list0101 以确保它正确运行?






好吧,动手吧。程序运行是否正确?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

该程序打印两行。第一个是程序从你的环境中推断出来的东西。第二个是编译器和库声称它们实现了什么。我希望他们是一样的。

好了,这很容易,不是吗?阅读编译器的文档,了解如何设置所需的 C++ 版本,并重新编译和运行程序。确保结果与您指定的版本相匹配。

在你完成这次探索之前,我还有一个练习。这一次,源文件更加复杂。这是一个专业特技程序员写的。不要试图阅读此程序,即使有成人监督。不要试图理解这个程序。最重要的是,不要模仿这个程序中使用的编程风格。这个练习不是给你的,是给你的工具的。它的目的是看你的编译器是否能正确编译这个程序,以及你的库实现是否有标准库的必要部分。对于编译器来说,这不是一个严峻的考验,但它确实触及了一些高级 C++ 特性。

所以不要费心去阅读代码。只需从该书的网站下载文件list0102.cpp并尝试用你的工具编译和链接它。(我把程序全文收录进来,只为缺乏便捷上网的读者。)如果你的编译器不能正确编译和运行清单 1-2 ,你必须替换它(你的编译器,而不是程序)。在早期的课程中,你可能勉强过关,但是到本书结束时,你将会编写一些相当复杂的程序,你需要一个能够胜任这项任务的编译器。

/// Sort the standard input alphabetically.
/// Read lines of text, sort them, and print the results to the standard output.
/// If the command line names a file, read from that file. Otherwise, read from
/// the standard input. The entire input is stored in memory, so don’t try
/// this with input files that exceed available RAM.
///
/// Comparison uses a locale named on the command line, or the default, unnamed
/// locale if no locale is named on the command line.

#include <cerrno>
#include <cstdlib>
import <algorithm>;
import <fstream>;
import <initializer_list>;
import <iostream>;
import <iterator>;
import <locale>;
import <string>;
import <system_error>;
import <vector>;

template<class C>
struct text : std::basic_string<C>
{
  using super = std::basic_string<C>;
  constexpr text() noexcept : super{} {}
  text(text&&) = default;
  text(text const&) = default;
  text& operator=(text const&) = default;
  text& operator=(text&&) = default;
  constexpr explicit operator bool() const noexcept {
    return not this->empty();
  }
};

/// Read lines of text from @p in to @p iter. Lines are appended to @p iter.
/// @param in the input stream
/// @param iter an output iterator
template<class Ch>
auto read(std::basic_istream<Ch>& in) -> std::vector<text<Ch>>
{
    std::vector<text<Ch>> result;

    text<Ch> line;
    while (std::getline(in, line))
        result.emplace_back(std::move(line));

    return result;
}

/// Main program.
int main(int argc, char* argv[])
try
{
    // Throw an exception if an unrecoverable input error occurs, e.g.,
    // disk failure.
    std::cin.exceptions(std::ios_base::badbit);

    // Part 1\. Read the entire input into text. If the command line names a file,
    // read that file. Otherwise, read the standard input.
    std::vector<text<char>> text; ///< Store the lines of text here
    if (argc < 2)
        text = read(std::cin);
    else
    {
        std::ifstream in{argv[1]};
        if (not in)
        {
            std::cout << argv[1] << ": " << std::system_category().message(errno) << '\n';
            return EXIT_FAILURE;
        }
        text = read(in);
    }

    // Part 2\. Sort the text. The second command line argument, if present,
    // names a locale, to control the sort order. Without a command line
    // argument, use the default locale (which is obtained from the OS).
    std::locale const& loc{ std::locale(argc >= 3 ? argv[2] : "") };
    std::collate<char> const& collate{ std::use_facet<std::collate<char>>(loc) };
    std::ranges::sort(text,
        &collate
        {
            return collate.compare(to_address(cbegin(a)), to_address(cend(a)),
                to_address(cbegin(b)), to_address(cend(b))) < 0;
        }
    );

    // Part 3\. Print the sorted text.
   for (auto const& line :  text)
      std::cout << line << '\n';
}
catch (std::exception& ex)
{
    std::cerr << "Caught exception: " << ex.what() << '\n';
    std::cerr << "Terminating program.\n";
    std::exit(EXIT_FAILURE);
}
catch (...)
{
    std::cerr << "Caught unknown exception type.\nTerminating program.\n";
    std::exit(EXIT_FAILURE);
}

Listing 1-2.Testing Your Compiler

我抓到你偷看。你不顾我的警告,试图读取源代码,是吗?请记住,我故意用复杂的方式编写这个程序来测试您的工具。当你读完这本书的时候,你将能够阅读和理解这个程序。更重要的是,你将能够写得更简单、更干净。然而,在你能跑之前,你必须学会走。一旦你习惯了使用工具,就该开始学习 C++ 了。接下来的探索从阅读课开始。

二、读取 C++ 代码

我怀疑你已经有了一些 C++ 的知识。也许你已经知道 C,Java,Perl,或者其他类似 C 的语言。也许你知道这么多的语言,你可以很容易地确定共同的元素。让我们来验证我的假设。花几分钟阅读清单 2-1 ,然后回答后面的问题。

 1 /// Read the program and determine what the program does.
 2
 3 import <iostream>;
 4 import <limits>;
 5
 6 int main()
 7 {
 8     int min{std::numeric_limits<int>::max()};
 9     int max{std::numeric_limits<int>::min()};
10     bool any{false};
11     int x;
12     while (std::cin >> x)
13     {
14         any = true;
15         if (x < min)
16             min = x;
17         if (x > max)
18             max = x;
19     }
20
21     if (any)
22         std::cout << "min = " << min << "\nmax = " << max << '\n';
23 }

Listing 2-1.Reading Test

清单 2-1 是做什么的?





清单 2-1 从标准输入中读取整数,并跟踪输入的最大值和最小值。输入完毕后,它打印这些值。如果输入不包含数字,程序将不打印任何内容。

让我们仔细看看程序的各个部分。

评论

第 1 行以三个连续的斜杠开始注释。注释在行尾结束。实际上,你只需要两个斜杠来表示一个注释的开始(//),但是正如你将在本书后面学到的,额外的斜杠有特殊的含义。

请注意,斜线之间不能有空格。这通常适用于 C++ 中的所有多字符符号。这是一条重要的规则,也是你必须尽早记住的规则。“符号中没有空格”规则的一个推论是,当 C++ 看到相邻字符时,它通常会构造尽可能长的符号,即使您可以看到这样做会产生无意义的结果。

用 C++ 编写注释的另一种方法是以/*开始注释,以*/结束注释。这种风格和清单 2-1 中展示的风格的区别在于,使用这种方法,你的注释可以跨越多行。你可能会注意到,本书中的一些程序使用/**来开始注释。与清单 2-1 中的第三个斜线非常相似,第二个星号(*)很神奇,但此时并不重要。一个注释不能嵌套在同一风格的注释中,但是你可以将一种风格的注释嵌套在另一种风格的注释中,如清单 2-2 所示。

/* Start of a comment /* start of comment characters are not special in a comment
 // still in a comment
 Still in a comment
*/
no_longer_in_a_comment();
// Start of a comment /* start of comment characters are not special in a comment
no_longer_in_a_comment();

Listing 2-2.Demonstrating

Comment Styles and Nesting

C++ 社区广泛使用这两种风格。习惯于看到和使用这两种风格。

修改清单 2-1 ,将///注释改为使用/***/风格,然后尝试重新编译程序。会发生什么?


如果您做了正确的更改,程序应该仍然可以正常编译和运行。编译器完全删除了注释,所以最终的程序应该没有什么不同。(一个例外是,一些二进制格式包含时间戳,这必然会因编译运行的不同而不同。)

模块

清单 2-1 的第 3 行和第 4 行从部分标准库中导入声明和定义。像 C 和许多其他语言一样,C++ 区分了核心语言和标准库。两者都是标准语言的一部分,没有这两部分,工具套件是不完整的。区别在于核心语言是自成体系的。例如,某些类型是内置的,编译器天生就知道它们。其他类型是根据内置类型定义的,因此它们是在标准库中声明的,并且您必须指示编译器您想要使用它们。这就是第 3 行和第 4 行的内容。

IMPORTING VS. INCLUDING

当我写这篇文章时,没有编译器(甚至是最新的预发行版)能够编译清单 2-1 ,因为它的import声明。理解import是做什么的以及它是如何工作的非常复杂,以至于在探索 2 之前我不会涉及它。但是它在这里,干扰探索。

任何时候你看到一个import声明,你都可以把它改成一个#include指令。只需将import替换为#include,并删除行尾的分号。该书网站上的代码清单( https://cpphelp.com/exploring/ )提供了两种风格的文件。下载适用于您的开发环境的文件,而不用担心import#include的实际含义。

实现关键字import只是一个更大任务的一部分,所以编译器和库需要一些时间才能跟上。在那之前,我们有一个变通办法。

特别是,第 3 行通知编译器标准 I/O 流的名称(std::cin表示标准输入,std::cout表示标准输出)、输入操作符(>>)和输出操作符(<<)。第四行带来了std::numeric_limits这个名字。注意,标准库中的名字一般以std::(“标准”的简称)开头。

按照 C++ 的说法,import关键字也是一个动词,比如“第 3 行导入模块iostream”,“第 4 行导入limits模块”,等等。一个模块包含一系列声明和定义。(声明是一种定义。定义告诉编译器更多的是名字而不是声明。先不要担心区别,但是请注意我何时使用声明以及何时使用定义。)编译器需要这些声明和定义,所以它知道如何处理像std::cin这样的名字。在 C++ 编译器和标准库的文档中,有关于标准模块的信息。如果您很好奇,您可以访问包含标准模块源代码的文件夹或目录,看看您能在那里找到什么,但是如果您不能理解它们,请不要失望。C++ 标准库充分利用了 C++ 语言的全部功能。很可能在你读完这本书的大部分内容之前,你无法理解库的大部分内容。

另一个重要的 C++ 规则:编译器必须知道每个名字的意思。人类通常可以从上下文中推断出意思或者至少是一个词类。例如,如果我说,“我把我的饮料弄得满衬衫都是”,你可能不知道furled到底是什么意思,但你可以推断出它是动词的过去式,它可能意味着一些不受欢迎的和有点混乱的事情。

C++ 编译器比你笨多了。当编译器读取一个符号或标识符时,它必须确切地知道这个符号或标识符是什么意思,以及它是“语音”的哪一部分。符号是标点符号(比如语句结尾的分号)还是运算符(比如加法的加号)?标识符是一种类型吗?一个功能?一个变量?编译器还必须知道你可以用那个符号或名字做的一切,这样它才能正确地编译代码。它能知道的唯一方法就是你告诉它,而你告诉它的方法就是写一个声明或者从一个模块导入一个声明。这就是import声明的意义所在。

在本书的后面,您甚至将学习编写自己的模块。

修改第 4 行,将limits拼错为stimil。试着编译程序。会发生什么?




编译器找不到任何名为stimil的模块,所以它发出一条消息。然后它可能试图编译程序,但是它不知道std::numeric_limits是什么,所以它发出一个或多个消息。一些编译器级联消息,这意味着每次使用std::numeric_limits都会产生额外的消息。实际误差消失在噪声中。关注编译器发出的前一条或几条消息。修复它们,然后再试一次。随着你获得 C++ 的经验,你会知道哪些消息仅仅是噪音,哪些是重要的。不幸的是,大多数编译器不会告诉你,例如,你不能使用std::numeric_limits,直到你包含了<limits>模块。相反,您需要一个好的 C++ 语言参考,这样您就可以自己查找正确的头文件。首先要检查的是编译器和库附带的文档。作者比编译器作者更慢地赶上了 C++ 20 的标准,所以要经常查看网站和书店的最新参考资料。

大多数程序员不怎么用<limits>;清单 2-1 包含它只是为了获得std::numeric_limits的定义。另一方面,本书中几乎每个程序都使用<iostream>,因为它声明了 I/O 流对象的名称和类型,std::cinstd::cout。还有其他的 I/O 模块,但是对于基本的控制台交互,你只需要<iostream>。在接下来的探索中,你会遇到更多的模块。

主程序

每个 C++ 程序都必须有int main(),如第 6 行所示。你被允许在一个主题上有一些变化,但是名字main是至关重要的。一个程序只能有一个main,并且名字必须全部用小写字母拼写。定义必须以int开头。

Note

有几本书教你使用void。那些书是错的。如果你必须说服某人void是错的而int是对的,让怀疑者参考 C++ 标准的[basic.start.main]部分。

现在,在名字main后面使用空括号。

下一行启动主程序。注意这些语句是如何在花括号({})内分组的。C++ 就是这样对语句分组的。新手的一个常见错误是在阅读程序时省略了一个花括号或者看不到花括号。如果您习惯于更冗长的语言,如 Pascal、Ada 或 Visual Basic,您可能需要一些时间来熟悉更简洁的 C++ 语法。这本书会给你很多练习的机会。

修改第 6 行,用大写字母(MAIN))拼写main。试着编译程序。会发生什么?




编译器可能会接受程序,但链接器会抱怨。您能否看出编译器和链接器之间的区别取决于您的特定工具。尽管如此,您还是没能创建一个有效的程序,因为您必须有一个main。只有main这个名字比较特别。就编译器而言,MAIN只是另一个名字,就像minmax。因此,你不会得到一个错误消息说你拼错了main,只是说main不见了。拥有一个名为MAIN的函数的程序并没有错,但是要成为一个完整的程序,你必须确保包含定义main

变量定义

第 8 行到第 11 行定义了一些变量。每行的第一个词是变量的类型。下一个词是变量名。该名称后面可选地跟着一个花括号中的初始值。type int整数的简称,bool布尔的简称。

Note

布尔以数理逻辑的发明者乔治·布尔的名字命名。因此,一些语言将这种类型命名为logical。不清楚为什么 C++ 等语言对以 Boole 命名的类型使用bool而不是boole

名称std::numeric_limits是 C++ 标准库的一部分,允许您查询内置算术类型的属性。您可以确定类型所需的位数、十进制位数、最小值和最大值等。把你好奇的类型放在尖括号里。(在 C++ 中,你会经常看到这种使用类型的方法。)因此,您也可以查询std::numeric_limits<bool>::min()并得到结果false

如果您要查询 bool 中的位数,您会得到什么结果?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

尝试编译并运行清单 2-3 ,看看是否正确。

import <iostream>;
import <limits>;

int main()
{
  // Note that "digits" means binary digits, i.e., bits.
  std::cout << "bits per bool: " << std::numeric_limits<bool>::digits << '\n';
}

Listing 2-3.Determining the Number of Bits in a bool

你得到你期望的价值了吗?如果不是,你明白为什么结果是 1 吗?

声明

清单 2-1 的第 12 行包含一条while语句。第 15、17 和 21 行开始了if语句。它们有相似的语法:两个语句都以关键字开始,后面是括号中的布尔条件,再后面是一个语句。语句可以是简单的语句,比如第 16 行的赋值,也可以是花括号内的语句列表。注意,一个简单的语句以分号结束。

赋值(第 14、16 和 18 行)使用了一个等号。为了清楚起见,当我大声读程序或自己读程序时,我喜欢把等号读成“得到”例如,“x 得到 min。”

当条件为真时,while循环执行其相关语句。在执行语句之前会测试条件,因此如果条件第一次为假,语句将永远不会执行。

在第 12 行,条件是输入操作。它从标准输入(std::cin)中读取一个整数,并将该整数存储在变量x中。只要一个值成功存储在x中,该条件就为真。如果输入格式不正确,或者如果程序到达输入流的末尾,则逻辑条件变为假,循环终止。

if语句后面可以跟一个else分支;您将在未来的探索中看到示例。

第 21 行的条件由一个名字组成:any。因为它有类型bool,所以可以直接作为条件使用。

修改第 15 行,将语句改为“if (x)”。这种错误有时会在你不小心的时候出现(我们都时不时会不小心)。你编译程序的时候预计会发生什么?



你对编译器没有抱怨感到惊讶吗?运行该程序时,您预计会发生什么?




如果您向程序提供以下输入,您希望输出什么?



0   1   2   3

如果您向程序提供以下输入,您希望输出什么?



3   2   1   0

解释正在发生的事情。






C++ 对于它所允许的条件是宽容的。任何数字类型都可以是条件,编译器将非零值视为真,将零视为假。换句话说,它提供了一个隐式≠ 0 来测试数值。

许多 C 和 C++ 程序员利用这些语言提供的简洁性,但我发现这是一种草率的编程实践。始终确保您的条件在本质上是符合逻辑的,即使这意味着使用与零的显式比较。比较≠的 C++ 语法是!=,和x != 0一样。

输出

输出操作符是<<,你的程序通过导入<iostream>得到。您可以打印变量值、字符串、单个字符或计算表达式。

用单引号将单个字符括起来,例如'X'。当然,有时您可能需要在输出中包含一个单引号。要打印单引号,必须用反斜杠(\')对引号字符进行转义。对字符进行转义会指示编译器将其作为标准字符处理,而不是作为程序语法的一部分。其他转义字符可以跟在反斜杠后面,比如\n表示换行符(即,一个神奇的字符序列开始一个新的文本行;输出中的实际字符取决于主机操作系统)。要打印反斜杠字符,将其转义:'\\'。一些字符的例子包括:'x''#''7''\\''\n'

如果您想一次打印多个字符,请使用用双引号括起来的字符串。要在字符串中包含双引号,请使用反斜杠转义:

std::cout << "not quoted; \"in quotes\", not quoted";

单个 output 语句可以使用多次出现的<<,如第 22 行所示,或者您可以使用多个 output 语句。唯一的区别是可读性。

修改清单 2-3 来试验不同风格的输出。尝试使用多个 output 语句。

当一个if语句的主体包含多个语句时,记得使用花括号。

看到了吧!我告诉过你你能读懂 C++ 程序。现在你要做的就是填补一些关于细节的知识空白。下一篇文章从基本的算术运算符开始。

三、整数表达式

在 Exploration 2 中,您检查了一个定义了一些变量并对它们执行一些简单操作的程序。这篇文章介绍了基本的算术运算符。阅读清单 3-1 ,然后回答后面的问题。

 1 /// Read the program and determine what the program does.
 2
 3 import <iostream>;
 4
 5 int main()
 6 {
 7     int sum{0};
 8     int count{};
 9
10     int x;
11     while (std::cin >> x)
12     {
13         sum = sum + x;
14         count = count + 1;
15     }
16
17     std::cout << "average = " << sum / count << '\n';
18 }

Listing 3-1.Integer Arithmetic

清单 3-1 中的程序是做什么的?



使用以下输入测试程序:

10   50   20   40   30

第 7 行和第 8 行将变量sumcount初始化为零。你可以在花括号中输入任意整数值来初始化一个变量(第 7 行);该值不必是常量。您甚至可以将花括号留空,将变量初始化为合适的默认值(例如,boolfalse,而int用 0),如第 8 行所示。如果没有花括号,变量就不会被初始化,所以程序唯一能做的就是给变量赋值,如第 10 行所示。通常,不初始化变量是一个坏主意,但是在这种情况下,x是安全的,因为第 11 行通过从标准输入中读取立即向其中填充一个值。

第 13 行和第 14 行显示了加法(+)和赋值(=)的例子。加法遵循计算机算术的正常规则(我们稍后会担心溢出)。赋值的工作方式和任何过程语言一样。

因此,您可以看到清单 3-1 从标准输入中读取整数,将它们相加,并打印由除法(/)运算符计算的平均值。还是真的?

清单 3-1 怎么了?




尝试在没有输入的情况下运行程序,即在启动程序后立即按下文件结束键。一些操作系统有一个“空”文件,可以作为输入流提供。当程序从空文件中读取时,输入流总是看到文件结束条件。在类似 UNIX 的操作系统上,运行以下命令行:

list0301 < /dev/null

在 Windows 上,空文件称为NUL,所以键入

list0301 < NUL

会发生什么?


C++ 不喜欢被零除吧?每个平台的反应都不一样。大多数系统都会以某种方式指示错误状态。少数悄悄给你垃圾结果。无论哪种方式,你都得不到任何有意义的东西。

通过引入一个if语句来修复程序。不要担心这本书还没有涵盖if语句。我相信你能找出如何确保这个程序避免被零除。在这里写下修正后的程序:























现在试试你的新程序。你的修复成功了吗?


将您的解决方案与清单 3-2 进行比较。

 1 /// Read integers and print their average.
 2 /// Print nothing if the input is empty.
 3
 4 import <iostream>;
 5
 6 int main()
 7 {
 8     int sum{0};
 9     int count{};
10
11    int x;
12    while (std::cin >> x)
13    {
14        sum = sum + x;
15        count = count + 1;
16    }
17
18    if (count != 0)
19        std::cout << "average = " << sum / count << '\n';
20 }

Listing 3-2.Print Average, Testing for a Zero Count

记住!=是运算符≠的 C++ 语法。因此,当count不为零时,count != 0为真,这意味着程序已经从其输入中读取了至少一个数字。

假设您使用以下输入运行程序:

2   5   3

你期望的输出是什么?


试试看。实际产量是多少?


你得到你所期望的了吗?有些语言对整数除法和浮点除法使用不同的运算符。C++(像 C 一样)使用相同的运算符,并根据上下文来决定执行哪种除法。如果两个操作数都是整数,则结果也是整数。

如果输入是,你期望什么


2   5   4

试试看。实际产量是多少?


整数除法将结果向零截断,因此 C++ 表达式5 / 3等于4 / 3等于1

其他算术运算符是-表示减法,*表示乘法,%表示余数。C++ 没有求幂运算符。

清单 3-3 向用户询问整数,并告诉用户数字是偶数还是奇数。(不用管输入具体是怎么工作的;探索号 5 将会报道这个。)完成第 11 行。

 1 /// Read integers and print a message that tells the user
 2 /// whether the number is even or odd.
 3
 4 import <iostream>;
 5
 6 int main()
 7 {
 8
 9     int x;
10     while (std::cin >> x)
11         if (                   )           // Fill in the condition.
12             std::cout << x << " is odd.\n";
13         else
14             std::cout << x << " is even.\n";
15 }

Listing 3-3.Testing for Even or Odd Integers

测试你的程序。你做对了吗?


我希望您使用了类似这样的一行代码:

if (x % 2 != 0)

换句话说,如果一个数除以 2 后有一个非零余数,那么这个数就是奇数。

你知道!=比较的是不平等。你认为应该如何写一个等式比较?尝试颠倒奇数和偶数消息的顺序,如清单 3-4 所示。完成第 11 行的条件。

 1 /// Read integers and print a message that tells the user
 2 /// whether the number is even or odd.
 3
 4 import <iostream>;
 5
 6 int main()
 7 {
 8
 9     int x;
10     while (std::cin >> x)
11         if (                   )           // Fill in the condition.
12             std::cout << x << " is even.\n";
13         else
14             std::cout << x << " is odd.\n";
15 }

Listing 3-4.Testing for Even or Odd Integers

为了测试相等性,使用两个等号(==)。在这种情况下:

        if (x % 2 == 0)

新的 C++ 程序员,尤其是那些习惯了 SQL 和类似语言的程序员,经常犯的一个错误是使用单个等号进行比较。在这种情况下,编译器通常会提醒您出现错误。继续尝试,看看编译器会做什么。当你在第 11 行使用一个等号时,编译器会发出什么信息?





一个等号是赋值运算符。因此,C++ 编译器认为您试图将值 0 赋给表达式x % 2,这是无意义的,编译器正确地告诉您这一点。

如果要测试x是否为零怎么办?修改清单 3-1 count **为零时打印一条信息。**一旦你得到正确的程序,它应该看起来像清单 3-5 。

 1 /// Read integers and print their average.
 2 /// Print nothing if the input is empty.
 3
 4 import <iostream>;
 5
 6 int main()
 7 {
 8      int sum{0};
 9      int count{};
10
11     int x;
12     while (std::cin >> x)
13     {
14         sum = sum + x;
15         count = count + 1;
16     }
17
18     if (count == 0)
19         std::cout << "No data.\n";
20     else
21         std::cout << "average = " << sum / count << '\n';
22 }

Listing 3-5.Print Average, Testing for a Zero Count

现在修改清单 3-5 ,在第 18 行使用一个等号。你的编译器发出了什么消息?





大多数现代编译器认识到这种常见错误并发出警告。严格来说,代码是正确的:条件将零赋给count。回想一下,条件 0 意味着 false,所以程序总是打印No data.,不管它实际读取了多少数据。

如果您的编译器没有发出警告,请阅读编译器的文档。您可能需要启用一个开关来打开额外的警告,比如“可能使用赋值来代替比较”或者“条件总是为假”

正如你所看到的,使用整数很容易,也不足为奇。然而,文本有点复杂,您将在下一篇文章中看到。

四、字符串

在前面的探索中,您使用带引号的字符串作为每个输出操作的一部分。在这个探索中,你将开始学习如何通过对字符串做更多的处理来使你的输出更有趣。从阅读清单 4-1 开始。

import <iostream>;

int main()
{
   std::cout << "Shape\tSides\n" << "-----\t-----\n";
   std::cout << "Square\t" << 4 << '\n' <<
                "Circle\t?\n";
}

Listing 4-1.Different Styles of String Output

预测清单 中程序的输出 4-1 **。**你可能已经知道\t是什么意思了。如果是这样的话,这个预测很容易做出。如果你不知道,猜一猜。


现在检查你的答案。你是对的吗?那么 \t 是什么意思呢?


在字符串内部,反斜杠(\)是一个特殊的,甚至是神奇的字符。它改变了后面字符的含义。您已经看到了\n如何开始一个新行。现在您知道了\t是一个水平制表符:也就是说,它将随后的输出对齐到一个制表符位置。在典型的控制台中,每八个字符位置设置一个制表位。

应该如何打印字符串中的双引号字符?


写一个程序来测试你的假设,然后运行程序。你是对的吗?


将您的程序与清单 4-2 进行比较。

import <iostream>;

int main()
{
   std::cout << "\"\n";
}

Listing 4-2.Printing a Double-Quote Character

在这种情况下,反斜杠将特殊字符转换为普通字符。C++ 可以识别其他一些反斜杠字符序列,但这三个是最常用的。(当你读到《探索》中的角色时,你会学到更多。)

现在修改清单 4-1 以将三角形添加到形状列表中。

输出是什么样的?制表符不会自动对齐一列,而只是将输出定位在下一个制表符位置。要对齐列,您必须控制输出。一种简单的方法是使用多个制表符,如清单 4-3 所示。

 1 import <iostream>;
 2
 3 int main()
 4 {
 5    std::cout << "Shape\t\tSides\n" <<
 6                 "-----\t\t-----\n";
 7    std::cout << "Square\t\t" << 4 << '\n' <<
 8                 "Circle\t\t?\n"
 9                 "Triangle\t" << 3 << '\n';
10 }

Listing 4-3.Adding a Triangle and Keeping the Columns Aligned

我在列表 4-3 中捉弄了你。仔细观察第 8 行的结尾和第 9 行的开头。请注意,该程序缺少一个输出操作符(<<),该操作符通常用于分隔所有输出项。只要有两个(或更多)相邻的字符串,编译器就会自动将它们合并成一个字符串。这个技巧只适用于字符串,不适用于字符。因此,你可以用许多不同的方式写第 8 行和第 9 行,意思完全一样。

std::cout << "\nCircle\t\t?\n" "Triangle\t" << 3 << '\n';
std::cout << "\nCircle\t\t?\nTriangle\t" << 3 << '\n';
std::cout << "\n" "Circle" "\t\t?\n" "Triangle" "\t" << 3 << '\n';

选择你最喜欢的风格,坚持下去。我喜欢在每一个新行之后做一个清晰的分隔,这样阅读我的程序的人就可以清楚地区分每一行的结束和新的一行的开始。

您可能会问自己,为什么我要麻烦地分别打印数字,而不是打印一个大字符串。这个问题问得好。在真正的程序中,打印单个字符串是最好的,但在本书中,我想不断提醒您可以用各种方式编写输出语句。例如,想象一下,如果你事先不知道一个形状的名称和它的边数,你会怎么做。也许这些信息存储在变量中,如清单 4-4 所示。

 1 import <iostream>;
 2 import <string>;
 3
 4 int main()
 5 {
 6    std::string shape{"Triangle"};
 7    int sides{3};
 8
 9    std::cout << "Shape\t\tSides\n" <<
10                 "-----\t\t-----\n";
11    std::cout << "Square\t\t" << 4 << '\n' <<
12                 "Circle\t\t?\n";
13    std::cout << shape << '\t' << sides << '\n';
14 }

Listing 4-4.Printing Information That Is Stored in Variables

字符串的类型是std::string。你必须在程序的顶部附近有import <string>来通知编译器你正在使用std::string类型。第 6 行展示了如何给一个字符串变量一个初始值。有时,您希望变量以空开始。你认为如何定义一个空的字符串变量?



写一个程序来测试你的假设。

如果在验证字符串是否真的为空时遇到困难,请尝试在两个其他非空字符串之间打印该字符串。清单 4-5 给出了一个例子。

1 import <iostream>;
2 import <string>;
3
4 int main()
5 {
6    std::string empty;
7    std::cout << "|" << empty << "|\n";
8 }

Listing 4-5.Defining and Printing an Empty String

将您的程序与清单 4-5 进行比较。你更喜欢哪个?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

为什么呢?



第 6 行没有为变量empty提供初始值。您在 Exploration 3 中了解到,省略初始值会导致变量未初始化,这将是一个错误,因为没有其他值被赋给empty。不允许打印或访问未初始化变量的值。但是std::string不同。在这种情况下,缺少初始化式与空括号是一样的;也就是说,变量被初始化为一个空字符串。

当你定义一个没有初始值的字符串变量时,C++ 保证这个字符串最初是空的。修改清单 4-4 所以 shape sides 变量未初始化。预测程序的输出。



发生了什么事?解释一下。




你的程序应该如清单 4-6 所示。

 1 import <iostream>;
 2 import <string>;
 3
 4 int main()
 5 {
 6    std::string shape;
 7    int sides;
 8
 9    std::cout << "Shape\t\tSides\n" <<
10                 "-----\t\t-----\n";
11    std::cout << "Square\t\t" << 4 << '\n' <<
12                 "Circle\t\t?\n";
13    std::cout << shape << '\t' << sides << '\n';
14 }

Listing 4-6.Demonstrating Uninitialized Variables

当我运行清单 4-6 时,我会得到不同的答案,这取决于我使用的编译器和平台。大多数编译器会发出警告,但仍然会编译程序,所以你可以运行它。我得到的答案之一是这样的:

Shape          Sides
-----          -----
Square         4
Circle         ?
        4226851

用另一个平台上的另一个编译器,最后的数字是0。然而,另一个编译器的程序打印出最后的数字-858993460。有些系统甚至会崩溃,而不是打印出shapesides的值。

这难道不奇怪吗?如果没有为类型为std::string的变量提供初始值,C++ 会确保该变量以初始值开始,即空字符串。另一方面,如果变量的类型是int,你无法判断初始值实际上是什么,事实上,你甚至无法判断程序是否会运行。这就是所谓的未定义行为。该标准允许 C++ 编译器和运行时环境在遇到某些错误情况时做任何事情,绝对是任何事情,比如访问未初始化的变量。

C++ 的一个设计目标是,如果可以避免,编译器和库不应该做任何额外的工作。只有程序员知道什么值作为变量的初始值是有意义的,所以赋予初始值必须是程序员的责任。毕竟,当你正在对你的天气模拟器进行最后的润色时(这将最终解释为什么当我去海滩时总是下雨),你不希望内部循环被一个浪费的指令所负担。性能保证的另一面是程序员的额外负担,以避免出现导致未定义行为的情况。一些语言帮助程序员避免问题,但是这种帮助总是伴随着性能的损失。

那么std::string是怎么回事?简而言之,复杂类型(如字符串)不同于简单的内置类型。对于std::string这样的类型,C++ 库提供一个定义良好的初始值其实更简单。标准库中大多数有趣的类型都有相同的行为方式。

如果你不记得什么时候定义一个没有初始值的变量是安全的,为了安全起见,使用空括号:

std::string empty{};
int zero{};

我建议初始化每个变量,即使你知道程序很快就会覆盖它,比如我们之前使用的输入循环。以“性能”的名义省略初始化很少能提高性能,而且总是会损害可读性。接下来的探索展示了初始化每个变量的重要性。

OLD-FASHIONED INITIALIZATION

初始化所有变量的大括号风格是在 C++ 11 中引入的,所以早于 C++ 11 的代码(或由在 C++ 11 之前学习 C++ 并且还没有掌握新的初始化风格的程序员编写的新代码)使用不同的方法来初始化变量。

例如,初始化整数的常用方法是使用等号。它看起来像一个赋值语句,但它不是。它定义并初始化一个变量。

int x = 42;

您也可以使用括号:

int x(42);

许多标准库类型也是如此:

std::string str1 = "sample";
std::string str2("sample");

有些类型需要括号,并且没有等号。其他类型在 C++ 11 之前使用花括号。等号、括号和花括号都有不同的规则,初学者很难理解等号和括号在初始化时的细微差别。

因此,标准化委员会努力在 C++ 11 中定义一个单一的、统一的初始化风格,他们不得不在 C++ 14 中进行调整。不过,您还没有完全走出困惑区,因为您将会看到一些需要等号进行初始化的上下文,或者大括号不像您预期的那样工作。但是普通变量应该总是使用花括号,这是我在这次探索中提出的。

五、简单输入

到目前为止,探索主要集中在产量上。现在是时候把注意力转向输入了。给定输出操作符是<<你期望输入操作符是什么?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

这不用火箭科学家来推断,是吗?输入操作符是>>,输出操作符的反方向。可以把操作符想象成指向信息流动方向的箭头:从输入流到变量,或者从变量到输出流。

清单 5-1 显示了一个执行输入和输出的简单程序。

import <iostream>;

int main()
{
   std::cout << "Enter a number: ";
   int x;
   std::cin >> x;
   std::cout << "Enter another number: ";
   int y;
   std::cin >> y;

   int z{x + y};
   std::cout << "The sum of " << x << " and " << y << " is " << z << "\n";
}

Listing 5-1.Demonstrating Input and Output

清单 5-1 从标准输入中读取多少个数字?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

假设您输入4221作为两个输入值。你对产量有什么期望?


现在运行程序,检查你的预测。希望你拿到了63。假设您键入以下内容作为输入:

42*21

你预测会有什么样的产出?


测试你的假设。实际产量是多少?


你看到发生了什么吗?如果没有,尝试将xyz作为程序的输入。试试42-21。试试42.21

这个程序展示了两个你必须理解的不同的行为。首先,要读取一个int,输入流必须包含一个有效的整数。整数可以以符号(-+)开头,但其后必须全是数字;不允许中间有空白。当输入操作到达不能作为有效整数一部分的第一个字符(如*)时,输入操作停止。如果从输入流中读取了至少一个数字,则读取成功,输入文本被转换为整数。如果输入流不是以有效的整数开头,则读取失败。如果读取失败,则不修改输入变量。

第二种行为是你在之前的探索中发现的;未初始化的int变量导致未定义的行为。换句话说,如果读取失败,变量包含垃圾,或者更糟。例如,当您学习浮点数时,您将了解到未初始化的浮点变量中的一些位模式会导致程序终止。在一些专门的硬件上,未初始化的整数也可以做到这一点。这个故事的寓意是,使用未初始化的变量会导致未定义的行为。那很糟糕。所以不要做。

因此,当输入为xyz时,两次读取都失败,并导致未定义的行为。您可能会看到这两个数字的垃圾值。当输入为42-21时,第一个数字为42,第二个数字为-21,所以结果是正确的。但是当输入的是42.21时,第一个数字是42,第二个数字是垃圾,因为整数不能以点开头(.)。

一旦输入操作失败,所有后续的输入尝试也将失败,除非您采取补救措施。这就是为什么如果第一个数字无效,程序不会等待你输入第二个数字。C++ 可以告诉你什么时候输入操作失败,所以你的程序可以避免使用垃圾值。此外,您可以重置流的错误状态,以便在处理错误后继续读取。我将在以后的探索中介绍这些技术。现在,确保您的输入是有效和正确的。

当你的程序没有初始化变量时,一些编译器会警告你,但是为了安全起见,最好始终初始化每个变量。如您所见,即使程序立即尝试在变量中存储一个值,也可能不会成功,这可能会导致意外的行为。

你想过整数会这么复杂吗?当然,字符串更简单,因为不需要解释它们或转换它们的值。让我们看看它们是否真的比整数简单。清单 5-2 类似于清单 5-1 ,但是它将文本读入std::string变量。

import <iostream>;
import <string>;

int main()
{
   std::cout << "What is your name? ";
   std::string name{};
   std::cin >> name;
   std::cout << "Hello, " << name << ", how are you? ";
   std::string response{};
   std::cin >> response;
   std::cout << "Good-bye, " << name << ". I'm glad you feel " << response << "\n";
}

Listing 5-2.Reading Strings

清单 5-2 显然不是人工智能的模型,但它很好地演示了一件事。假设输入如下:

Ray Lischner
Fine

你期望的结果是什么?




运行程序,测试你的假设。你是对的吗?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

解释。



尝试不同的输入,并尝试辨别 C++ 用来在输入流中分隔字符串的规则。准备好了吗?去吧。我会一直等到你做完。

这么快就回来了?C++ 如何在输入流中分隔字符串?




任何空白字符(空白字符的确切列表取决于您的实现,但通常包括空格、制表符、换行符等)都会结束一个字符串,至少就输入操作而言是这样。具体来说,C++ 跳过前导空白字符。然后它累积非空格字符形成字符串。字符串在下一个空白字符处结束。

那么当你混合整数和字符串时会发生什么呢?编写一个程序,要求输入一个人的名字(仅名字)和年龄(年龄),然后将输入回显到标准输出中。你想先要哪个?阅读后打印信息。

表 5-1 显示了您的程序的一些样本输入。在每一个旁边,**写下你对程序输出的预测。**然后运行程序,写出实际输出。

表 5-1。

姓名和年龄的输入示例

|

投入

|

预测产量

|

实际输出

|
| — | — | — |
| Ray44 |   |   |
| 44Ray |   |   |
| Ray 44 |   |   |
| 44 Ray |   |   |
| Ray44 |   |   |
| 44Ray |   |   |
| 44-Ray |   |   |
| Ray-44 |   |   |

把标准输入想象成一串字符。不管用户如何输入这些字符,程序都会看到它们一个接一个地出现。(好吧,它们通过缓冲区负载大量到达,但这是一个次要的实现细节。就你而言,你的程序一次读取一个字符,这个字符来自缓冲区,而不是实际的输入设备,这并不重要。)因此,程序总是保持流中当前位置的概念。下一个读取操作总是从该位置开始。

在开始任何输入操作之前,如果输入位置处的字符是空白字符,则程序跳过(即,读取并丢弃)该字符。它一直读取并丢弃字符,直到到达一个非空格字符。然后开始实际的读取。

如果程序试图读取一个整数,它会在输入位置抓取字符,并检查它对一个整数是否有效。否则,读取失败,并且输入位置不移动。否则,输入操作将保留该字符和所有后续字符,它们是整数的有效元素。输入操作将文本解释为整数,并将值存储在变量中。因此,在读取一个整数后,您知道输入位置指向的字符是而不是一个有效的整数字符。

读取字符串时,从流中抓取所有字符,直到到达一个空白字符。因此,字符串变量不包含任何空白字符。如前所述,下一个读操作将跳过空白。

当用户关闭控制台或终端时,或者当用户键入特殊的击键序列来告诉操作系统结束输入时(如 UNIX 上的 Ctrl+D 或 DOS 或 Windows 上的 Ctrl+Z),输入流在文件的末尾结束(如果从文件中读取)。一旦到达输入流的末尾,所有后续的读取尝试都将失败。这就是导致循环在探索 2 中结束的原因。

清单 5-3 显示了我的名字优先程序版本。当然,你们的计划在细节上会有所不同,但基本大纲应该与你们的一致。

import <iostream>;
import <string>;

int main()
{
   std::cout << "What is your name? ";
   std::string name{};
   std::cin >> name;

   std::cout << "Hello, " << name << ", how old are you? ";
   int age{};
   std::cin >> age;

   std::cout << "Good-bye, " << name << ". You are " << age << " year";
   if (age != 1)
      std::cout << 's';
   std::cout << " old.\n";
}

Listing 5-3.Getting the User’s Name and Age

现在修改程序,颠倒姓名和年龄的顺序,再次尝试所有输入值。解释你观察到的现象。




当输入操作由于畸形输入而失败时,流进入错误状态;例如,当程序试图读取一个整数时,输入流包含字符串“Ray”。所有后续读取流的尝试都会导致错误,而不会真正尝试读取。即使流随后尝试读取一个字符串(否则会成功),错误状态也是粘滞的,字符串读取也会失败。

换句话说,当程序不能读取用户的年龄时,它也不能读取名字。这就是为什么程序会把名字和年龄都写对,或者都写错。

清单 5-4 显示了我版本的年龄优先计划。

import <iostream>;
import <string>;

int main()
{
   std::cout << "How old are you? ";
   int age{};
   std::cin >> age;

   std::cout << "What is your name? ";
   std::string name{};
   std::cin >> name;

   std::cout << "Good-bye, " << name << ". You are " << age << " year";
   if (age != 1)
      std::cout << 's';
   std::cout << " old.\n";
}

Listing 5-4.Getting the User’s Age and Then Name

表 5-2 显示了每种情况下输出的截断版本(只有姓名和年龄)。

表 5-2。

用 C++ 方式解释输入

|

投入

|

先说名字

|

年龄第一

|
| — | — | — |
| Ray44 | "Ray44"0 | 0"" |
| 44Ray | 44Ray"0 | 44"Ray" |
| Ray 44 | "Ray"44 | 0"" |
| 44 Ray | "44"0 | 44"Ray" |
| Ray44 | "Ray"44 | 0"" |
| 44Ray | "44"0 | 44"Ray" |
| 44#Ray | "44#Ray"0 | 44"#Ray" |
| Ray#44 | "Ray#44"0 | 0"" |

处理输入流中的错误需要一些更高级的 C++,但是处理代码中的错误是您现在可以处理的事情。接下来的探索将帮助您解开编译器错误消息。

六、错误消息

到目前为止,您已经看到了来自 C++ 编译器的大量错误消息。毫无疑问,有些是有用的,有些是神秘的——有些两者都是。这个探索展示了一些常见的错误,并让您有机会看到编译器会针对这些错误发出什么类型的消息。你对这些信息越熟悉,你将来就越容易理解它们。

通读清单 6-1 并留意错误。

 1 #include <iosteam>
 2 // Look for errors
 3 int main()
 4 
 5   std::cout < "This program prints a table of squares.\n";
 6          "Enter the starting value for the table: ";
 7   int start{0};
 8   std::cin >> start;
 9   std::cout << "Enter the ending value for the table: ";
10   int end(start);
11   std::cin << endl
12   std::cout << "#   #²\n";
13   int x{start};
14   end = end + 1; // exit loop when x reaches end
15   while (x != end)
16   {
17     std:cout << x << "   " << x*x << "\n";
18     x = x + 1;
19   }
20 }

Listing 6-1.Deliberate Errors

你希望编译器检测出哪些错误?







下载源代码并编译清单 [6-1 。

你的编译器实际上会发出什么消息?








创建三个组:您正确预测的消息、您预期但编译器没有发出的消息,以及编译器发出但您没有预期的消息。每组有多少条消息?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

如果您使用命令行工具,预计会在屏幕上看到大量错误。如果您使用 IDE,它将有助于收集错误消息,并将每条消息与编译器认为是错误原因的源代码中的相关点相关联。编译器并不总是正确的,但是它的提示通常是一个很好的起点。

编译器通常将问题分为两类:错误和警告。错误会阻止编译器生成输出文件(目标文件或程序)。警告告诉你有问题,但不会阻止编译器产生它的输出。现代编译器非常擅长检测有问题但有效的代码并发出警告,所以要养成留意警告的习惯。事实上,我建议提高对警告的敏感度。查看编译器的文档,寻找指导编译器检测尽可能多的警告的选项。对于 g++ 和 clang++,开关是-Wall。Visual Studio 用的是/Wall。另一方面,有时编译器会出错,某些警告是没有帮助的。你通常可以禁用一个特定的警告,比如 g++ 的我的最爱:-Wno-unused-local-typedefs或者 Visual Studio 的/wd4514

这个程序实际上包含了七个错误,但是如果您错过了它们,请不要担心。让我们一个一个来。

拼错

第 1 行将<iostream>拼错为<iosteam>。你的编译器应该给你一个简单的消息,通知你它找不到<iosteam>。编译器可能不知道您想要键入<iostream>,所以它不会给你任何建议。你必须知道标题名称的正确拼写。

大多数编译器在这一点上完全放弃了。如果您遇到这种情况,请修复这个错误,然后再次运行编译器以查看更多消息。

如果您的编译器试图继续,它会在没有来自拼错的头文件的声明的情况下继续。在这种情况下,<iostream>声明了std::cinstd::cout,因此编译器还会发出关于这些名称未知的消息,以及关于输入和输出操作符的其他错误消息。

虚假字符

最有趣的错误是在第 4 行中使用了方括号字符([)而不是大括号字符({)。一些编译器可能能够猜出您的意思,这可以限制产生的错误消息。其他人则不能,他们给出的信息可能相当隐晦。例如,g++ 发布了许多错误,但没有一个直接指向错误。相反,它会发出许多消息,从以下内容开始:

list0601.cpp:6:13: error: no match for 'operator<' (operand types are 'std::ostream' {aka 'std::basic_ostream<char>'} and 'const char [41]')
    6 |   std::cout < "This program prints a table of squares.\n";
      |   ~~~~~~~~~ ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      |        |      |
      |        |      const char [41]
      |        std::ostream {aka std::basic_ostream<char>}
In file included from /usr/include/c++/9/bits/stl_algobase.h:64,
                 from /usr/include/c++/9/bits/char_traits.h:39,
                 from /usr/include/c++/9/ios:40,
                 from /usr/include/c++/9/ostream:38,
                 from /usr/include/c++/9/iostream:39,
                 from list0601.cpp:2:
/usr/include/c++/9/bits/stl_pair.h:454:5: note: candidate: 'template<class _T1, class _T2> constexpr bool std::operator<(const std::pair<_T1, _T2>&, const std::pair<_T1, _T2>&)'
  454 |     operator<(const pair<_T1, _T2>& __x, const pair<_T1, _T2>& __y)
      |     ^~~~~~~~
/usr/include/c++/9/bits/stl_pair.h:454:5: note:   template argument deduction/substitution failed:
list0601.cpp:6:15: note:   'std::ostream' {aka 'std::basic_ostream<char>'} is not derived from 'const std::pair<_T1, _T2>'
    6 |   std::cout < "This program prints a table of squares.\n";
      |               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

当您无法理解错误消息时,请查看第一条消息及其标识的行号。搜索行号处或附近的错误。忽略其余的消息。

在其他行上,您可能会看到一两个错误。然而,在您修复它们之后,大量的消息仍然存在。这意味着您仍然没有找到真正的罪魁祸首(在第 4 行)。不同的编译器发出不同的消息。例如,clang++ 发出类似的消息,但是格式不同。

list0601.cpp:6:13: error: invalid operands to binary expression ('std::ostream' (aka 'basic_ostream<char>') and 'const char [41]')
  std::cout < "This program prints a table of squares.\n";
  ~~~~~~~~~ ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/lib64/gcc/x86_64-suse-linux/9/../../../../include/c++/9/system_error:208:3: note: candidate function not viable: no known conversion from 'std::ostream' (aka 'basic_ostream<char>') to 'const std::error_code' for 1st argument
  operator<(const error_code& __lhs, const error_code& __rhs) noexcept
^

一旦找到方括号并将其改为花括号,您可能会得到完全不同的消息。这是因为用[代替{会让编译器彻底糊涂,它无法理解程序的其余部分。纠正这个问题可以让编译器把程序理顺,但是现在它可能会发现一系列新的错误。

未知运算符

输入和输出操作符(>><<)和其他任何 C++ 操作符没有什么不同,比如加法(+)、乘法(*)或者比较(比如>)。每个运算符都有一组有限的允许操作数。例如,您不能“添加”两个 I/O 流(如std::cin + std::cout),也不能使用输出操作符将一个数字“写入”一个字符串(如"text" << 3)。

在第 5 行,一个错误是使用了<而不是<<。编译器无法确定您打算使用<<,而是发出一条消息指出<有什么问题。确切的消息取决于编译器,但最有可能的是,该消息并不能帮助您解决这个特殊的问题。

一旦修复了操作符,请注意编译器不会为另一个错误(即多余的分号)发出任何消息。严格来说,不是 C++ 错误。这是一个逻辑错误,但结果是一个有效的 C++ 程序。有些编译器会发出警告,建议您第 6 行什么也不做,这是您犯了一个错误的暗示。其他编译器会默默地接受程序。

发现这种错误的唯一可靠的方法是学会校对你的代码。

未知名称

编译器容易发现的一个错误是,当你使用了一个编译器根本无法识别的名字。在这种情况下,偶然键入字母l而不是分号会产生名称endl而不是end;。编译器会发出一条关于这个未知名称的明确消息。

修复分号,现在编译器报错另一个操作符。这一次,您应该能够放大问题,并注意到运算符面向错误的方向(<<而不是>>)。然而,编译器可能不会提供太多帮助。一个编译器产生如下形式的错误:

list0601.cxx
list0601.cxx(11) : error C2784: 'std::basic_ostream<char,_Traits> &std::operator
<<(std::basic_ostream<char,_Traits> &,unsigned char)' : could not deduce
template argument for 'std::basic_ostream<char,_Elem> &' from 'std::istream'
        C:\Program Files\Microsoft Visual C++ Toolkit 2003\include\ostream(887)
: see declaration of 'std::operator`<<''
list0601.cxx(11) : error C2784: 'std::basic_ostream<char,_Traits> &std::operator
<<(std::basic_ostream<char,_Traits> &,unsigned char)' : could not deduce
template argument for 'std::basic_ostream<char,_Elem> &' from 'std::istream'
        C:\Program Files\Microsoft Visual C++ Toolkit 2003\include\ostream(887)
: see declaration of 'std::operator`<<''

行号告诉你去哪里找,但是要靠你自己去找问题。

符号误差

但是现在你遇到了一个奇怪的问题。编译器抱怨说它不知道名字的意思(第 17 行的cout),但是你知道它的意思。毕竟剩下的程序使用std::cout没有任何困难。第 17 行有什么问题导致编译器忘记?

在 C++ 中,小错误会产生深远的后果。事实证明,单冒号和双冒号的意思完全不同。编译器将std:cout视为一个标记为std的语句,后跟一个简单的名字cout。至少错误消息会将您指向正确的位置。然后由您来决定是否注意到丢失的冒号。

错误中的乐趣

在您修复了所有的语法和语义错误之后,编译并运行程序,以确保您真正找到了它们。然后引入一些新的错误,看看会发生什么。以下是一些建议:

试着在一条语句的末尾去掉一个分号。会发生什么?



尝试从字符串的开头或结尾删除双引号。会发生什么?



试将 int 拼错为 iny 。会发生什么?



现在我要你自己去探索。一次引入一个错误,看看会发生什么。试着一次犯几个错误。有时,错误有办法掩盖彼此。去狂野吧!玩得开心!你的老师多久鼓励你犯一次错误?

现在是时候回到正确的 C++ 代码了。下一个探索引入了for循环。

七、更多循环

探索 2 和 3 展示了一些简单的while循环。这个探索引入了while循环的老大哥,for循环。

有界循环

您已经看到了while循环,它从标准输入读取数据,直到没有更多输入可用。这是一个典型的无界循环。除非您事先确切知道输入流将包含什么,否则您无法确定循环的界限或限制。有时你预先知道循环必须运行多少次;也就是说,你知道循环的边界,使它成为一个有界的循环。for循环是 C++ 实现有界循环的方式。

让我们从一个简单的例子开始。清单 7-1 显示了一个打印前十个非负整数的程序。

import <iostream>;

int main()
{
  for (int i{0}; i != 10; i = i + 1)
    std::cout << i << '\n';
}

Listing 7-1.Using a for Loop to Print Ten Non-negative Numbers

for循环在一个小空间里塞满了大量信息,所以一步一步来。括号内是循环的三个部分,用分号分隔。你觉得这三件是什么意思?





这三个部分是初始化、条件和后迭代。仔细看看每个部分。

初始化

第一部分看起来类似于变量定义。它定义了一个名为iint变量,初始值为 0。一些受 C 启发的语言只允许初始化表达式,而不允许变量定义。在 C++ 中,你有一个选择:表达式或定义。将循环控制变量定义为初始化的一部分的好处是,您不会意外地在循环之外引用该变量。在初始化部分定义循环控制变量的缺点是,您不能故意在循环之外引用该变量。清单 7-2 展示了限制回路控制变量的优势。

import <iostream>;

int main()
{
  for (int i{0}; i != 10; i = i + 1)
    std::cout << i << '\n';
  std::cout << "i=" << i << '\n';        // error: i is undefined outside the loop
}

Listing 7-2.You Cannot Use the Loop Control Variable Outside the Loop

限制循环控制变量的另一个后果是,您可能在多个循环中定义和使用相同的变量名,如清单 7-3 所示。

import <iostream>;

int main()
{
  std::cout << '+';
  for (int i{0}; i != 20; i = i + 1)
    std::cout << '-';
  std::cout << "+\n|";

  for (int i{0}; i != 3; i = i + 1)
    std::cout << ' ';
  std::cout << "Hello, reader!";

  for (int i{0}; i != 3; i = i + 1)
    std::cout << ' ';
  std::cout << "|\n+";

  for (int i{0}; i != 20; i = i + 1)
    std::cout << '-';
  std::cout << "+\n";
}

Listing 7-3.Using and Reusing a Loop Control Variable Name

清单 7-3 产生什么作为输出?




如果不必执行任何初始化,可以将初始化部分留空,但仍然需要分号来分隔空初始化和条件。

情况

中间部分遵循与while循环条件相同的规则。正如您所料,它控制着循环的执行。当条件为真时,循环体执行。如果条件为假,循环终止。如果循环第一次运行时条件为假,则循环体永远不会执行(但初始化部分总是执行)。

有时您会看到一个for循环,其中缺少一个条件。这意味着条件总是为真,所以循环会不停地运行。编写始终为真的条件的更好方法是显式地使用true作为条件。这样,任何将来必须阅读和维护您的代码的人都会明白,您故意将循环设计为永远运行。可以把它想象成一个注释:“这个条件故意留空白。”

后迭代

最后一部分看起来像一个语句,尽管它缺少结尾的分号。其实并不是完整的陈述,只是一种表达。表达式在循环体之后(因此命名为 post 迭代)和再次测试条件之前被求值。你可以把任何你想要的东西放在这里,或者留空。通常,for循环的这一部分控制迭代,根据需要推进循环控制变量。

一个for循环如何工作

控制流程如下:

  1. 初始化部分只运行一次。

  2. 测试条件。如果为 false,则循环终止,程序继续执行循环体后面的语句。

  3. 如果条件为真,则执行循环体。

  4. 后迭代部分执行。

  5. 控制跳转到 2。

轮到你了

现在轮到你写一个for循环了。清单 7-4 展示了一个 C++ 程序的框架。填写缺失的部分,计算整数之和 从 10 到 20,包括 10 和 20。

import <iostream>;

int main()
{
  int sum{0};

  // Write the loop here.

  std::cout << "Sum of 10 to 20 = " << sum << '\n';
}

Listing 7-4.Compute Sum of Integers from 10 to 20

在测试你的程序之前,你必须首先确定你如何知道程序是否正确。换句话说,10 到 20 的整数之和是多少,含 10 和 20?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

好,现在编译并运行你的程序。你的程序产生了什么答案?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 你的程序正确吗? _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

将您的程序与清单 7-5 中所示的程序进行比较。

import <iostream>;

int main()
{
  int sum{0};
  for (int i{10}; i != 21; i = i + 1)
    sum = sum + i;
  std::cout << "Sum of 10 to 20 = " << sum << '\n';
}

Listing 7-5.Compute Sum of Integers from 10 to 20 (Completed)

for循环的用途是格式化和打印信息表。要实现这一点,您需要比目前所学的更好地控制输出格式。这将是下一次探索的主题。

八、格式化输出

在 Exploration 4 中,您使用制表符整齐地排列输出。标签有用,但很粗糙。本文介绍了 C++ 提供的一些特性,可以很好地格式化输出,比如设置输出字段的对齐、填充和宽度。C++ 20 提供了两种非常不同的格式化输出的方法。本章介绍了这两种方式,您可以选择自己喜欢的方式。

问题

这种探索开始有点不同。你必须编写自己的程序来解决问题,而不是阅读一个程序并回答关于它的问题。任务是打印从 1 到 20 的整数的正方形和立方体(算术变体,而不是几何形状)的表格。程序的输出应该如下所示:

 N   N²    N³
 1     1      1
 2     4      8
 3     9     27
 4    16     64
 5    25    125
 6    36    216
 7    49    343
 8    64    512
 9    81    729
10   100   1000
11   121   1331
12   144   1728
13   169   2197
14   196   2744
15   225   3375
16   256   4096
17   289   4913
18   324   5832
19   361   6859
20   400   8000

为了帮助你开始,清单 8-1 给出了一个框架程序。你只需要填充循环体。

import <iomanip>;
import <iostream>;

int main()
{
  std::cout << " N   N²    N³\n";
  for (int i{1}; i != 21; ++i)
  {
    // write the loop body here
  }
}

Listing 8-1.Print a Table of Squares and Cubes

这是一个棘手的问题,所以如果你有困难,不要担心。这个练习的目的是演示格式化输出实际上有多难。如果你已经学到了那么多,即使你没有完成程序,你也成功地完成了这个练习。也许你一开始尝试过使用制表符,但那会使数字在左边对齐。

 N   N²   N³
 1   1     1
 2   4     8
 3   9     27
 4   16    64
 5   25    125
 6   36    216
 7   49    343
 8   64    512
 9   81    729
 10  100   1000

左对齐不是我们平时写数字的方式。传统上,数字应该右对齐(或者在小数点上对齐,如果适用的话——在本文后面的相关部分“对齐”中有更多的介绍)。右对齐的数字更容易阅读。

C++ 提供了一些简单但强大的技术来格式化输出。要格式化乘幂表,必须为每一列定义一个字段。字段具有宽度、填充字符和对齐方式。以下部分深入解释了这些概念。

字段宽度

在探索如何指定对齐方式之前,首先您必须知道如何设置输出字段的宽度。我在清单 8-1 中给了你提示。有什么暗示?


节目第一行是import <iomanip>;,你没见过。这个头声明了一些有用的工具,包括std::setw(),它设置输出字段的最小宽度。例如,要打印一个至少占据三个字符位置的数字,调用std::setw(3)。如果这个数字需要更多的空间,比如说 314159,那么实际的输出将会占用更多的空间。在这种情况下,间距变成了六个字符位置。

要使用setw,调用函数作为输出语句的一部分。该语句看起来像是在试图打印setw,但实际上什么都没有打印出来,您所做的只是操纵输出流的状态。这就是为什么setw被称为 I/O 机械手的原因。<iomanip>头声明了几个操纵器,您将在适当的时候了解到。

清单 8-2 显示了功率表程序,使用setw设置表中每个字段的宽度。

import <iomanip>;
import <iostream>;

int main()
{
  std::cout << " N   N²    N³\n";
  for (int i{1}; i != 21; ++i)
    std::cout << std::setw(2) << i
              << std::setw(6) << i*i
              << std::setw(7) << i*i*i
              << '\n';
}

Listing 8-2.Printing a Table of Powers the Right Way

表格的第一列需要两个位置,以容纳多达 20 的数字。第二列需要一些列与列之间的空间,以及最多容纳 400 个数字的空间;setw(6)N列之间使用三个空格,数字使用三个字符位置。最后一列也使用列间三个空格和四个字符位置,允许最多 8000 个数字。

默认的字段宽度是零,这意味着您打印的所有内容都会占用它所需的确切空间,不多也不少。

打印一个项目后,字段宽度自动重置为零。例如,如果您想对整个表使用统一的六列宽度,您不能调用一次setw(6)就让它保持不变。相反,您必须在每次输出操作之前调用setw(6),如下所示:

    std::cout << std::setw(6) << i
              << std::setw(6) << i*i
              << std::setw(6) << i*i*i
              << '\n';

填充字符

默认情况下,值用空格字符(' ')填充。您可以将填充字符设置为您选择的任何字符,例如零('0')或星号('*')。清单 8-3 展示了在打印支票的程序中两个填充字符的奇特用法。

import <iomanip>;
import <iostream>;

int main()
{
  using namespace std;

  int day{14};
  int month{3};
  int year{2006};
  int dollars{42};
  int cents{7};

  // Print date in USA order. Later in the book, you will learn how to
  // handle internationalization.
  cout << "Date: " << setfill('0') << setw(2) << month
                            << '/' << setw(2) << day
                            << '/' << setw(2) << year << '\n';
  cout << "Pay to the order of: CASH\n";
  cout << "The amount of $" << setfill('*') << setw(8) << dollars << '.'
                            << setfill('0') << setw(2) << cents << '\n';
}

Listing 8-3.Using Alternative Fill Characters

注意,与setw不同的是,setfill是粘性的。也就是说,输出流会记住填充字符,并将该字符用于所有输出字段,直到您设置了不同的填充字符。

标准前缀

清单 8-3 中的另一个新特性是声明using namespace std;。所有这些前缀有时会使代码难以阅读。名字的重要部分在混乱中消失了。通过用using namespace std;开始你的程序,你是在指示编译器把它不能识别的名字当作是以std::开始的。

如关键字所示,std被称为名称空间。几乎标准库中的每个名字都是std名称空间的一部分。不允许向std名称空间添加任何东西,任何第三方库供应商也不允许。因此,如果你看到std::,你就知道接下来的是标准库的一部分(所以你可以在任何可靠的参考资料中查找)。更重要的是,您知道您在自己的程序中发明的大多数名称不会与标准库中的任何名称冲突,反之亦然。名称空间将您的名字与标准库名分开。在本书的后面,您将学习创建自己的名称空间,这有助于组织库和管理大型应用程序。

另一方面,using namespace std;是一个危险的声明,我很少使用。如果没有在每个标准库名前面加上std::限定符,就会导致混乱。例如,想象一下,如果你的程序定义了一个名为coutsetw的变量。编译器有解释名字的严格规则,一点也不会混淆,但是人类读者肯定会混淆。不管有没有using namespace std;,最好避免与标准库中的名字冲突。

对齐

C++ 允许您将输出字段向左或向右对齐。如果你想集中一个数字,你只能靠自己。要强制向左或向右对齐,请使用leftright操纵器,包括<iostream>后可免费获得。(唯一需要<iomanip>的时候是当你想使用需要额外信息的操纵器,比如setwsetfill。)

默认的对齐方式是向右,这可能会让你觉得奇怪。毕竟,第一次尝试使用制表符来对齐表列会产生左对齐的值。然而,就 C++ 而言,它对你的表一无所知。对齐在字段内。setw操纵器指定宽度,对齐方式决定填充字符是添加在值之后(左对齐)还是值之前(右对齐)。输出流没有它之前可能打印过的其他值的记忆(比如在前一行)。因此,例如,如果您想要将一列数字按小数点对齐,您必须手动完成(或者确保列中的每个值在小数点后都有相同的位数)。

探索格式

现在您已经了解了格式化输出字段的基本知识,是时候稍微探索一下,帮助您全面理解字段宽度、填充字符和对齐方式是如何相互作用的。读取清单 8-4 中的程序并预测其输出。

import <iomanip>;
import <iostream>;

int main()
{
  using namespace std;
  cout << '|' << setfill('*') << setw(6) <<  1234 << '|' << '\n';
  cout << '|' << left <<         setw(6) <<  1234 << '|' << '\n';
  cout << '|' <<                 setw(6) << -1234 << '|' << '\n';
  cout << '|' << right <<        setw(6) << -1234 << '|' << '\n';
}

Listing 8-4.Exploring Field Width, Fill Character, and Alignment

您期望清单 8-4 的输出是什么?





现在编写一个程序,它将产生以下输出。不作弊,简单打印一长串。相反,只打印整数和换行符,并加入字段宽度、填充字符和对齐操作符,以获得期望的输出。

000042
420000
42
-42-

许多不同的程序可以实现相同的目标。我的程序,如清单 8-5 所示,只是许多可能性中的一种。

import <iomanip>;
import <iostream>;

int main()
{
  using namespace std;

  cout << setfill('0') << setw(6) << 42 << '\n';
  cout << left         << setw(6) << 42 << '\n';
  cout << 42 << '\n';
  cout << setfill('-') << setw(4) << -42 << '\n';
}

Listing 8-5.Program to Produce Formatted Output

接受参数的操纵器,如setwsetfill,在<iomanip>中声明。没有参数的操纵器,如leftright,在<iostream>中声明。如果你不记得了,把两个模块都包括进去。如果你包含一个你并不真正需要的模块,你不会注意到任何不同。

I’M LYING TO YOU

leftboolalpha机械手是<iostream>中声明的而不是。我骗了你。它们实际上是在<ios>中声明的。但是<iostream>包含<ios>,所以当你包含<iostream>时,你会自动获得<ios>中的所有内容。

我对你撒谎已经有一段时间了。输入运算符(>>)实际上是在<istream>中声明的,输出运算符(<<)是在<ostream>中声明的。与<ios>一样,<iostream>割台始终包括<istream><ostream>。因此,您可以包含<iostream>并获得典型输入和输出所需的所有头文件。其他的头,比如<iomanip>,不太常用,所以不是<iostream>的一部分。

所以我并没有真的对你撒谎,只是在等你接受事实。

替代语法

我喜欢使用操纵器,因为它们简洁、清晰、易于使用。您也可以使用点运算符(.)将函数应用于输出流对象。比如设置填充字符,可以调用std::cout.fill('*')fill函数被称为成员函数,因为它是输出流类型的成员。您不能将其应用于任何其他类型的对象。只有一些类型有成员函数,并且每个类型都定义了它所允许的成员函数。任何 C++ 库参考的很大一部分都与各种类型及其成员函数有关。(输出流的成员函数与输出操作符一起在<ostream>中声明。输入流的成员函数在<istream>中声明。当您导入<iostream>时,这两个模块都会自动导入。)

当设置粘性属性时,如填充字符或对齐,您可能更喜欢使用成员函数而不是操纵器。您还可以使用成员函数来查询当前填充字符、对齐和其他标志以及字段宽度,这是您无法使用操纵器完成的事情。

成员函数语法使用流对象、点(.)和函数调用,例如cout.fill('0')。设置对齐稍微复杂一点。清单 8-6 显示了与清单 8-5 相同的程序,但是使用了成员函数而不是操纵器。

import <iostream>;

int main()
{
  using namespace std;

  cout.fill('0');
  cout.width(6);
  cout << 42 << '\n';
  cout.setf(ios_base::left, ios_base::adjustfield);
  cout.width(6);
  cout << 42 << '\n';
  cout << 42 << '\n';
  cout.fill('-');
  cout.width(4);
  cout << -42 << '\n';
}

Listing 8-6.A Copy of Listing 8-5, but Using Member Functions

要查询当前填充字符,调用cout.fill()。这与您用来设置填充字符的函数名相同,但是当您不带参数调用该函数时,它会返回当前的填充字符。类似地,cout.width()返回当前字段宽度。获取标志略有不同。您调用setf来设置标志,例如对齐,但是您调用flags()来返回当前标志。此时细节并不重要,但是如果你很好奇,可以参考任何相关的库参考资料。

独立地

现在是你从头开始写程序的时候了。请随意查看其他程序,以确保您拥有所有必要的部分。编写这个程序来生成一个从 1 到 10(包括 1 和 10)的乘法表,如下所示:

   *|   1   2   3   4   5   6   7   8   9  10
----+----------------------------------------
   1|   1   2   3   4   5   6   7   8   9  10
   2|   2   4   6   8  10  12  14  16  18  20
   3|   3   6   9  12  15  18  21  24  27  30
   4|   4   8  12  16  20  24  28  32  36  40
   5|   5  10  15  20  25  30  35  40  45  50
   6|   6  12  18  24  30  36  42  48  54  60
   7|   7  14  21  28  35  42  49  56  63  70
   8|   8  16  24  32  40  48  56  64  72  80
   9|   9  18  27  36  45  54  63  72  81  90
  10|  10  20  30  40  50  60  70  80  90 100

在您完成您的程序并确保它产生正确的输出后,将您的程序与我的程序进行比较,如清单 8-7 所示。

import <iomanip>;
import <iostream>;

int main()
{
  using namespace std;

  int constexpr low{1};        ///< Minimum value for the table
  int constexpr high{10};      ///< Maximum value for the table
  int constexpr colwidth{4};   ///< Fixed width for all columns

  // All numbers must be right-aligned.
  cout << right;

  // First print the header.
  cout << setw(colwidth) << '*'
       << '|';
  for (int i{low}; i <= high; i = i + 1)
    cout << setw(colwidth) << i;
  cout << '\n';

  // Print the table rule by using the fill character.
  cout << setfill('-')
       << setw(colwidth) << ""                    // one column's worth of "-"
       << '+'                                     // the vert. & horz. intersection
       << setw((high-low+1) * colwidth) << ""     // the rest of the line
       << '\n';

  // Reset the fill character.
  cout << setfill(' ');

  // For each row...
  for (int row{low}; row <= high; row = row + 1)
  {
    cout << setw(colwidth) << row << '|';
    // Print all the columns.
    for (int col{low}; col <= high; col = col + 1)
      cout << setw(colwidth) << row * col;
    cout << '\n';
  }
}

Listing 8-7.Printing a Multiplication Table

我猜你写的程序和我写的有一点不同,或者你写的很不一样。没关系。最有可能的是,您为表格规则使用了一个硬编码的字符串(分隔标题和表格的线),或者您使用了一个for循环。我使用了 I/O 格式,只是为了向您展示什么是可能的。打印具有非零字段宽度的空字符串是打印单个字符的重复的一种快速而简单的方法。

另一个我额外增加的新特性是constexpr关键字。在定义中使用该关键字将对象定义为常量而不是变量。编译器确保您不会意外地将任何内容赋给该对象。如你所知,命名常量比在源代码中添加数字更容易阅读和理解。

format功能

C++ 20 中的新功能是format()函数,它的工作方式类似于 Python 的format()函数。第一个参数是格式字符串,后续参数是要格式化的值。该函数知道如何格式化所有内置类型,以及标准库中的类型,并且您可以为您的自定义类型定义格式化程序。

在格式字符串中,每个要格式化的值或字段由一组花括号指定。大括号外的文本被逐字复制。格式字符串后跟要作为附加函数参数打印的值。例如,

std::format("x: {}, y: {}, z: {}\n", 10, 20, 30)

返回以下字符串:

x: 10, y: 20, z: 30

您也可以从零开始给字段编号。如果格式字符串中的顺序与参数的顺序不匹配,这将非常有用。例如,

std::format("x: {2}, y: {1}, z: {0}\n", 10, 20, 30)

返回以下字符串:

x: 30, y: 20, z: 10

不要在一个格式字符串中混合编号和未编号的字段。

为了更好地控制格式,请在冒号后指定格式细节。细节取决于参数的类型。对于标准类型,格式说明符由以下部分组成。所有部分都是可选的,但如果有,必须按以下顺序排列:

fill-and-align sign # 0 width type

说明符(如果有)以可选的填充和对齐开始。开始调整时对准为'<',居中调整时对准为'^',结束调整时对准为'>'。填充字符可以是除'{''}'之外的任何字符。如果指定填充字符,还必须指定对齐字符。默认情况下,数字是结束调整的,其他类型是开始调整的。

从左向右阅读的语言将开始调整的字段在左边对齐,结束调整的字段在右边对齐。从右向左阅读的语言会颠倒顺序,因此开始调整的字段靠右对齐,结束调整的字段靠左对齐。

在填充和对齐之后是一个可选符号:'+'为所有数字发出一个符号,'-'只为负数发出一个符号,或者一个空格为负数发出一个符号,一个空格为其他值发出一个空白。默认是'-'

接下来是一个可选的'#'字符,用于请求另一种形式,比如一个基本前缀(0x表示十六进制,0b表示二进制,等等。).

接下来是字段宽度。如果不使用填充和对齐方式,可以使用“0”字符开始字段宽度,这将使用默认对齐方式,并在符号和基线后填充“0”字符。还可以通过嵌套一组花括号和一个可选的参数编号来代替字段宽度,从而将参数用作字段宽度。

最后,可选的类型字母进一步控制格式。对于整数,可以用'b'表示二进制输出,'d'表示十进制,'o'表示八进制,'x'表示十六进制,或者'c'将值格式化为等价字符。字符的默认值是'c',整数的默认值是'd'。举个例子,

std::format("'{0:c}': {0:#04x} {0:0>#10b} |{0:{1}d}| {2:s}\n", '*', 4, "str")

返回以下字符串:

'*': 0x2a 0b00101010 |  42| str

完整的规则稍微复杂一些,但是这应该足够让你开始了。使用 std::format() 功能重写清单。务必将<iomanip>模块更改为<format>。在清单 8-8 中比较你的程序和我的程序。

import <format>;
import <iostream>;

int main()
{
  int constexpr low{1};        ///< Minimum value for the table
  int constexpr high{10};      ///< Maximum value for the table
  int constexpr colwidth{4};   ///< Fixed width for all columns

  // First print the header.
  std::cout << std::format("{1:>{0}c}|", colwidth, '*');
  for (int i{low}; i <= high; i = i + 1)
    std::cout << std::format("{1:{0}}", colwidth, i);
  std::cout << '\n';

  // Print the table rule by using the fill character.
  std::cout << std::format("{2:->{0}}+{2:->{1}}\n",
       colwidth, (high-low+1) * colwidth, "");

  // For each row...
  for (int row{low}; row <= high; row = row + 1)
  {
    std::cout << std::format("{1:{0}}|", colwidth, row);
    // Print all the columns.
    for (int col{low}; col <= high; col = col + 1)
      std::cout << std::format("{1:{0}}", colwidth, row * col);
    std::cout << '\n';
  }
}

Listing 8-8.Printing a Multiplication Table Using the format Function

格式字符串是紧凑的,但也可能是隐晦的。选择你喜欢的风格,并在你的代码中统一使用。即使你更喜欢format(),也要准备好阅读你那部分使用更冗长风格的代码,因为那是出现在数百万行现有 C++ 代码中的内容。

无论你如何格式化输出,循环都是你的朋友。想到循环,你首先想到的是什么数据结构?我希望您选择了数组,因为这是下一篇文章的主题。**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值