Skip to content

Latest commit

 

History

History
1812 lines (1311 loc) · 98.5 KB

File metadata and controls

1812 lines (1311 loc) · 98.5 KB

六、字符串、流类和正则表达式

我们将在本章介绍以下食谱:

  • 创建、连接和转换字符串
  • 修剪字符串开头和结尾的空白
  • 获得std::string的舒适性,无需花费构建std::string对象的成本
  • 从用户输入中读取值
  • 计算文件中的所有单词
  • 用输入输出流操纵器格式化您的输出
  • 从文件输入初始化复杂对象
  • std::istream迭代器填充容器
  • 使用std::ostream迭代器进行通用打印
  • 将输出重定向到特定代码段的文件
  • std::char_traits继承创建自定义字符串类
  • 使用正则表达式库标记输入
  • 根据上下文动态打印不同的数字,非常舒服
  • std::iostream错误中捕捉可读异常

介绍

本章专门讨论任意数据的字符串处理、解析和打印。对于此类作业,STL 提供其输入/输出流库。该库基本上由以下类组成,每个类都用灰色框表示:

箭头显示了类的继承方案。这在一开始可能看起来很难,但是我们将在本章中使用这些类的大部分,并逐个类地熟悉它们。在查看 C++ STL 文档中的那些类时,我们不会直接用这些确切的名称找到它们。这是因为图中的名称是我们作为应用程序员看到的,但它们实际上大多只是带有basic_类名前缀的类的类型定义(例如,我们将更容易在 STL 文档中搜索basic_istream而不是istream)。basic_*输入输出流类是可以专门用于不同字符类型的模板。图中的类专门研究char值。我们将在整本书中使用这些专门化。如果我们在这些类名前面加上w字符,我们会得到wistreamwostream等等——例如,这些是wchar_t而不是char的专门化类型定义。

在图的顶部,我们看到std::ios_base。我们基本上不会直接使用它,但它是为了完整性而列出的,因为所有其他类都从它继承。下一个特殊化是std::ios,它体现了一个维护数据流的对象的思想,可以处于好的状态,运行空的数据状态(EOF),或者某种失败的状态。

我们将实际使用的第一个专门化是std::istreamstd::ostream"i""o"前缀代表输入和输出。在我们最早的 C++ 编程时代,我们已经在最简单的例子中以对象std::coutstd::cin(但也是std::cerr)的形式看到了它们。这些是这些类的实例,它们总是全局可用的。我们通过ostream进行数据输出,通过istream进行输入。

istreamostream继承的一个类是iostream。它结合了输入和输出能力。当我们理解了由istreamostreamiostream组成的三个组中的所有类都可以使用时,我们基本上也准备好立即使用以下所有类:

ifstreamofstreamfstream分别继承自istreamostreamiostream,但提升了它们将输入/输出从计算机的文件系统重定向到文件的能力。

istringstreamostringstreamiostringstream的工作原理非常相似。它们有助于在内存中构建字符串,和/或从中消费数据。

创建、连接和转换字符串

即使是很久以前的 C++ 程序员也会知道std::string。虽然字符串处理在 C 语言中既繁琐又痛苦,尤其是在解析、连接、复制等方面,std::string在简单性和安全性方面迈出了真正的一步。

多亏了 C++ 11,当我们想要将所有权转移到其他函数或数据结构时,我们甚至不再需要复制字符串,因为我们可以移动它们。这样,在大多数情况下不会有太多开销。

在过去的几个标准增量中,std::string到处都有一些新特性。C++ 17 中全新的是std::string_view。我们将两者都玩一会儿(但还有另一个食谱,它更专注于std::string_view-唯一的特性),以了解它们以及它们在 C++ 17 时代是如何工作的。

怎么做...

在本节中,我们将创建字符串和字符串视图,并使用它们进行基本的连接和转换:

  1. 像往常一样,我们首先包含头文件,并声明我们使用std命名空间:
      #include <iostream>
      #include <string>
      #include <string_view>
      #include <sstream>
      #include <algorithm>      

      using namespace std;
  1. 让我们首先创建字符串对象。最明显的方法是实例化类string的对象a。我们通过给构造函数一个 C 风格的字符串来控制它的内容(它将在编译后作为包含字符的静态数组嵌入到二进制文件中)。构造函数将复制它,并将其作为字符串对象a的内容。或者,我们可以使用字符串文字运算符""s,而不是从 C 风格的字符串初始化它。它会动态创建一个字符串对象。用它来构造对象b,我们甚至可以使用自动类型演绎:
      int main()
      {
          string a { "a"  };
          auto   b ( "b"s );
  1. 我们刚刚创建的字符串是将它们的输入从构造函数参数复制到自己的缓冲区中。为了不抄袭,而是引用底层字符串,我们可以使用string_view实例。这个类确实也有一个文字运算符,叫做""sv:
          string_view c { "c"   };
          auto        d ( "d"sv );
  1. 好了,现在让我们玩一下我们的弦乐和弦乐视图。对于这两种类型,std::ostream类都有operator<<重载,因此可以轻松打印:
          cout << a << ", " << b << 'n';
          cout << c << ", " << d << 'n';
  1. 字符串类重载operator+,所以我们可以添加两个字符串,并得到它们的串联结果。这样,"a" + "b"就产生了"ab"。以这种方式连接ab很容易。有了ac,就没那么容易了,因为 c 不是一个string,而是一个string_view。我们必须先从c中取出字符串,这可以通过从c中构造一个新的字符串,然后将其添加到a中来完成。在这一点上,人们可能会问,“等等,你为什么要把c复制到一个中间字符串对象中,只是为了把它添加到a中?你不能用c.data()来避免那个副本吗?”这是一个不错的想法,但它有一个缺陷- string_view实例不必携带以零结尾的字符串。这是一个可能导致缓冲区溢出的问题:
          cout << a + b << 'n';
          cout << a + string{c} << 'n';
  1. 让我们创建一个新的字符串,它包含我们刚刚创建的所有字符串和字符串视图。通过使用std::ostringstream,我们可以任何变量打印到行为与std::cout完全相同的流对象中,但是它不会打印到 shell 中。相反,它会打印到一个字符串缓冲区中。在我们使用operator<<对所有变量进行流处理并在它们之间留有一些分隔空间之后,我们可以使用o.str()从这些变量中构造并打印一个新的字符串对象:
          ostringstream o;

          o << a << " " << b << " " << c << " " << d;
          auto concatenated (o.str());
          cout << concatenated << 'n';
  1. 例如,我们现在还可以通过将新字符串的所有字母转换为大写来转换它。将小写字符映射到大写字符并保持其他字符不变的 C 库函数toupper已经可用,并且可以与std::transform组合,因为字符串基本上也是带有char项的可迭代容器对象:
          transform(begin(concatenated), end(concatenated), 
                    begin(concatenated), ::toupper);
          cout << concatenated << 'n';
      }
  1. 编译和运行程序会产生以下输出,这正是我们所期望的:
      $ ./creating_strings 
      a, b
      c, d
      ab
      ac
      a b c d
      A B C D

它是如何工作的...

显然,字符串可以像数字一样用+运算符相加,但这与数学无关,而是导致串联字符串。为了和string_view混合,我们需要先转换到std::string

但是,真正需要注意的是,在代码中混合字符串和字符串视图时,我们绝不能假设一个string_view后面的底层字符串是零终止!这就是为什么我们宁愿写"abc"s + string{some_string_view}而不写"abc"s + some_string_view.data()。除此之外,std::string提供了一个成员函数,append,它可以处理string_view实例,但是它改变了字符串,而不是返回一个附加了字符串视图内容的新字符串。

std::string_view is useful, but be cautious when mixing it with strings and string functions. We cannot assume that they are zero-terminated, which breaks things quickly in a standard string environment. Fortunately, there are often proper function overloads, which can deal with them the right way.

如果我们想通过格式化等方式进行复杂的字符串连接,那么我们不应该在字符串实例上逐个进行。std::stringstreamstd::ostringstreamstd::istringstream类更适合这种情况,因为它们在追加时增强了内存管理,并提供了我们通常从流中知道的所有格式化特性。std::ostringstream类是我们在本节中选择的,因为我们要创建一个字符串,而不是解析它。一个std::istringstream实例可以从一个现有的字符串实例化,然后我们可以轻松地解析成其他类型的变量。如果要两者结合,std::stringstream就是完美的全才。

修剪字符串开头和结尾的空白

特别是当从用户输入中获取字符串时,它们经常被不需要的空白所污染。在另一个方法中,我们删除了单词之间多余的空白。

现在让我们来看看被空格包围的字符串,并去掉它。std::string有一些很好的助手功能来完成这项工作。

After reading this recipe that shows how to do this with plain string objects, make sure to also read the following recipe. There we will see how to avoid unnecessary copies or data modifications with the new std::string_view class.

怎么做...

在这一节中,我们将编写一个助手函数来识别字符串中周围的空白,并返回一个没有空白的副本,然后我们将简单地测试它。

  1. 和往常一样,头包含和使用指令排在第一位:
      #include <iostream>
      #include <string>
      #include <algorithm>
      #include <cctype>

      using namespace std;
  1. 我们修剪字符串周围空白的函数对现有字符串进行常量引用。它将返回一个没有任何周围空白的新字符串:
      string trim_whitespace_surrounding(const string &s)
      {
  1. std::string提供了两个方便的功能,对我们帮助很大。第一个是string::find_first_not_of,它接受一个包含我们想要跳过的所有字符的字符串。当然,这是空白,意思是字符空间' '、制表符't'和新行'n'。它返回给我们第一个非空白字符位置。如果字符串中只有空格,则返回string::npos。这意味着,如果我们从中删除空白,就只剩下一个空字符串。因此,在这种情况下,让我们返回一个空字符串:
          const char whitespace[] {" tn"};
          const size_t first (s.find_first_not_of(whitespace));
          if (string::npos == first) { return {}; }
  1. 我们现在知道新字符串必须从哪里开始,但我们还不知道它必须从哪里结束。因此,我们使用另一个方便的字符串函数string::find_last_not_of。它将返回字符串中最后一个没有空格的字符位置:
          const size_t last (s.find_last_not_of(whitespace));
  1. 使用string::substr,我们现在可以返回字符串中被空格包围但没有空格的部分。该函数采用两个参数——字符串中第一个位置和该位置后面的字符数:
          return s.substr(first, (last - first + 1));
      }
  1. 就这样。让我们编写一个主函数,在这个函数中,我们创建一个字符串,用各种各样的空白包围一个文本句子,以便对它进行修剪:
      int main()
      {
          string s {" tn string surrounded by ugly"
                    " whitespace tn "};
  1. 我们打印字符串的未修剪和修剪版本。通过用括号将字符串括起来,可以更明显地看出在修剪之前哪些空白属于它:
          cout << "{" << s << "}n";
          cout << "{" 
               << trim_whitespace_surrounding(s) 
               << "}n";
      }
  1. 编译和运行程序会产生我们期望的输出:
      $ ./trim_whitespace 
      {  
        string surrounded by ugly whitespace    
         }
      {string surrounded by ugly whitespace}

