Skip to content

Latest commit

 

History

History
384 lines (266 loc) · 15.5 KB

File metadata and controls

384 lines (266 loc) · 15.5 KB

八、处理控制台输入/输出和文件

本章介绍了使用 C++ 标准库基于控制台、流和文件输入/输出的方法。我们已经在其他章节中向我们编写的程序中读取了参数,但是还有其他几种方法可以做到这一点。我们将深入探讨这些主题,并通过具体的、专门的实践方法学习每一个主题的替代方法、技巧和最佳实践。

再说一次,我们的主要重点是尽可能多地使用 C++(及其标准库)来编写系统编程软件,因此代码将具有非常有限的 C 和 POSIX 解决方案。

本章将涵盖以下主题:

  • 实现控制台的输入/输出
  • 操纵输入输出字符串
  • 使用文件

技术要求

为了让您从一开始就尝试这些程序,我们设置了一个 Docker 映像,其中包含了我们在整本书中需要的所有工具和库。它基于 Ubuntu 19.04。

要进行设置,请执行以下步骤:

  1. www.docker.com下载并安装 Docker 引擎。

  2. 从 Docker 中心拉出图像:docker pull kasperondocker/system_programming_cookbook:latest

  3. 图像现在应该可以使用了。输入以下命令查看图像:docker images

  4. 你现在应该有这个图像了:kasperondocker/system_programming_cookbook

  5. 借助以下命令,使用交互式外壳运行 Docker 映像:docker run -it **-**-cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash

  6. 运行容器上的外壳现已可用。使用root@39a5a8934370/# cd /BOOK/获取我们在整本书中开发的所有程序,按章节组织。

需要--cap-add sys_ptrace参数来允许 Docker 容器中的 GDB 设置断点,这是 Docker 默认不允许的。

实现控制台的输入/输出

这个食谱主要关注控制台输入/输出。我们编写的大多数程序都需要与用户进行某种交互:我们需要获取输入,进行一些处理,并返回输出。例如,考虑一下您可以在将要构建的应用中收集的用户输入。在本食谱中,我们将编写代码,展示从控制台获取输入并返回输出的不同方法。

怎么做...

让我们写一些代码:

  1. 随着 Docker 映像的运行,让我们创建一个名为console_01.cpp的新文件,并在其中键入以下代码:
#include <iostream>
#include <string>
int main ()
{
    std::string name;
    std::cout << "name: ";
    std::cin >> name;

    std::string surname;
    std::cout << "surname: ";
    std::cin >> surname;

    int age;
    std::cout << "age: ";
    std::cin >> age;

    std::cout << "Hello " << name << ", " 
              << surname << ": " << age << std::endl;
    return 0;
}
  1. 现在创建另一个名为console_02.cpp的文件,并输入该代码以查看这种方法的局限性:
#include <iostream>
#include <string>
int main ()
{
    std::string fullNameWithCin;
    std::cout << "full Name got with cin: ";
    std::cin >> fullNameWithCin;

    std::cout << "hello " << fullNameWithCin << std::endl;
    return 0;
}
  1. 最后,让我们创建一个新文件并命名为console_03.cpp;让我们看看std::getlinestd::cin如何克服之前的这个限制:
#include <iostream>
#include <string>

int main ()
{
    std::string fullName;
    std::cout << "full Name: ";
    std::getline (std::cin, fullName);
    std::cout << "Hello " << fullName << std::endl;
    return 0;
}

虽然这些都是非常简单的例子,但是它们展示了与控制台标准输入和输出交互的 C++ 方式。

它是如何工作的...

第一步,console_01.cpp程序只是使用std::cinstd::cout获取用户的namesurname信息,保存在std::string变量中。当需要与标准输入和输出进行简单的交互时,首先要使用这些东西。通过构建和运行console_01.cpp文件,我们将获得以下输出:

食谱的第二步显示std::cinstd::cout的限制。用户将命令行中的namesurname赋予编程的运行过程,但奇怪的是,只是名字存储在fullNameWithCin变量中,完全跳过了姓氏。怎么会这样原因很简单:std:cin始终将空格、制表符或换行符视为从标准输入中捕获的值的分隔符。那么,我们如何从标准输入中获得完整的行呢?通过编译运行console_02.cpp,我们得到如下结果:

第三步显示了结合使用getline功能和std::cin从标准输入中获取整行。std::getlinestd::cin获取该行,并将其存储在fullName变量中。一般来说,std::getline接受任何std::istream作为输入,可以指定分隔符。标准库中可用的原型如下:

istream& getline (istream& is, string& str, char delim);
istream& getline (istream&& is, string& str, char delim);
istream& getline (istream& is, string& str);
istream& getline (istream&& is, string& str);