它是如何工作的...

在本节中,我们使用了string::find_first_not_ofstring::find_last_not_of。这两个函数都接受一个 C 风格的字符串,该字符串作为搜索不同字符时应该跳过的字符列表。如果我们有一个携带字符串的字符串实例,"foo bar",我们在上面调用find_first_not_of("bfo "),它将返回值5,因为'a'字符是第一个不在"bfo "字符串中的字符。参数字符串中字符的顺序并不重要。

同样的功能存在于反向逻辑中,尽管我们在本食谱中没有使用它们:string::find_first_ofstring::find_last_of

类似于基于迭代器的函数,我们需要检查这些函数是否返回了字符串中的实际位置或表示它们没有而不是找到满足约束的字符位置的值。如果没有找到,他们会返回string::npos

根据我们在助手函数中从这些函数中检索到的字符位置,我们使用string::substring构建了一个不带空格的子字符串。这个函数接受一个相对偏移量和一个字符串长度,然后用自己的内存返回一个新的字符串实例,它只包含那个子字符串。比如string{"abcdef"}.substr(2, 2)会给我们返回一个新的字符串"cd"

无需构造 std::string 对象即可获得 std::string 的舒适性

std::string类是一个非常有用的类,因为它大大简化了字符串的处理。一个缺陷是,如果我们想要传递子串,我们需要传递一个指针和一个长度变量,两个迭代器,或者子串的一个副本。我们在前面的方法中做到了这一点,我们通过获取不包含周围空白的子字符串范围的副本来移除字符串周围的空白。

如果我们想把一个字符串或者一个子字符串传递给一个甚至不支持std::string的库,我们只能提供一个原始的字符串指针,这有点令人失望,因为这让我们回到了过去的 C 天。就像子串问题一样,原始指针不携带关于字符串长度的信息。这样,就必须实现一组指针和一个字符串长度。

简单来说,这正是std::string_view的含义。它从 C++ 17 开始可用,并提供了一种将指向某个字符串的指针与该字符串的大小配对的方法。它体现了为数据数组提供引用类型的思想。

如果我们设计的函数以前接受std::string实例作为参数,但没有以要求字符串实例重新分配保存实际字符串有效负载的内存的方式改变它们,我们现在可以使用std::string_view并与 STL 不可知的库更兼容。我们可以让其他库提供其复杂字符串实现背后的有效负载字符串的string_view视图,然后在我们的 STL 代码中使用它。这样,string_view类就充当了一个最小且有用的接口,可以在不同的库之间共享。

另一个很酷的地方是string_view可以作为较大字符串对象的子串的非复制引用。有很多种利用它获利的可能性。这一节我们就来玩玩string_view来感受一下它的起伏。我们还将看到如何通过调整字符串视图来隐藏字符串周围的空白,而不是修改或复制实际的字符串。这种方法避免了不必要的复制或数据修改。

怎么做...

我们将实现一个依赖于某些string_view特性的函数,然后,我们看到我们可以向其中输入多少种不同的类型:

  1. 标题包括和使用指令优先:
      #include <iostream>
      #include <string_view>

      using namespace std;
  1. 我们实现了一个接受string_view作为唯一参数的函数:
      void print(string_view v)
      {
  1. 在对输入字符串进行任何操作之前,我们会删除任何前导和尾随空格。我们不打算更改字符串,而是通过将字符串缩小到字符串的实际非空白部分来查看字符串上的*。find_first_not_of函数将查找字符串中的第一个字符,该字符不是空格(' '),不是制表符('t'),也不是换行符('n')。使用remove_prefix,我们将内部string_view指针前进到第一个非空白字符。如果字符串只包含空格,find_first_not_of函数返回值npos,即size_type(-1)。由于size_type是一个无符号变量,这可以归结为一个非常大的数字。因此,我们取两者中较小的一个:words_begin或字符串视图的大小:*
          const auto words_begin (v.find_first_not_of(" tn"));
          v.remove_prefix(min(words_begin, v.size()));
  1. 我们对尾随空白做同样的事情。remove_suffix缩小视图的大小变量:
          const auto words_end (v.find_last_not_of(" tn"));
          if (words_end != string_view::npos) {
              v.remove_suffix(v.size() - words_end - 1);
          }
  1. 现在我们可以打印字符串视图及其长度:
          cout << "length: " << v.length()
               << " [" << v << "]n";
      }
  1. 在我们的主函数中,我们通过向新的print函数提供完全不同的参数类型来玩转它。首先,我们从argv指针给它一个运行时char*字符串。在运行时,它包含我们的可执行文件的文件名。然后,我们给它一个空的string_view实例。然后,我们用一个 C 风格的静态字符串和一个""sv文字来填充它,这就动态地为我们构建了一个string_view。最后,我们给它一个std::string。好的一点是,这些参数都没有为了调用print函数而被修改或复制。不会发生堆分配。对于许多和/或大型字符串,这非常有效:
      int main(int argc, char *argv[])
      {
          print(argv[0]);
          print({});
          print("a const char * array");
          print("an std::string_view literal"sv);
          print("an std::string instance"s);
  1. 我们没有测试空白删除功能。所以,让我们给它一个有很多前导和尾随空格的字符串:
          print(" tn foobar n t ");
  1. 另一个很酷的特点是,字符串string_view让我们可以访问不一定是零终止。如果我们构造一个没有尾随零的字符串,如"abc",则print函数仍然可以安全地处理它,因为string_view还携带它所指向的字符串的大小:
          char cstr[] {'a', 'b', 'c'};
          print(string_view(cstr, sizeof(cstr)));
      }
  1. 编译并运行程序会产生以下输出。所有的字符串都被正确处理。我们填充了大量前导和尾随空白的字符串被正确过滤,没有零终止的abc字符串也被正确打印,没有任何缓冲区溢出:
      $ ./string_view 
      length: 17 [./string_view]
      length: 0 []
      length: 20 [a const char * array]
      length: 27 [an std::string_view literal]
      length: 23 [an std::string instance]
      length: 6 [foobar]
      length: 3 [abc]

它是如何工作的...

我们刚刚看到,我们可以调用一个接受string_view参数的函数,该参数基本上是字符串形式的,因为它以连续的方式存储字符。在我们的任何print通话中,都没有复制基础字符串

有趣的是,在我们的print(argv[0])调用中,字符串视图自动确定了字符串长度,因为按照惯例,这是一个以零结尾的字符串。反过来,我们不能假设通过计数项目的数量直到到达零终止符,可以确定string_view实例的数据长度。正因为如此,我们必须始终小心使用string_view::data()将指针指向字符串视图数据的位置。通常的字符串函数大多假设零终止,因此,对于指向字符串视图有效负载的原始指针,缓冲区溢出会非常严重。使用已经期望字符串视图的接口总是更好。

除此之外,我们已经从std::string获得了很多我们知道的豪华界面。

Use std::string_view for passing strings or substrings where you want to avoid copies or heap allocations, without losing the comfort of string classes. But be aware of the fact that std::string_view drops the assumption that strings are zero terminated.

从用户输入中读取值

这本书里的很多食谱从一个输入源读取值,比如标准输入或者一个文件,然后用它做一些事情。这一次,我们只专注于阅读,并了解更多关于错误处理的知识,如果从流中阅读一些东西并没有很好地进行,并且我们需要处理它,而不是终止整个程序,这一点就变得很重要。

在这个食谱中,我们将只从用户输入中读取,但是一旦我们知道如何做到这一点,我们也知道如何从任何其他流中读取。用户输入通过std::cin读取,这本质上是一个输入流对象,例如ifstreamistringstream的实例。

怎么做...

在本节中,我们将把用户输入读入不同的变量,并了解如何处理错误,以及如何将输入进行更复杂的标记化,使之成为有用的块:

  1. 这次我们只需要iostream了。因此,让我们包含这个单独的头,并声明我们默认使用std命名空间:
      #include <iostream>

      using namespace std;
  1. 让我们首先提示用户输入两个数字。我们将把它们解析成一个int和一个double变量。用户可以用空格将它们分开。例如,1 2.3是一个有效的输入:
      int main()
      {
          cout << "Please Enter two numbers:n> ";
          int x;
          double y;
  1. 解析和错误检查在我们的if分支的条件部分同时完成。只有当这两个数字都能被解析时,它们对我们才有意义,我们才会打印它们:
          if (cin >> x >> y) {
              cout << "You entered: " << x 
                   << " and " << y << 'n';
  1. 如果解析由于任何原因没有成功,我们会告诉用户解析进行得不顺利。cin流对象现在处于失败状态,并且在我们再次清除失败状态之前不会给我们其他输入。为了以后能够解析一个新的输入,我们调用cin.clear()并丢弃到目前为止收到的所有输入。删除是通过cin.ignore完成的,在这里我们指定删除最大数量的字符,直到我们最终看到一个换行符,这个换行符也被删除。之后的一切又是有趣的输入:
          } else {
              cout << "Oh no, that did not go well!n";
              cin.clear();
              cin.ignore(
                  std::numeric_limits<std::streamsize>::max(),
                  'n');
          }
  1. 现在让我们请求一些其他的输入。我们让用户输入姓名。由于名称可以由多个由空格分隔的单词组成,空格字符不再是一个好的分隔符。因此,我们使用std::getline,它接受一个流对象,如cin,一个它将输入复制到其中的字符串引用,以及一个分隔字符。让我们选择逗号(,)作为分隔字符。通过不仅仅单独使用cin和使用cin >> ws作为getline的流参数,我们可以让cin去掉任何名称前的任何前导空格。在每个循环步骤中,我们打印当前名称,但是如果名称为空,我们将退出循环:
          cout << "now please enter some "
                  "comma-separated names:n> ";

          for (string s; getline(cin >> ws, s, ',');) {
              if (s.empty()) { break; }
              cout << "name: "" << s << ""n";
          }
      }
  1. 编译和运行程序会得到下面的输出,其中我们假设只输入有效的输入。数字是"1 2",解析正确,然后我们输入一些名字,然后也正确列出。以两个连续逗号形式输入的空名称退出循环:
      $ ./strings_from_user_input 
      Please Enter two numbers:
      > 1 2
      You entered: 1 and 2
      now please enter some comma-separated names:
      > john doe,  ellen ripley,       alice,    chuck norris,,
      name: "john doe"
      name: "ellen ripley"
      name: "alice"
      name: "chuck norris"
  1. 当再次运行程序时,在开始输入错误的数字时,我们看到程序正确地接受了另一个分支,丢弃了错误的输入,并正确地继续使用名称侦听。摆弄一下cin.clear()cin.ignore(...)线,看看这是如何篡改姓名读码的:
      $ ./strings_from_user_input
      Please Enter two numbers:
      > a b
      Oh no, that did not go well!
      now please enter some comma-separated names:
      > bud spencer, terence hill,,
      name: "bud spencer"
      name: "terence hill"

它是如何工作的...

我们在这一部分做了一些复杂的输入检索。首先值得注意的是,我们总是同时进行检索和错误检查。

表达式cin >> x的结果再次引用了cin。这样,我们就可以写出cin >> x >> y >> z >> ...。同时,可以在if条件等布尔上下文中使用,将其转换为布尔值。布尔值告诉我们最后一次读取是否成功。这就是为什么我们能够写if (cin >> x >> y) {...}

例如,如果我们试图读取一个整数,但是输入包含"foobar"作为下一个标记,那么将它解析成整数是不可能的,并且流对象进入失败状态。这只是解析尝试的关键,而不是整个程序。可以先重置它,然后再尝试其他东西。在我们的食谱程序中,我们试图在读取两个数字的尝试可能失败后读取一个名字列表。在尝试读取这些数字失败的情况下,我们使用cin.clear()cin恢复到工作状态。但是,它的内部光标仍然在我们键入的内容上,而不是数字上。为了删除这个旧的输入并清除名称输入的管道,我们使用了非常长的表达式cin.ignore(std::numeric_limits<std::streamsize>::max(), 'n');。这对于清除缓冲区中的任何内容都是必要的,因为当我们向用户请求名称列表时,我们希望从一个真正新鲜的缓冲区开始。

以下循环一开始可能看起来也很奇怪:

for (string s; getline(cin >> ws, s, ',');) { ... }

for循环的条件部分,我们使用getlinegetline函数接受输入流对象、作为输出参数的字符串引用和分隔符。默认情况下,分隔符是换行符。在这里,我们将其定义为逗号(,)字符,因此列表中的所有名称,如"john, carl, frank",都是单独读取的。

目前为止,一切顺利。但是提供cin >> ws功能作为流对象意味着什么呢?这使得cin首先刷新所有空格,这些空格位于下一个非空格字符之前和最后一个逗号之后。回顾"john, carl, frank"的例子,我们会得到子字符串"john"" carl"" frank",而不使用ws。注意carlfrank不必要的前导空格字符?由于我们对输入流的ws预处理,这些实际上消失了。

计算文件中的所有单词

假设我们读了一个文本文件,我们想计算文本中的字数。我们定义一个单词是空白字符之间的字符范围。我们怎么做?

例如,我们可以计算空格的数量,因为单词之间必须有空格。在句子"John has a funny little dog."中,我们有五个空格字符,所以我们可以说有六个单词。

如果我们有一个有空白噪音的句子,比如" John has t anfunny little dog ."?这个字符串中有太多不必要的空格,甚至不仅仅是空格。从这本书的其他食谱中,我们已经学会了如何去除这些多余的空白。因此,我们可以先将字符串预处理成正常的句子形式,然后应用计算空格字符的策略。是的,这是可行的,但是有一个更简单的方法。为什么我们不应该使用 STL 已经提供给我们的东西呢?

除了为这个问题找到一个优雅的解决方案之外,我们将让用户选择是从标准输入还是文本文件中计算单词。

怎么做...

在本节中,我们将编写一个单行函数来计算输入缓冲区的字数,并让用户选择输入缓冲区的读取位置:

  1. 让我们首先包含所有必要的头,并声明我们使用std命名空间:
      #include <iostream>
      #include <fstream>
      #include <string>
      #include <algorithm>
      #include <iterator>      

      using namespace std;
  1. 我们的wordcount函数接受输入流,例如cin。它创建了一个std::input_iterator迭代器,标记出流中的字符串,然后将它们提供给std::distancedistance参数接受两个迭代器作为参数,并试图确定从一个迭代器位置到另一个位置需要多少递增步骤。对于随机访问迭代器,这很简单,因为它们实现了数学差分运算(operator-)。这样的迭代器可以像指针一样相互相减。然而,一个istream_iterator是一个向前迭代器,并且必须被推进,直到它等于结束迭代器。最终,需要的步骤数就是单词数:
      template <typename T>
      size_t wordcount(T &is)
      {
          return distance(istream_iterator<string>{is}, {});
      }
  1. 在我们的主功能中,我们让用户选择输入流是std::cin还是输入文件:
      int main(int argc, char **argv)
      {
          size_t wc;
  1. 如果用户在 shell 中与一个文件名(如$ ./count_all_words some_textfile.txt)一起启动程序,那么我们从argv命令行参数数组中获取该文件名并打开它,以便将新的输入文件流送入wordcount:
          if (argc == 2) {
              ifstream ifs {argv[1]};
              wc = wordcount(ifs);
  1. 如果用户在没有任何参数的情况下启动程序,我们假设输入来自标准输入:
          } else {
              wc = wordcount(cin);
          }
  1. 已经这样了,所以我们只打印变量wc中保存的字数:
          cout << "There are " << wc << " wordsn";
      };
  1. 让我们编译并运行这个程序。首先,我们从没有任何文件参数的标准输入中馈送程序。我们可以通过管道发送带有一些单词的回声呼叫,或者启动程序,从键盘输入一些单词。在后一种情况下,我们可以通过按 Ctrl + D 来停止输入。这就是一些单词在程序中的呼应方式:
      $ echo "foo bar baz" | ./count_all_words 
      There are 3 words
  1. 当以源代码文件作为输入启动程序时,它将计算它包含多少个单词:
      $ ./count_all_words count_all_words.cpp
      There are 61 words

它是如何工作的...

没什么好说的了;因为这个程序非常短,所以在实现它时已经解释了大部分内容。我们可以详细说明的一件事是,我们以完全可互换的方式使用了std::cinstd::ifstream实例。cinstd::istream型,std::ifstream继承自std::istream。看看本章开头的类继承图。这样,即使在运行时,它们也是完全可互换的。

Keep your code modular by using stream abstractions. This helps decouple source code parts and makes your code easy to test because you can just inject any other matching type of stream.

用输入输出流操纵器格式化您的输出

在许多情况下,仅仅打印出字符串和数字是不够的。有时,数字需要打印为十进制数,有时是十六进制数,有时甚至是八进制数。有时候我们想在十六进制数字前面看到一个"0x"前缀,有时候不是。

打印浮点数时,我们可能还想对很多事情产生影响。十进制值应该总是以相同的精度打印吗?它们应该被打印出来吗?或者,我们想要一个科学符号?

除了科学呈现和十六进制、八进制等,我们还希望以整齐的形式呈现用户输出。例如,一些输出可以安排在表格中,以使其尽可能易读。

当然,对于输出流,所有这些都是可能的。当解析输入流中的值时,其中一些设置也很重要。在这个食谱中,我们会通过玩弄这些所谓的输入输出操纵器来获得一种感觉。有时候,它们看起来很棘手,所以我们也会进入一些细节。

怎么做...

在本节中,我们将打印格式设置变化很大的数字,以便熟悉输入/输出操纵器:

  1. 首先,我们包括所有必要的头,并声明我们默认使用std命名空间:
      #include <iostream>
      #include <iomanip>
      #include <locale>      

      using namespace std;
  1. 接下来,我们定义一个助手函数,打印一个不同样式的整数值。它接受一个填充宽度和一个填充字符,默认设置为空格' ':
      void print_aligned_demo(int val, 
                              size_t width, 
                              char fill_char = ' ')
      {
  1. 通过setw,我们可以设置打印一个数字输出的最小字符数。例如,如果我们打印宽度为6123,我们会得到" 123",或者"123 "。我们可以通过std::leftstd::rightstd::internal来控制填充发生在哪边。当以十进制形式打印数字时,internal看起来与right相同。但是如果我们打印数值0x1,比如宽度为6,宽度为internal,则得到"0x 6"setfill操纵器定义将用于填充的字符。我们将尝试不同的风格:
          cout << "================n";
          cout << setfill(fill_char);
          cout << left << setw(width) << val << 'n';
          cout << right << setw(width) << val << 'n';
          cout << internal << setw(width) << val << 'n';
      }
  1. 在主函数中,我们开始使用刚刚实现的函数。首先,我们打印值12345,宽度为15。我们这样做了两次,但是第二次,我们使用了'_'字符作为填充:
      int main()
      {
          print_aligned_demo(123456, 15);
          print_aligned_demo(123456, 15, '_');
  1. 之后,我们以与之前相同的宽度打印值0x123abc。但是,在此之前,我们应用std::hexstd::showbase来告诉输出流对象cout它应该以十六进制格式打印数字,并且应该在它们前面加上"0x",这样很明显它们将被解释为十六进制:
          cout << hex << showbase;
          print_aligned_demo(0x123abc, 15);
  1. 我们可以用oct做同样的事情,它告诉cout使用八进制打印数字。showbase仍然有效,因此0将被添加到每个打印的号码前:
          cout << oct;
          print_aligned_demo(0123456, 15);
  1. 有了hexuppercase,我们得到了"0x"打印的大写的'x''0x123abc'中的'abc'也是大写的:
          cout << "A hex number with upper case letters: "
               << hex << uppercase << 0x123abc << 'n';
  1. 如果我们想再次以十进制格式打印100,我们必须记住我们之前已经将流切换到了hex。通过使用dec,我们可以将其恢复正常:
          cout << "A number: " << 100 << 'n';
          cout << dec;

          cout << "Oops. now in decimal again: " << 100 << 'n';
  1. 我们还可以配置布尔值的打印方式。默认情况下,true打印为1false打印为0。借助boolalpha,我们可以将其设置为文本表示:
          cout << "true/false values: " 
               << true << ", " << false << 'n';
          cout << boolalpha
               << "true/false values: "
               << true << ", " << false << 'n';
  1. 让我们看看floatdouble类型的浮点变量。如果我们打印一个数字,比如12.3,当然会打印为12.3。如果我们有一个像12.0这样的数字,那么输出流将会丢失小数点,我们可以用showpoint来改变它。使用此选项,小数点始终显示:
          cout << "doubles: "
               << 12.3 << ", "
               << 12.0 << ", "
               << showpoint << 12.0 << 'n';
  1. 浮点数的表示可以是科学的,也可以是固定的。scientific表示数字是归一化成这样的形式,第一位数字是小数点前唯一的数字,然后打印指数,需要把数字乘回实际大小。例如,值300.0将被打印为"3.0E2",因为300等于3.0 * 10^2fixed恢复到正常的小数点表示法:
          cout << "scientific double: " << scientific 
               << 123000000000.123 << 'n';
          cout << "fixed      double: " << fixed 
               << 123000000000.123 << 'n';
  1. 除了符号,我们还可以决定浮点数的打印精度。让我们创建一个非常小的值,并在小数点后打印 10 位数字,在小数点后仅打印一位数字:
          cout << "Very precise double: " 
               << setprecision(10) << 0.0000000001 << 'n';
          cout << "Less precise double: " 
               << setprecision(1)  << 0.0000000001 << 'n';
      }
  1. 编译和运行程序会产生以下冗长的输出。前四个输出块来自打印助手函数,该函数篡改了setwleft / right / internal修饰符。之后,我们使用了基本表示、布尔表示和浮点格式的大小写。玩这些游戏来熟悉它们是个好主意:
      $ ./formatting 
      ================
      123456         
               123456
               123456
      ================
      123456_________
      _________123456
      _________123456
      ================
      0x123abc       
             0x123abc
      0x       123abc
      ================
      0123456        
              0123456
              0123456
      A hex number with upper case letters: 0X123ABC
      A number: 0X64
      Oops. now in decimal again: 100
      true/false values: 1, 0
      true/false values: true, false
      doubles: 12.3, 12, 12.0000
      scientific double: 1.230000E+11
      fixed      double: 123000000000.123001
      Very precise double: 0.0000000001
      Less precise double: 0.0