这些使得getline成为一个非常灵活的方法。通过构建和运行console_03.cpp,我们得到如下输出:

让我们看一下下面的示例,其中我们将一个流传递给方法、存储提取的信息的变量和分隔符:

#include <iostream>
#include <string>
#include <sstream>

int main ()
{
    std::istringstream ss("ono, vaticone, 43");

    std::string token;
    while(std::getline(ss, token, ','))
    {
        std::cout << token << '\n';
    }

    return 0;
}

上述方法的输出如下:

这可以为构建您自己的标记器方法奠定基础。

还有更多...

std::cinstd::cout允许链请求,这使得代码更加易读和简洁:

std::cin >> name >> surname;
std::cout << name << ", " << surname << std::endl;

std::cin期望用户先传自己的名字,再传自己的姓氏。它们必须用空格、制表符或换行符隔开。

请参见

  • 学习如何操作输入/输出字符串食谱涵盖了如何操作字符串作为控制台输入/输出的补充。

学习如何操作输入输出字符串

字符串操作几乎是任何软件的一个非常重要的方面。能够简单有效地操作字符串是软件开发的一个关键方面。如何读取或解析应用的配置文件?这个食谱将教你 C++ 提供了什么工具来让std::stringstream课成为一个愉快的任务。

怎么做...

在本节中,我们将通过使用std::stringstream来解析流来开发一个程序,流实际上可以来自任何来源:文件、字符串、输入参数等。

  1. 让我们开发一个打印文件所有条目的程序。在一个新的 CPP 文件中输入以下代码,console_05.cpp:
#include <iostream>
#include <string>
#include <fstream>

int main ()
{
    std::ifstream inFile ("file_console_05.txt", std::ifstream::in);
    std::string line;
    while( std::getline(inFile, line) )
        std::cout << line << std::endl;

    return 0;
}
  1. std::stringstream在我们必须将字符串解析成变量时非常方便。让我们通过在一个新文件console_06.cpp中编写以下代码来看看这一点:
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>

int main ()
{
    std::ifstream inFile ("file_console_05.txt",
        std::ifstream::in);
    std::string line;
    while( std::getline(inFile, line) )
    {
        std::stringstream sline(line);
        std::string name, surname; 
        int age{};
        sline >> name >> surname >> age;
        std::cout << name << "-" << surname << "-"<< age << 
            std::endl;
    }
    return 0;
}
  1. 此外,为了补充第二步,解析和创建字符串流也很容易。让我们在console_07.cpp中进行:
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>

int main ()
{
    std::stringstream sline;
    for (int i = 0; i < 10; ++ i)
        sline << "name = name_" << i << ", age = " << i*7 << 
            std::endl;

    std::cout << sline.str();
    return 0;
}

前面三个程序展示了用 C++ 解析字符串是多么简单。下一节将逐步解释它们。

它是如何工作的...

步骤 1 显示std::getline接受任何流作为输入,而不仅仅是标准输入(即std::cin)。在这种情况下,它从文件中获取流。我们包括std::coutiostreamstring可以使用字符串、fstream可以读取文件。

然后,我们使用std::fstream(文件流)打开file_console_05.txt文件。在它的构造函数中,我们传递文件名和标志(在这种情况下,只是带有std::ifstream::in的输入文件的信息)。我们将文件流传递给std::getline,T3 将负责从流中复制每一行,并将其存储在刚刚打印的std::string变量line中。该程序的输出如下:

第 2 步显示了相同的程序读取file_console_05.txt文件,但是,这次我们要解析文件的每一行。我们通过将line字符串变量传递给sline std::stringstream变量来实现。std::stringstream提供方便易用的解析功能。

通过只写行sline >> name >> surname >> agestd::stringstream类的operator>>将把namesurnameage保存到各自的变量中,注意类型转换(也就是说,对于age变量,从stringint),假设这些变量在文件中以该顺序出现。operator>>将解析该字符串,并通过跳过前导的空格 *,*将为每个标记调用适当的方法(例如,basic_istream& operator>>( short& value );basic_istream& operator>>( long long& value );等)。该程序的输出如下:

第 3 步显示了将流解析成变量的简单性同样适用于构建流。相同的std::stringstream变量sline<<运算符一起使用,表示数据流现在流向string stream变量,该变量在下面的截图中以两行打印到标准输出。正如预期的那样,该程序的输出如下:

std::stringstream使得解析字符串和流变得非常容易,无论它们来自哪里。

还有更多...

如果您正在寻找低延迟,使用std::stringstream进行流操作可能不是您的首选。我们始终建议您衡量绩效,并根据数据做出决定。如果是这样,您可以尝试不同的解决方案:

  • 如果可以的话,只需关注要优化的代码的低延迟部分。
  • 使用标准的 C 或 C++ 方法来解析数据,例如典型的atoi()方法来编写您的层。
  • 使用任何开源的低延迟框架。