它是如何工作的...

所有这些,有时相当长,<< foo << bar流表达式真的很混乱,如果读者不清楚它们每一个做什么的话。因此,让我们看一下现有格式修饰符的表格。它们都将被置于input_stream >> modifieroutput_stream << modifier表达式中,然后影响以下输入或输出:

| 符号 | 表示 | | setprecision(int n) | 打印或解析浮点值时设置精度参数。 | | showpoint / noshowpoint | 启用或禁用浮点数的小数点打印,即使它们没有任何小数位。 | | fixed / scientific / hexfloat / defaultfloat | 数字可以用固定样式(最直观的一种)或科学样式打印。fixedscientific代表这些模式。hexfloat激活两种模式,以十六进制浮点表示法格式化浮点数。defaultfloat停用两种模式。 | | showpos / noshowpos | 启用或禁用打印正浮点值的'+'前缀。 | | setw(int n) | 准确阅读或书写n字符。读取时,这会截断输入。打印时,如果输出短于n字符,则应用填充。 | | setfill(char c) | 应用填充时(见setw),用字符值c填充输出。默认为空格(' ')。 | | internal / left / right | leftright控制固定宽度打印的填充位置(参见setw)。internal将填充字符放在整数及其负号、十六进制前缀和十六进制打印值或货币单位和值之间的中间。 | | dec / hex / oct | 可以以十进制、十六进制和八进制为基础系统打印和解析整数值。 | | setbase(int n) | 这是dec / hex / oct的数字同义函数,如果与10 / 16 / 8值一起使用,它们是等价的。其他值将基本选项重置为0,这将再次导致十进制打印,或基于输入的前缀进行解析。 | | quoted(string) | 打印带引号的字符串或从带引号的输入中进行分析,然后删除引号。string可以是字符串类实例,也可以是 C 风格的字符数组。 | | boolalpha / noboolalpha | 将布尔值打印或解析为/来自字母表示法,而不是1 / 0字符串。 | | showbase / noshowbase | 打印或解析数字时启用或禁用基本前缀。对于hex,这是0x;对于octal来说是0。 | | uppercase / nouppercase | 打印浮点和十六进制值时,启用或禁用大写或字母字符。 |

熟悉它们的最好方法是稍微研究一下它们的种类,然后和它们一起玩。

然而,当玩它们的时候,我们可能已经注意到这些修改器中的大多数看起来都是粘性的,而其中的一些并不是这样。粘性意味着一旦应用,它们似乎会永远影响输入/输出*,直到它们再次复位。这张表中唯一不粘的是setwquoted。它们只影响输入/输出中的下一项。这一点很重要,因为如果我们打印一些带有特定格式的输出,我们应该在之后整理我们的流对象格式设置,因为来自不相关代码的下一个输出可能看起来很疯狂。同样的情况也适用于输入解析,在这种情况下,输入/输出操纵器选项可能会出错。*

*我们实际上并没有使用其中的任何一个,因为它们与格式化没有任何关系,但是出于完整性的原因,我们还应该看看其他一些流状态操纵器:

| 符号 | 表示 | | skipws / noskipws | 启用或禁用输入流跳过空白的功能 | | unitbuf / nounitbuf | 在任何输出操作后启用或禁用立即输出缓冲区刷新 | | ws | 可用于输入流,跳过流头的任何空白 | | ends | 将字符串终止''字符写入流中 | | flush | 立即清除输出缓冲区中的任何内容 | | endl | 将'n'字符插入输出流并刷新输出 |

从这些来看,只有skipws / noskipwsunitbuf / nounitbuf出现粘性。

从文件输入初始化复杂对象

读入单个整数、浮点和单词串真的很容易,因为输入流对象的>>运算符对所有这些类型都是重载的,输入流方便地为我们丢弃所有中间的空白。

但是,如果我们想要从输入流中读取一个更复杂的结构,并且如果我们需要读取包含多个单词的字符串(由于空白跳过,它们通常会被分块为单个单词),该怎么办?

对于任何类型,都有可能提供另一个输入流operator>>重载,我们来看看怎么做。

怎么做...

在本节中,我们将定义一个自定义数据结构,并提供从输入流中读取这些项目作为标准输入的工具:

  1. 我们需要首先包含一些头,为了方便起见,我们声明默认使用std命名空间:
      #include <iostream>
      #include <iomanip>
      #include <string>
      #include <algorithm>
      #include <iterator>
      #include <vector>      

      using namespace std;
  1. 作为一个复杂对象的例子,我们定义一个city结构。城市应有名称、人口数量和地理坐标:
      struct city {
          string name;
          size_t population;
          double latitude;
          double longitude;
      };
  1. 为了能够从串行输入流中读取这样的城市,我们需要重载流功能operator>>。在这个运算符中,我们首先跳过所有带有ws的前导空格,因为我们不希望空格污染城市名称。然后,我们阅读一整行文本输入。这意味着在输入文件中,有一整行文本只包含一个城市对象的名称。然后,在一个换行符之后,会出现一个由空格分隔的数字列表,指示人口、地理纬度和经度:
      istream& operator>>(istream &is, city &c)
      {
          is >> ws;
          getline(is, c.name);
          is >> c.population 
             >> c.latitude 
             >> c.longitude;
          return is;
      }
  1. 在我们的主要功能中,我们创建了一个可以容纳一系列城市项目的向量。我们用std::copy填充。复制调用的输入是istream_iterator范围。通过给它city结构类型作为模板参数,它将使用我们刚刚实现的operator>>函数重载:
      int main()
      {
          vector<city> l;

          copy(istream_iterator<city>{cin}, {}, 
               back_inserter(l));
  1. 为了查看我们的城市解析是否正确,我们打印了我们在列表中得到的内容。输入/输出格式,left << setw(15) <<,导致城市名被空格填充,因此我们得到可读性很好的输出:
          for (const auto &[name, pop, lat, lon] : l) {
              cout << left << setw(15) << name
                   << " population=" << pop
                   << " lat=" << lat
                   << " lon=" << lon << 'n';
          }
      }
  1. 我们将向程序提供的文本文件如下所示。有四个示例城市有其人口数量和地理坐标:
      Braunschweig
      250000 52.268874 10.526770
      Berlin
      4000000 52.520007 13.404954
      New York City
      8406000 40.712784 -74.005941
      Mexico City
      8851000 19.432608 -99.133208
  1. 编译并运行程序会产生以下输出,这是我们所期望的。尝试篡改输入文件,在城市名称前添加一些不必要的空白,以便查看它是如何被过滤掉的:
      $ cat cities.txt  | ./initialize_complex_objects
      Braunschweig    population=250000 lat=52.2689 lon=10.5268
      Berlin          population=4000000 lat=52.52 lon=13.405
      New York City   population=8406000 lat=40.7128 lon=-74.0059
      Mexico City     population=8851000 lat=19.4326 lon=-99.1332

它是如何工作的...

这又是一个简单的食谱。我们唯一做的就是创建一个新的结构city,然后我们重载了这个类型的std::istream迭代器operator>>,就这样。这已经使我们能够使用istream_iterator<city>从标准输入中反序列化城市项目。

关于错误检查,可能还有一个悬而未决的问题。为此,我们再来看看operator>>的实现:

      istream& operator>>(istream &is, city &c)
      {
          is >> ws;
          getline(is, c.name);
          is >> c.population >> c.latitude >> c.longitude;
          return is;
      }

我们正在阅读许多不同的东西。如果其中一个失败了,下一个没有,会发生什么?这是否意味着我们可能正在读取令牌流中具有错误“偏移量”的所有以下项目?不,这不可能。一旦无法从输入流中解析这些项中的一项,输入流对象就会进入错误状态,并拒绝进一步解析任何内容。这意味着,如果例如c.populationc.latitude不能被解析,剩余的>>操作数只是“通过”,我们将这个运算符函数范围留给一个半反序列化的城市对象。

在调用者方面,当我们写if (input_stream >> city_object)时,我们会收到这个通知。当用作条件表达式时,这样的流表达式被隐式转换为布尔值。如果输入流对象处于错误状态,则返回false。知道我们可以重置流并做任何合适的事情。

在这个食谱中,我们没有自己编写这样的if条件句,因为我们让std::istream_iterator<city>进行反序列化。这个迭代器类的operator++ 实现也在解析时检查错误。如果出现任何错误,它将拒绝进一步迭代。在这种状态下,当与结束迭代器比较时,它返回true,这使得copy算法终止。这样,我们就安全了。

从 std::istream 迭代器填充容器

在上一个食谱中,我们学习了如何从输入流中组装复合数据结构,然后用它们填充列表或向量。

这一次,我们通过从标准输入中填充一个std::map来使它变得更难一点。这里的问题是,我们不能仅仅用值填充单个结构,并将其推回到像列表或向量这样的线性容器中,因为map将其有效负载分为键和值部分。然而,正如我们将看到的,这并不完全不同。

在研究了这个方法之后,我们会对将复杂的数据结构从字符流序列化和反序列化到字符流感到很舒服。

怎么做...

我们将定义另一个结构,就像上一个配方一样,但这次我们将把它填充到一个映射中,这使它变得更加复杂,因为这个容器从键映射到值,而不是只保存列表中的所有值:

  1. 首先,我们包括所有需要的头,并声明我们默认使用std命名空间:
      #include <iostream>
      #include <iomanip>
      #include <map>
      #include <iterator>
      #include <algorithm>
      #include <numeric>      

      using namespace std;
  1. 我们想维护一个小小的互联网迷因数据库。假设一个模因有一个名字,一个描述,以及它诞生或发明的年份。我们将它们保存在std::map中,其中名称是键,其他信息作为与键相关联的值聚集在一个结构中:
      struct meme {
          string description;
          size_t year;
      };
  1. 我们先忽略这个键,只为struct meme实现一个流operator>>函数重载。我们假设描述用引号括起来,后面是年份。这看起来像文本文件中的"some description" 2017。由于描述被引号包围,它可以包含空白,因为我们知道引号之间的一切都属于它。通过使用is >> quoted(m.description)阅读,引号会自动用作分隔符,并在之后被删除。这很方便。就在那之后,我们读了年号:
      istream& operator>>(istream &is, meme &m) {
          return is >> quoted(m.description) >> m.year;
      }
  1. 好了,现在我们把迷因的名字作为地图的关键考虑进去。为了在地图中插入一个模因,我们需要一个std::pair<key_type, value_type>实例。key_type当然是string,而value_type就是meme。名称也允许包含空格,所以我们使用相同的quoted包装作为描述。p.first是名称,p.second是与之关联的整个meme结构。它将被输入到我们刚刚实现的另一个operator>>实现中:
      istream& operator >>(istream &is, 
                           pair<string, meme> &p) {
          return is >> quoted(p.first) >> p.second;
      }
  1. 好了,就这样。让我们编写一个主函数,它实例化一个映射,并填充该映射。因为我们重载了流函数operator>>istream_iterator可以直接处理这个类型。我们让它从标准输入中反序列化我们的模因项,并使用一个inserter迭代器将它们输入到映射中:
      int main()
      {
          map<string, meme> m;

          copy(istream_iterator<pair<string, meme>>{cin},
               {},
               inserter(m, end(m)));
  1. 在我们打印我们所拥有的之前,让我们首先找出地图中最长的模因名称是什么。我们用std::accumulate来表示这个。它获得一个初始值0u ( u表示无符号),并将逐元素访问地图,以便将它们合并在一起。就accumulate而言,合并通常意味着增加。在我们的例子中,我们不需要任何数字,只需要最大的字符串长度。为了得到这个结果,我们提供了accumulate一个助手函数max_func,它获取当前最大大小变量(因为字符串长度是无符号的,所以必须是unsigned),并将其与当前项目的 meme 名称字符串的长度进行比较,以便获取两个值的最大值。每个元素都会发生这种情况。accumulate函数的最终返回值是最大模因名称长度:**
          auto max_func ([](size_t old_max, 
                            const auto &b) {
              return max(old_max, b.first.length());
          });
          size_t width {accumulate(begin(m), end(m), 
                                   0u, max_func)};
  1. 现在,让我们快速循环浏览地图并打印每个项目。我们使用<< left << setw(width)获得一个漂亮的类似表格的打印:
          for (const auto &[meme_name, meme_desc] : m) {
              const auto &[desc, year] = meme_desc;

              cout << left << setw(width) << meme_name
                   << " : " << desc
                   << ", " << year << 'n';
          }
      }
  1. 就这样。我们需要一个小的互联网迷因数据库文件,所以让我们用一些例子来填充一个文本文件:
      "Doge" "Very Shiba Inu. so dog. much funny. wow." 2013
      "Pepe" "Anthropomorphic frog" 2016
      "Gabe" "Musical dog on maximum borkdrive" 2016
      "Honey Badger" "Crazy nastyass honey badger" 2011
      "Dramatic Chipmunk" "Chipmunk with a very dramatic look" 2007
  1. 使用示例 meme 数据库编译和运行程序会产生以下输出:
      $ cat memes.txt | ./filling_containers 
      Doge              : Very Shiba Inu. so dog. much funny. wow., 2013
      Dramatic Chipmunk : Chipmunk with a very dramatic look, 2007
      Gabe              : Musical dog on maximum borkdrive, 2016
      Honey Badger      : Crazy nastyass honey badger, 2011
      Pepe              : Anthropomorphic frog, 2016

它是如何工作的...

这个食谱有三个特色菜。一个是我们没有从一个连续的字符流中填充一个法向量或者一个列表,而是像std::map这样一个更复杂的容器。另一个是我们使用了那些神奇的quoted流操纵器。最后一个是accumulate调用,它找出最大的键串大小。

让我们从map部分开始。我们的struct meme只包含一个description字段和year。互联网模因的名称不属于这种结构,因为它被用作地图的关键。当我们在地图中插入一些东西时,我们可以为std::pair提供一个键类型和一个值类型。这就是我们所做的。我们首先为struct meme实现了流operator>>,然后为pair<string, meme>做了同样的事情。然后我们使用istream_iterator<**pair<string, meme>**>{cin}从标准输入中获取这些项目,并使用inserter(m, end(m))将其输入到地图中。

当我们从流中反序列化 meme 项时,我们允许名称和描述包含空白。这很容易实现,尽管我们每个模因只用了一行,因为我们引用了 T2 的那些字段。行格式的示例如下:"Name with spaces" "Description with spaces" 123

在处理输入和输出中的引用字符串时,std::quoted是一个很大的帮助。如果我们有一个字符串,s,使用cout << quoted(s)打印它会把它放在引号中。如果我们从流中反序列化一个字符串,例如,通过cin >> quoted(s),它将读取下一个引号,用后面的内容填充字符串,并继续,直到它看到下一个引号,无论涉及多少空白。

最后一个看起来奇怪的东西是我们累积呼叫中的max_func:

auto max_func ([](size_t old_max, const auto &b) {
    return max(old_max, b.first.length());
});

size_t width {accumulate(begin(m), end(m), 0u, max_func)};

显然,max_func接受了一个size_t参数和另一个auto-类型的参数,结果是地图上的一个pair项目。起初这看起来很奇怪,因为大多数二进制约简函数接受相同类型的参数,然后通过某种操作将它们合并在一起,就像std::plus所做的那样。在这种情况下,真的不同,因为我们没有合并实际的pair项目。我们只从每一对中挑选键串长度,去掉剩下的,然后用max函数减少得到的size_t值。

在累加调用中,max_func的第一次调用获得了我们最初作为左侧参数提供的0u值,以及对右侧第一对项目的引用。这会产生一个max(0u, string_length)返回值,它是下一个调用中的左参数,下一个配对项作为右参数,依此类推。

带有标准::ostream 迭代器的通用打印

用输出流打印任何东西都非常容易,因为 STL 已经为最基本的类型提供了许多有用的operator<<重载。这样,包含这种类型项目的数据结构可以很容易地使用std::ostream_iterator类打印出来,我们在本书中已经经常这样做了。

在本食谱中,我们将集中讨论如何使用自定义类型来实现这一点,以及我们还可以做些什么来通过模板类型选择来操作打印,而不需要在调用方进行太多的代码。

怎么做...

我们将通过启用与新的自定义类的组合来玩std::ostream_iterator,并查看它的隐式转换功能,这可以帮助我们进行打印:

  1. 首先是包含文件,然后我们声明默认使用std命名空间:
      #include <iostream>
      #include <vector>
      #include <iterator>
      #include <unordered_map>
      #include <algorithm>      

      using namespace std;
  1. 让我们实现一个转换函数,它将数字映射到字符串。对于值1,返回"one",对于值2,返回"two",依此类推:
      string word_num(int i) {
  1. 我们用需要的映射来填充哈希映射,以便以后访问它们:
          unordered_map<int, string> m {
              {1, "one"}, {2, "two"}, {3, "three"},
              {4, "four"}, {5, "five"}, //...
          };
  1. 现在,我们可以用参数i来填充哈希映射的find函数,并返回它找到的内容。如果它没有找到任何东西,因为没有给定数字的翻译,我们返回字符串,"unknown":
          const auto match (m.find(i));
          if (match == end(m)) { return "unknown"; }
          return match->second;
      };
  1. 我们稍后将玩的另一个东西是struct bork。它只包含一个整数,也可以从整数隐式构造。它有一个print函数,该函数接受输出流引用并根据其成员整数borks的值重复打印"bork"字符串:
      struct bork {
          int borks;

          bork(int i) : borks{i} {}

          void print(ostream& os) const {
              fill_n(ostream_iterator<string>{os, " "}, 
                     borks, "bork!"s);
          }
      };
  1. 为了方便使用bork::print,我们为流对象重载operator<<,因此每当bork对象被流式传输到输出流时,它们会自动调用bork::print:
      ostream& operator<<(ostream &os, const bork &b) {
          b.print(os);
          return os;
      }
  1. 现在我们终于可以开始实现实际的主功能了。我们最初只是用一些示例值创建一个向量:
      int main()
      {
          const vector<int> v {1, 2, 3, 4, 5};
  1. 类型为ostream_iterator的对象需要一个模板参数,该参数表示它们可以打印哪种类型的变量。如果写ostream_iterator<**T**>,以后会用ostream& operator(ostream&, const **T**&)打印。例如,这正是我们之前为bork类型实现的。这次我们只是打印整数,所以是ostream_iterator<**int**>。应使用cout进行打印,因此我们将其作为构造器参数提供。我们在一个循环中遍历向量,并将每一项i分配给解引用的输出迭代器。STL 算法也是这样使用流迭代器的:
          ostream_iterator<int> oit {cout};

          for (int i : v) { *oit = i; }
          cout << 'n';
  1. 我们刚刚生成的迭代器的输出很好,但是它打印的数字没有任何分隔符。如果我们想在所有打印的项目之间分隔一点空白,我们可以提供一个自定义的间隔字符串作为输出流迭代器的构造函数的第二个参数。这样打印的是"1, 2, 3, 4, 5, "而不是"12345"。不幸的是,我们不能轻易地告诉它在最后一个数字之后删除逗号空格字符串,因为迭代器在到达它之前不知道它的结尾:
          ostream_iterator<int> oit_comma {cout, ", "};

          for (int i : v) { *oit_comma = i; }
          cout << 'n';
  1. 为输出流迭代器分配项目以便打印它们并不是一种错误的使用方式,但这并不是它们被发明的目的。想法是将它们与算法结合使用。最简单的就是std::copy。我们可以提供向量的开始和结束迭代器作为输入范围,输出流迭代器作为输出迭代器。它将打印矢量的所有数字。让我们用输出迭代器来做这件事,然后将输出与我们之前编写的循环进行比较:
          copy(begin(v), end(v), oit);
          cout << 'n';

          copy(begin(v), end(v), oit_comma);
          cout << 'n';
  1. 还记得把数字映射成字符串的函数word_num吗,比如1"one"2"two"等等?是的,我们也可以用它们来印刷。我们只需要使用一个输出流操作符,它是专门用于string的模板,因为我们不再打印整数了。我们使用std::transform而不是std::copy,因为它允许我们在将输入范围复制到输出范围之前,对输入范围中的每个项目应用转换函数:
          transform(begin(v), end(v), 
                    ostream_iterator<string>{cout, " "}, 
                    word_num);
          cout << 'n';
  1. 本程序最后一行输出最后将struct bork投入使用。我们可以,但不能为std::transform提供转换功能。相反,我们可以只创建一个输出流迭代器,它专门处理std::copy调用中的bork类型。这导致bork实例隐式地从输入范围整数创建。这会给我们一些有趣的输出:
          copy(begin(v), end(v), 
               ostream_iterator<bork>{cout, "n"});
      }
  1. 编译并运行程序会产生以下输出。前两行和后两行完全一样,这也是我们怀疑的。然后,我们得到一行漂亮的、写好的数字字符串,后面是许多bork!字符串。这些出现在多行中,因为我们使用了一个"n"分隔符字符串来代替空格:
      $ ./ostream_printing 
      12345
      1, 2, 3, 4, 5, 
      12345
      1, 2, 3, 4, 5, 
      one two three four five 
      bork! 
      bork! bork! 
      bork! bork! bork! 
      bork! bork! bork! bork! 
      bork! bork! bork! bork! bork! 