请参见

  • 实现控制台输入输出的方法包括如何处理控制台输入输出。

使用文件

这个食谱将教会你处理文件所需的基本知识。C++ 标准库在历史上提供了一个非常好的接口,但是 C++ 17 增加了一个名为std::filesystem的命名空间,这进一步丰富了这个功能。不过,我们不会利用 C++ 17 std::filesystem命名空间,因为它已经在第 2 章中介绍过了。考虑一个创建配置文件的具体用例,或者需要复制该配置文件的地方。这个食谱将教你 C++ 如何让这个任务变得容易。

怎么做...

在本节中,我们将编写三个程序来学习如何使用std::fstreamstd::ofstreamstd::ifstream处理文件:

  1. 让我们使用std::ofstream开发一个打开并写入新文件file_01.cpp的程序:
#include <iostream>
#include <fstream>

int main ()
{
    std::ofstream fout;
    fout.open("file_01.txt");

    for (int i = 0; i < 10; ++ i)
        fout << "User " << i << " => name_" << i << " surname_" 
            << i << std::endl;

    fout.close();
}
  1. 在一个新的源文件file_02.cpp中,让我们从一个文件中读取并打印到标准输出:
#include <iostream>
#include <fstream>

int main ()
{
    std::ifstream fiut;
    fiut.open("file_01.txt");

    std::string line;
    while (std::getline(fiut, line))
        std::cout << line << std::endl;

    fiut.close();
}
  1. 现在,我们希望将打开文件的灵活性与读写结合起来。我们将使用std::fstreamfile_01.txt的内容复制到file_03.txt中,然后打印其内容。在另一个源文件file_03.cpp中,键入以下代码:
#include <iostream>
#include <fstream>

int main ()
{
    std::fstream fstr;
    fstr.open("file_03.txt", std::ios::trunc | std::ios::out | std::ios::in);

    std::ifstream fiut;
    fiut.open("file_01.txt");
    std::string line;
    while (std::getline(fiut, line))
        fstr << line << std::endl;
    fiut.close();

    fstr.seekg(0, std::ios::beg);
    while (std::getline(fstr, line))
        std::cout << line << std::endl; 
    fstr.close();
}

让我们看看这个食谱是如何工作的。

它是如何工作的...

在深入研究前面三个程序之前,我们必须阐明标准库是如何针对文件流构建的。让我们看看下表:

| | | <fstream> | | <ios> | | ofstream | | <ios> | | ifstream |

让我们把它分解如下:

  • <ostream>:负责输出流的 streams 类。
  • <istream>:负责输入流的 streams 类。
  • ofstream:流类,用于写入文件。出现在fstream头文件中。
  • ifstream:流类,用于读取文件。出现在fstream头文件中。

std::ofstreamstd::ifstream分别继承自std::ostreamstd::istream的泛型流类。可以想象,std::cinstd::cout也是从std::istreamstd::ostream继承而来的(上表未显示)。

第一步:我们首先要做的就是包含<iostream><fstream>,以便使用std::coutstd::ofstream读取file_01.txt文件。然后我们调用open方法,在这种情况下,它以写入模式打开文件,因为我们使用的是std::ofstream类。我们现在准备用<<操作符将字符串写入fout文件流。最后,我们必须关闭流,这将最终关闭文件。通过编译和运行程序,我们将获得以下输出:

第二步:这种情况下我们反其道而行之:从file_01.txt文件中读取,打印到标准输出。在这种情况下,唯一的区别是我们使用了std::ifstream类,它代表一个读取文件流。通过调用open()方法,文件以读取模式(std::ios::in)打开。通过使用std::getline方法,我们可以打印到标准输出文件的所有行。输出如下所示:

最后的第三步展示了std::fstream类的用法,通过允许我们以读写模式打开文件(std::ios::out | std::ios::in)给了我们更多的自由。如果文件存在,我们也要截断它(std::ios::trunc)。有更多的选择可以传递给std::fstream建造者。

还有更多...

C++ 17 通过在标准库中添加std::filesystem进行了巨大的改进。它并不是全新的——它受到了 Boost 库的极大启发。曝光的主要公众成员如下:

| 方法名称 | 描述 | | path | 表示路径 | | filesystem_error | 文件系统错误异常 | | directory_iterator | 目录内容的迭代器(递归版本也可用) | | space_info | 关于文件系统上可用空间的信息 | | perms | 标识文件系统权限系统 |

std::filesystem命名空间中,也有给出文件信息的辅助函数,如is_directory()is_fifo()is_regular_file()is_socket()等。

请参见

  • 第二章中的理解文件系统配方,重温 C++ ,给出了这个主题的复习。