它是如何工作的...

我们已经看到std::ostream_iterator实际上只是一个*语法黑客,*将打印的行为挤压到迭代器的形式和语法中。递增这样的迭代器没有任何作用。取消对它的引用只会返回一个代理对象,该对象的赋值操作符将其参数转发给输出流。

专用于类型T(如在ostream_iterator<T>中)的输出流迭代器适用于所有提供了ostream& operator<<(ostream&, const T&)实现的类型。

ostream_iterator总是试图通过模板参数调用其专用类型的operator<<。如果允许,它将尝试隐式转换类型。当我们迭代一系列A类型的项目,但我们将这些项目复制到output_iterator<B>实例时,如果A可以隐式转换为B,这将会起作用。我们对struct bork做了完全相同的事情:一个bork实例可以从整数值隐式转换。这就是为什么在用户外壳上抛出大量"bork!"字符串如此容易的原因。

如果隐式转换不可能,我们可以自己做,使用std::transform,这是我们结合word_num函数做的。

Note that it is, in general, bad style to allow implicit conversions for custom types because this is a common source of bugs that are really hard to find later. In our example use case, the implicit constructor is more useful than dangerous because the class is used for nothing else but printing.

将输出重定向到特定代码段的文件

std::cout提供了一种非常好的方式来打印我们想要的任何东西,无论何时,因为它使用简单,易于扩展,并且可以全局访问。即使我们想打印特殊的消息,比如错误消息,我们想从正常消息中隔离出来,我们也可以只使用std::cerr,它与cout相同,但是打印到标准的错误通道,而不是标准的输出通道。

有时我们可能会有一些更复杂的伐木欲望。比方说,我们想要一个函数的输出重定向到一个文件,或者我们想要静音一个函数的输出,而完全不改变函数。也许,这是一个我们无法访问源代码的库函数。也许,它从来没有被设计成写入文件,但我们希望它的输出在一个文件中。

确实可以重定向流对象的输出。在这个食谱中,我们将看到如何以一种非常简单和优雅的方式做到这一点。

怎么做...

我们将实现一个助手类,解决重定向流的问题,并使用构造函数/析构函数魔法再次恢复重定向。然后我们看看如何使用它:

  1. 这次我们只需要输入、输出和文件流的头。我们将std命名空间声明为查找的默认命名空间:
      #include <iostream>
      #include <fstream>     

      using namespace std;
  1. 我们实现一个类,它保存一个文件流对象和一个指向流缓冲区的指针。作为流对象的cout有一个内部的流缓冲区,我们可以简单的交换。当我们交换它的时候,我们可以保存它以前的样子,所以我们可以撤销以后的任何改变。我们可以在 C++ 引用中查找它的类型,但是我们也可以使用decltype来找出cout.rdbuf()返回的类型。这通常不是所有情况下的好做法,但在这种情况下,它只是一种指针类型:
      class redirect_cout_region
      {
          using buftype = decltype(cout.rdbuf());

          ofstream ofs;
          buftype  buf_backup;
  1. 我们类的构造函数接受一个文件名字符串作为它的唯一参数。文件名用于初始化文件流成员ofs。初始化后,我们可以将其作为新的流缓冲区送入cout。接受新缓冲区的同一个函数也返回一个指向旧缓冲区的指针,所以我们可以保存它以便以后恢复它:
      public:
          explicit 
          redirect_cout_region (const string &filename)
              : ofs{filename}, 
                buf_backup{cout.rdbuf(ofs.rdbuf())}
          {}
  1. 默认构造函数的作用与其他构造函数相同。不同的是,它不打开任何文件。将默认构造的文件流缓冲区送入cout流缓冲区会导致cout有点像去激活。它将只是放下它的输入我们给它打印。这在某些情况下也很有用:
          redirect_cout_region()
              : ofs{}, 
                buf_backup{cout.rdbuf(ofs.rdbuf())}
          {}
  1. 析构函数只是恢复我们的变化。当这个类的一个对象超出范围时,cout的流缓冲区又是原来的那个:
          ~redirect_cout_region() { 
              cout.rdbuf(buf_backup); 
          }
      };
  1. 让我们模拟一个输出重的函数,这样我们以后就可以玩它了:
      void my_output_heavy_function()
      {
          cout << "some outputn";
          cout << "this function does really heavy workn";
          cout << "... and lots of it...n";
          // ...
      }
  1. 在主函数中,我们首先产生一些完全正常的输出:
      int main()
      {
          cout << "Readable from normal stdoutn";
  1. 现在我们打开另一个作用域,在这个作用域中我们做的第一件事是用一个文本文件参数实例化我们的新类。默认情况下,文件流以读写模式打开文件,因此它会为我们创建这个文件。尽管我们使用cout进行打印,但以下任何输出现在都将被重定向到该文件:
          {
              redirect_cout_region _ {"output.txt"};
              cout << "Only visible in output.txtn";
              my_output_heavy_function();
          }
  1. 离开作用域后,文件被关闭,输出再次重定向到正常的标准输出。现在让我们打开另一个范围,在其中实例化同一个类,但是通过它的默认构造函数。这样,以下打印的文本行在任何地方都不可见。它将被丢弃:
          {
              redirect_cout_region _;
              cout << "This output will "
                      "completely vanishn";
          }
  1. 在离开这个范围之后,我们的标准输出被恢复,最后一行文本输出将在 shell 中再次可读:
          cout << "Readable from normal stdout againn";
      }
  1. 编译和运行程序会产生我们期望的输出。在 shell 中只能看到输出的第一行和最后一行:
      $ ./log_regions 
      Readable from normal stdout
      Readable from normal stdout again
  1. 我们可以看到,一个新的文件output.txt已经被创建,并且包含了第一个作用域的输出。第二个作用域的输出完全消失:
      $ cat output.txt 
      Only visible in output.txt
      some output
      this function does really heavy work
      ... and lots of it...

它是如何工作的...

每个流对象都有一个作为前端的内部缓冲区。这种缓冲剂是可交换的。如果我们有一个流对象s,并想将其缓冲区保存到一个变量a中,并安装一个新的缓冲区b,这看起来像下面这样:a = s.rdbuf(b)。恢复它可以简单地用s.rdbuf(a)来完成。

这正是我们在这个食谱中所做的。另一件很酷的事情是,我们可以把那些帮手堆起来:

{
    cout << "print to standard outputn";

    redirect_cout_region la {"a.txt"};
    cout << "print to a.txtn";

    redirect_cout_region lb {"b.txt"};
    cout << "print to b.txtn";
}
cout << "print to standard output againn";

这是因为对象是按照与其构造相反的顺序被析构的。这种利用对象的构造和破坏之间的紧密耦合的模式背后的概念被称为资源获取是初始化 ( RAII )。

有一件非常重要的事情应该被提及——类成员变量的初始化顺序:

class redirect_cout_region {
    using buftype = decltype(cout.rdbuf());

    ofstream ofs;
    buftype  buf_backup;

public:
    explicit 
    redirect_cout_region(const string &filename)
        : ofs{filename}, 
          buf_backup{cout.rdbuf(ofs.rdbuf())}
    {}

...

如我们所见,成员buf_backup是由依赖于ofs的表达式构成的。这显然意味着ofs需要在buf_backup之前初始化。有趣的是,这些成员的初始化顺序并不取决于初始化列表项的顺序。初始化顺序只取决于成员声明的顺序!

If one class member variable needs to be initialized after another member variable, they must also appear in that order in the class member declaration. The order of their appearance in the initializer list of the constructor is not critical.

通过继承 std::char_traits 创建自定义字符串类

std::string非常有用。然而,只要人们需要一个语义略有不同的字符串类来处理字符串,一些人就倾向于编写他们自己的字符串类。

*编写自己的字符串类很少是个好主意,因为安全的字符串处理很难。幸运的是,std::string只是模板类std::basic_string的一个专门化的 typedef。这个类包含了所有复杂的内存处理内容,但是它没有对字符串的复制、比较等方式强加任何策略。这是通过接受包含特征类的模板参数导入到basic_string中的东西。

在这个食谱中,我们将看到如何构建我们自己的特性类,以及如何在不重新实现任何东西的情况下创建自定义字符串。

怎么做...

我们将实现两个不同的自定义字符串类:lc_stringci_string。第一个类从任何字符串输入中构造小写字符串。另一个类不转换任何字符串,但它可以进行不区分大小写的字符串比较:

  1. 让我们首先包含几个必要的头,然后声明我们默认使用std命名空间:
      #include <iostream>
      #include <algorithm>
      #include <string>      

      using namespace std;
  1. 然后我们重新实现std::tolower函数,这个函数已经在<cctype>中定义了。已经存在的功能还好,但不是constexpr。然而,从 C++ 17 开始,一些string函数就成为了constexpr,我们希望能够在我们自己的自定义字符串特征类中利用这一点。该函数将大写字符映射为小写字符,其他字符保持不变:
      static constexpr char tolow(char c) {
          switch (c) {
          case 'A'...'Z': return c - 'A' + 'a';
          default:        return c;
          }
      }
  1. std::basic_string类接受三个模板参数:基础字符类型、字符特征类和分配器类型。我们只是在这一节中更改字符特征类,因为它定义了字符串的行为。为了只重新实现不同于普通字符串的内容,我们公开继承了标准的 traits 类:
      class lc_traits : public char_traits<char> {
      public:
  1. 我们的类接受输入字符串,但将其转换为小写。有一个函数,是按字符来做这个的,所以我们可以在这里放自己的tolow函数。这个函数就是constexpr,这就是为什么我们给自己重新实现了一个constexpr tolow函数:
          static constexpr 
          void assign(char_type& r, const char_type& a ) {
              r = tolow(a);
          }
  1. 另一个函数负责将整个字符串复制到自己的内存中。我们使用std::transform调用将所有字符从源字符串复制到内部目标字符串,同时将每个字符映射到其小写版本:
          static char_type* copy(char_type* dest, 
                                 const char_type* src, 
                                 size_t count) {
              transform(src, src + count, dest, tolow);
              return dest;
          }
      };
  1. 另一个特性有助于构建一个字符串类,有效地将字符串转换为小写。我们将编写另一个特性,保持实际的字符串有效负载不变,但是在比较字符串时不区分大小写。我们再次继承了现有的标准角色特征类,这一次,我们重新定义了一些其他成员函数:
      class ci_traits : public char_traits<char> {
      public:
  1. eq功能告知两个字符是否相等。我们也这样做,但是我们比较它们的小写版本。这样'A'就等于'a':
          static constexpr bool eq(char_type a, char_type b) {
              return tolow(a) == tolow(b);
          }
  1. lt功能告知a的值是否小于b的值。我们对此应用了正确的逻辑运算符,仅在两个字符的小写之后:
          static constexpr bool lt(char_type a, char_type b) {
              return tolow(a) < tolow(b);
          }
  1. 后两个函数用于字符输入,后两个函数用于字符串输入。compare功能的工作原理类似于老派的strncmp功能。如果两个字符串在count定义的长度内相等,则返回0。如果它们不同,它将返回一个负数或正数,这表明哪个输入字符串在字典序上更小。当然,计算每个位置的两个字符之间的差异必须在它们的小写版本上完成。好的一点是,从 C++ 14 开始,整个循环代码就成为了一个constexpr函数的一部分:
          static constexpr int compare(const char_type* s1,
                                       const char_type* s2,
                                       size_t count) {
              for (; count; ++ s1, ++ s2, --count) {
                  const char_type diff (tolow(*s1) - tolow(*s2));
                  if      (diff < 0) { return -1; }
                  else if (diff > 0) { return +1; }
              }
              return 0;
          }
  1. 我们需要为不区分大小写的字符串类实现的最后一个函数是find。对于给定的输入字符串p和长度count,它会找到字符的位置ch。然后,它返回一个指向该字符第一次出现的指针,如果没有,则返回nullptr。该功能中的比较必须使用tolow“眼镜”来完成,以使搜索不区分大小写。可惜我们不能用std::find_if,因为不是constexpr,必须自己写一个循环:
          static constexpr 
          const char_type* find(const char_type* p,
                                size_t count,
                                const char_type& ch) {
              const char_type find_c {tolow(ch)};

              for (; count != 0; --count, ++ p) {
                  if (find_c == tolow(*p)) { return p; }
              }

              return nullptr;
          }
      };
  1. 好了,这就是特质。既然我们现在已经有了它们,我们可以定义两个新的字符串类类型。lc_string表示小写字母串ci_string表示不区分大小写的字符串。这两个班与std::string的区别仅在于他们的性格特征班:
      using lc_string = basic_string<char, lc_traits>;
      using ci_string = basic_string<char, ci_traits>;
  1. 为了让输出流接受这些新类进行打印,我们需要快速重载流operator<<:
      ostream& operator<<(ostream& os, const lc_string& str) {
          return os.write(str.data(), str.size());
      }

      ostream& operator<<(ostream& os, const ci_string& str) {
          return os.write(str.data(), str.size());
      }
  1. 现在我们终于可以开始实施实际的计划了。让我们实例化一个普通字符串、一个小写字符串和一个不区分大小写的字符串,并立即打印它们。它们在终端上都应该看起来正常,但是小写字符串应该都是小写的:
      int main()
      {
          cout << "   string: " 
               << string{"Foo Bar Baz"} << 'n'
               << "lc_string: " 
               << lc_string{"Foo Bar Baz"} << 'n'
               << "ci_string: "
               << ci_string{"Foo Bar Baz"} << 'n';
  1. 为了测试不区分大小写的字符串,我们可以实例化两个基本相等但某些字符大小写不同的字符串。当进行真正不区分大小写的比较时,它们看起来应该是相等的:
          ci_string user_input {"MaGiC PaSsWoRd!"};
          ci_string password   {"magic password!"};
  1. 因此,让我们比较它们,如果它们匹配,就打印出来:
          if (user_input == password) {
              cout << "Passwords match: "" << user_input
                   << "" == "" << password << ""n";
          }
      }
  1. 编译和运行这个程序会给我们带来预期的结果。当我们第一次以不同的类型打印同一个字符串三次时,我们得到了不变的结果,但是lc_string实例都是小写的。这两个字符串只有字符大小写不同,对它们的比较确实是成功的,并为我们提供了正确的输出:
      $ ./custom_string 
         string: Foo Bar Baz
      lc_string: foo bar baz
      ci_string: Foo Bar Baz
      Passwords match: "MaGiC PaSsWoRd!" == "magic password!"

它是如何工作的...

对于初学者来说,我们所做的所有子类化和函数重新实现看起来肯定有点疯狂。所有的函数签名来自哪里,我们神奇地知道我们需要重新实现?

我们先来看看std::string到底从何而来:

template <
    class CharT, 
    class Traits    = std::char_traits<CharT>, 
    class Allocator = std::allocator<CharT>
    > 
class basic_string;

std::string实际上是一个std::basic_string<char>,并扩展到std::basic_string<char, std::char_traits<char>, std::allocator<char>>。好吧,这是一个很长的类型描述,但它意味着什么?所有这些的要点是,字符串不仅可以基于单字节char项,还可以基于其他更大的类型。这支持字符串类型,它可以处理比典型的美国 ASCII 字符集更多的内容。这不是我们现在要调查的事情。

然而char_traits<char>类包含basic_string运行所需的算法。知道如何比较、查找和复制字符和字符串。

allocator<char>类也是一个 traits 类,但是它的特殊工作是处理字符串分配和解除分配。这在此时对我们来说并不重要,因为默认行为满足了我们的需求。

如果我们想要一个字符串类有不同的行为,我们可以尝试从basic_stringchar_traits已经提供的东西中尽可能多地重用。这就是我们所做的。我们实现了两个名为case_insentitivelower_caserchar_traits子类,并通过使用它们作为标准char_traits类型的替代品来配置两个全新的字符串类型。

In order to explore what other possibilities there are to adapt basic_string to your own needs, look up the C++ STL documentation for std::char_traits and see what other functions it has that can be reimplemented.

使用正则表达式库标记输入

当以复杂的方式解析或转换字符串或将其分解成块时,正则表达式是一个很大的帮助。在许多编程语言中,它们已经内置,因为它们非常有用和方便。

如果你还不知道正则表达式,可以看看维基百科关于正则表达式的文章。它们肯定会扩展你的视野,因为很容易看出它们在解析任何类型的文本时有多有用。例如,正则表达式可以测试电子邮件地址字符串或 IP 地址字符串是否有效,从遵循复杂模式的大字符串中查找和提取子字符串,等等。

在这个食谱中,我们将从一个 HTML 文件中提取所有链接,并为用户列出它们。代码将会非常短,因为我们从 C++ 11 开始就在 C++ STL 中内置了正则表达式支持。

怎么做...

我们将定义一个检测链接的正则表达式,并将它应用于一个 HTML 文件,以便漂亮地打印该文件中出现的所有链接:

  1. 让我们首先包含所有必要的头,并声明我们默认使用std命名空间:
      #include <iostream>
      #include <iterator>
      #include <regex>
      #include <algorithm>
      #include <iomanip>      

      using namespace std;
  1. 稍后我们将生成一个由字符串组成的可迭代范围。这些字符串总是成对出现在链接和链接描述中。因此,让我们编写一个小助手函数,它可以很好地打印以下内容:
      template <typename InputIt>
      void print(InputIt it, InputIt end_it)
      {
          while (it != end_it) {
  1. 在每个循环步骤中,我们将迭代器递增两次,并复制它们包含的链接和链接描述。为了安全起见,在两个迭代器解引用之间,我们添加了另一个保护if分支,检查我们是否过早到达了可迭代范围的末尾:
              const string link {*it++};
              if (it == end_it) { break; }
              const string desc {*it++};
  1. 现在,让我们用漂亮的修饰形式打印带有描述的链接,就是这样:
              cout << left << setw(28) << desc 
                   << " : " << link << 'n';
          }
      }
  1. 在主功能中,我们阅读来自标准输入的所有内容。为此,我们通过输入流迭代器从整个标准输入中构造一个字符串。为了防止标记化,因为我们希望整个用户输入保持原样,所以我们使用noskipws。此修饰符停用空白跳过和标记化:
      int main()
      {
          cin >> noskipws;
          const std::string in {istream_iterator<char>{cin}, {}};
  1. 现在我们需要定义一个正则表达式来描述我们假设的 HTML 链接的外观。正则表达式中的括号()定义了组。这些是我们想要访问的链接的部分——它链接到的网址及其描述:
          const regex link_re {
              "<a href="([^"]*)"[^<]*>([^<]*)</a>"};
  1. sregex_token_iterator级的观感与istream_iterator级相同。我们给它整个字符串作为可迭代的输入范围和我们刚刚定义的正则表达式。还有第三个参数{1, 2},它是整数值的初始化列表。它定义了我们想要从它捕获的表达式中迭代组 1 和组 2:
          sregex_token_iterator it {
              begin(in), end(in), link_re, {1, 2}};
  1. 现在我们有了一个迭代器,如果它找到链接和链接描述,就会发出链接和链接描述。我们将它与一个相同类型的默认构造迭代器一起提供给我们之前实现的print函数:
          print(it, {});
      }
  1. 编译并运行该程序会给出以下输出。我在 ISO C++ 主页上运行curl程序,它只是从网上下载一个 HTML 页面。当然,也可以写cat some_html_file.html | ./link_extraction。我们使用的正则表达式基本上是硬编码在 HTML 文档中链接外观的固定假设上的。您可以使用它来使它更通用:
      $ curl -s "https://isocpp.org/blog" | ./link_extraction 
      Sign In / Suggest an Article : https://isocpp.org/member/login
      Register                     : https://isocpp.org/member/register
      Get Started!                 : https://isocpp.org/get-started
      Tour                         : https://isocpp.org/tour
      C++ Super-FAQ                : https://isocpp.org/faq
      Blog                         : https://isocpp.org/blog
      Forums                       : https://isocpp.org/forums
      Standardization              : https://isocpp.org/std
      About                        : https://isocpp.org/about
      Current ISO C++ status       : https://isocpp.org/std/status
      (...and many more...)

它是如何工作的...

正则表达式(简称正则表达式)极其有用。它们可能看起来很神秘,但值得了解它们是如何工作的。如果我们手动进行匹配,一个简短的正则表达式可以省去我们写很多行代码。

在这个方法中,我们首先实例化了一个 regex 类型的对象。我们为它的构造函数提供了一个描述正则表达式的字符串。一个非常简单的正则表达式是".",它匹配每个字符的*,因为一个点是正则表达式通配符。如果我们写"a",那么这仅匹配'a'字符。如果我们写"ab*",那么这意味着“一个a,零个或任意多个b字符”。等等。正则表达式是另一个大话题,维基百科和其他网站或文献上有很好的解释。*

让我们再看看我们的正则表达式,它与我们假设的 HTML 链接相匹配。一个简单的 HTML 链接可以看起来像<a href="some_url.com/foo">A great link</a>。我们想要some_url.com/foo部分,还有A great link。所以我们想出了下面的正则表达式,它包含用于匹配子串的:

整场比赛本身永远是0 组。在这种情况下,这是完整的<a href ..... </a>字符串。引用的href-包含链接到的网址的部分是组 1 。正则表达式中的( )括号定义了这样一个,另一个是<a ...></a>之间的部分,包含链接描述。

有各种接受 regex 对象的 STL 函数,但是我们直接使用了一个 regex token 迭代器适配器,这是一个高级抽象,在引擎盖下使用std::regex_search以便自动化重复的匹配工作。我们这样实例化它:

sregex_token_iterator it {begin(in), end(in), link_re, {1, 2}};

开始和结束部分表示我们的输入字符串,正则表达式标记迭代器将对其进行迭代并匹配所有链接。当然,是我们为匹配链接而实现的复杂正则表达式。{1, 2}部分是下一个看起来复杂的东西。它指示标记迭代器在每次完全匹配时停止,首先产生组 1,然后在迭代器递增后产生组 2,再次递增后,它将最终搜索字符串中的下一个匹配。这种有些智能的行为真的让我们少了一些代码行。

让我们看看另一个例子,以确保我们得到了这个想法。让我们想象一下正则表达式,"a(b*)(c*)"。它将匹配包含一个a字符、一个或任意多个b字符、一个或任意多个c字符的字符串:

const string s {" abc abbccc "};
const regex re {"a(b*)(c*)"};

sregex_token_iterator it {begin(s), end(s), re, {1, 2}};

print( *it ); // prints b
++ it;
print( *it ); // prints c
++ it;
print( *it ); // prints bb
++ it;
print( *it ); // prints ccc

还有std::regex_iterator类,它发出的子串是正则表达式匹配之间的*。*

根据上下文动态打印不同的数字,非常舒服

在上一个食谱中,我们学习了如何用输出流格式化输出。同时,我们意识到两个事实:

  • 大多数输入输出操纵器都是粘性的,所以我们必须在使用后恢复它们的效果,以免篡改其他不相关的代码,这些代码也会打印出来
  • 如果我们必须设置输入/输出操纵器的长链,以便只打印几个带有特定格式的变量,这可能会非常繁琐,而且看起来不太可读

很多人因为这样的原因不喜欢 I/O 流,甚至在 C++ 中,他们仍然使用printf来格式化自己的字符串。

在这个食谱中,我们将看到如何在没有太多输入/输出操纵器噪音的情况下动态格式化类型。

怎么做...

我们将要实现一个类,format_guard,它可以自动恢复任何格式设置。此外,我们添加了一个包装类型,它可以包含任何值,但是当它被打印时,它会得到特殊的格式,而不会给我们带来输入/输出操纵器噪音:

  1. 首先,我们包含一些头,并声明我们使用std命名空间:
      #include <iostream>
      #include <iomanip>      

      using namespace std;
  1. 为我们整理流格式状态的助手类叫做format_guard。它的构造函数保存了std::cout当前设置的格式化标志。它的析构函数将它们恢复到调用构造函数时的状态。这有效地撤销了在以下两者之间应用的任何格式设置:
      class format_guard {
          decltype(cout.flags()) f {cout.flags()};

      public:
          ~format_guard() { cout.flags(f); }
      };
  1. 另一个小帮手类是scientific_type。因为它是一个类模板,所以它可以将任何负载类型包装为成员变量。它基本上什么都不做:
      template <typename T>
      struct scientific_type {
          T value;

          explicit scientific_type(T val) : value{val} {}
      };
  1. 我们可以为之前包装到scientific_type中的任何类型定义完全自定义的格式设置,因为如果我们为其重载流operator>>,则流库在打印这些类型时会执行完全不同的代码。这样,我们可以用科学浮点表示法打印科学值,如果它们有正值,则使用大写格式和显式+前缀。我们也使用我们的format_guard类,以便在再次离开该功能时整理我们的所有设置:
      template <typename T>
      ostream& operator<<(ostream &os, const scientific_type<T> &w) {
          format_guard _;
          os << scientific << uppercase << showpos;
          return os << w.value;
      }
  1. 在主功能中,我们将首先玩format_guard类。我们打开一个新的范围,首先获取类的一个实例,然后我们将一些野生格式标志应用到std::cout:
      int main()
      {
          {
              format_guard _;
              cout << hex << scientific << showbase << uppercase;

              cout << "Numbers with special formatting:n";
              cout << 0x123abc << 'n';
              cout << 0.123456789 << 'n';
          }
  1. 在我们打印了一些启用了许多格式标志的数字后,我们再次离开了范围。发生这种情况时,format_guard的析构函数整理了格式。为了测试这一点,我们再次打印完全相同的数字*。它们应该看起来不同:*
          cout << "Same numbers, but normal formatting again:n";
          cout << 0x123abc << 'n';
          cout << 0.123456789 << 'n';
  1. 现在我们将scientific_type投入使用。让我们连续打印三个浮点数。我们将第二个数字包装在scientific_type中。这样,它以我们特殊的科学风格打印,但它前后的数字得到默认格式。同时,我们避免难看的格式化线噪音:
          cout << "Mixed formatting: "
               << 123.0 << " "
               << scientific_type{123.0} << " "
               << 123.456 << 'n';
      }
  1. 编译并运行程序会产生以下结果。前两个数字以特定格式打印。接下来的两个数字以默认格式出现,这向我们表明我们的format_guard工作得很好。最后一行的三个数字看起来也和预期的一样。中间只有一个有scientific_type的格式,其余有默认格式:
      $ ./pretty_print_on_the_fly 
      Numbers with special formatting:
      0X123ABC
      1.234568E-01
      Same numbers, but normal formatting again:
      1194684
      0.123457
      Mixed formatting: 123 +1.230000E+02 123.456

从 std::iostream 错误中捕获可读异常

在本章的食谱中,我们使用了例外来捕捉错误。虽然这当然是可能的,但是毫无例外地处理流对象已经非常方便了。如果我们试图解析 10 个值,但这在中间的某个地方失败了,整个流对象会将自己设置为失败状态,并停止进一步的解析。这样,我们就不会遇到从流中错误的偏移量解析变量的危险。我们可以只做条件句的解析,比如if (cin >> foo >> bar >> ...)。如果失败了,我们会处理的。在try { ... } catch ...块中包含解析似乎不是很有利。

事实上,在 C++ 中出现异常之前,C++ I/O 流库就已经存在了。后来增加了异常支持,这可能是为什么它们不是流库中一流的受支持特性的一个解释。

为了在流库中使用异常,我们必须单独配置每个流对象,以便在它将自己设置为失败状态时抛出异常。不幸的是,异常对象中的错误解释并没有完全标准化,我们可以稍后捕捉到这些错误解释。正如我们将在本节中看到的,这将导致实际上没有帮助的错误消息。如果我们真的想对流对象使用异常,我们可以另外轮询 C 库中的文件系统错误状态,以获得一些额外的信息。

在这一节中,我们将编写一个可能以不同方式失败的程序,处理那些有异常的程序,并看看以后如何从这些程序中挤出更多的信息。

怎么做...

我们将实现一个打开文件的程序(可能会失败),然后我们将从中读取一个整数(也可能会失败)。我们通过激活异常来实现这一点,然后看看如何处理这些异常:

  1. 首先,我们包含一些头,并声明我们使用std命名空间:
      #include <iostream>
      #include <fstream>
      #include <system_error>
      #include <cstring>      

      using namespace std;
  1. 如果我们想使用带有异常的流对象,我们必须首先启用它们。为了让文件流对象在我们允许它访问的文件不存在或者存在解析错误时抛出异常,我们需要在异常掩码中设置一些失败位。如果我们后来做了一些失败的事情,它将触发一个异常。通过激活failbitbadbit,我们可以为文件系统错误和解析错误启用异常:
      int main()
      {
          ifstream f;
          f.exceptions(f.failbit | f.badbit);
  1. 现在我们可以打开一个try块并访问一个文件。如果打开文件成功,我们会尝试从中读取一个整数。只有当这两个步骤都成功时,我们才打印整数:
          try {
              f.open("non_existant.txt");

              int i;
              f >> i;

              cout << "integer has value: " << i << 'n';
          }
  1. 在错误的两种预期可能性中,都会抛出一个std::ios_base::failure的实例。这个对象有一个what()成员函数,它应该解释是什么触发了异常。不幸的是,这个消息的标准化被遗漏了,它没有给出太多的信息。但是,我们至少可以区分是否存在文件系统问题(例如,因为文件不存在)或格式解析问题。甚至在 C++ 发明之前,全局变量errno就已经存在了,它被设置为一个错误值,我们现在可以检查一下。strerror函数将错误号翻译成人类可读的字符串。如果errno0,至少没有文件系统错误:
          catch (ios_base::failure& e) {
              cerr << "Caught error: ";
              if (errno) {
                  cerr << strerror(errno) << 'n';
              } else {
                  cerr << e.what() << 'n';
              }
          }
      }
  1. 编译程序并在两个不同的场景中运行它会产生以下输出。如果要打开的文件确实存在,但是无法从中解析一个整数,我们会收到一条iostream_category错误消息:
      $ ./readable_error_msg 
      Caught error: ios_base::clear: unspecified iostream_category error
  1. 如果文件存在,我们将从strerror(errno)收到不同的消息:
      $ ./readable_error_msg
      Caught error: No such file or directory

它是如何工作的...

我们已经看到,我们可以使用s.exceptions(s.failbit | s.badbit)为每个流对象s启用异常。这意味着,没有办法使用std::ifstream实例的构造函数来打开文件,如果我们想在无法打开文件时获得异常:

ifstream f {"non_existant.txt"};
f.exceptions(...); // too late for an exception

这是一个遗憾,因为异常实际上承诺,与老派的 C 风格代码相比,它们使错误处理变得不那么笨拙,后者充满了大量的if分支,每一步后都会处理错误。

如果我们试图激发各种原因导致流失败,我们会意识到没有不同的异常被抛出。这样,我们只能在得到错误时找出*,而不能找出具体有什么错误(这当然是不是通用中异常处理的真实情况,而是对 STL 流库的情况)。这就是我们额外咨询errno值的原因。这个全局变量是一个古老的构造,在没有 C++ 或一般异常的旧时代已经被使用。*

如果任何与系统相关的函数出现错误情况,它能够将errno变量设置为除0以外的其他值(0描述没有错误),然后调用者能够读取该错误号并查找其值的含义。唯一的问题是,当我们有一个多线程应用,并且所有线程都使用可以设置这个错误变量的函数时,它的错误值是多少?如果我们读取它,即使没有错误,它也可能携带错误值,因为在不同线程中运行的一些或其他系统功能可能遇到了错误。幸运的是,这个缺陷从 C++ 11 开始就消失了,在 c++ 11 中,进程中的每个线程都看到自己的errno变量。

在不详细说明一个古老的错误指示方法的起伏的情况下,当基于系统的事情(如文件流)触发异常时,它可以给我们提供有用的额外信息。异常告诉我们什么时候发生的,errno可以告诉我们如果发生在系统层面发生了什么。*****