跳转至

Input and Output (I/O)

输入和输出功能并未定义为核心 C++ 语言的一部分,而是通过 C++ 标准库提供(因此驻留在 std 命名空间中)。

  • 输入流用于保存来自数据生成器(例如键盘、文件或网络)的输入
    • 例如,用户可能按下键盘上的某个键,而程序当前不需要任何输入。数据不会忽略用户的按键,而是被放入输入流中,在那里它将等待,直到程序准备好
  • 输出流用于保存特定数据使用者的输出
    • 例如监视器、文件或打印机。将数据写入输出设备时,该设备可能尚未准备好接受该数据
    • 例如,当程序将数据写入其输出流时,打印机可能仍在预热。数据将位于输出流中,直到打印机开始使用它

I/O family tree

alt text

C++ 中的标准流

标准流是由计算机程序的环境提供给计算机程序的预连接流。 C++ 附带了四个预定义的标准流对象:

流名称 标准 对象类别 缓冲区
cin 标准输入 (keyboard) istream 有缓冲输出
cout 标准输出 (monitor) ostream 有缓冲输出
cerr 标准错误 (monitor) ostream 无缓冲输出
clog 标准日志 (monitor) ostream 有缓冲输出

非缓冲输出通常立即处理,而缓冲输出通常作为块存储和写出。因为 clog 不经常使用,所以它经常被从标准流列表中省略。

I/O and string stream

这一部分涉及到输入和输出的基本概念,包括换行符/制表符/... 以及使用get() / getline() / ...

太细节了,建议随用随查,不需要死记硬背

Basic file I/O

C++ 中的文件 I/O 的工作方式与普通 I/O 非常相似。

C++ 中有 3 个基本文件 I/O 类:

  1. ifstream(派生自 istream)文件输入
  2. ofstream(派生自 ostream)文件输出
  3. fstream(派生自 iostream)文件输入/输出

要使用文件 I/O 类,您需要包含 fstream 标头

如何对一个文件进行I/O操作(打开一个文件进行读取和/或写入):

  1. 实例化适当文件 I/O 类的对象,并将文件名作为参数即可
  2. 使用插入 (<<) 或提取 (>>) 运算符向文件写入或读取数据
  3. 完成后,有多种方法可以关闭文件:显式调用 close() 函数,或者只是让文件 I/O 变量超出范围(文件 I/O 类析构函数将为您关闭文件)

文件输出

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <fstream> // file obj related HRD
#include <iostream>

int main()
{
    // ofstream is used for writing files

    // (1) Init Obj: We'll make a file object called Sample.txt
    std::ofstream outf{ "Sample.txt" };

    // (2) Error Avoidance: If we couldn't open the output file stream for writing
    if (!outf)
    {
        // Print an error and exit
        std::cerr << "Uh oh, Sample.txt could not be opened for writing!\n";
        return 1;
    }

    // (3) File Input: We'll write two lines into this file
    outf << "This is line 1\n";
    outf << "This is line 2\n";

    return 0;

    // When outf goes out of scope, the ofstream
    // destructor will close the file
}

如果查看项目目录,您应该会看到一个名为 Sample.txt 的文件。如果您使用文本编辑器打开它,您将看到它包含了我们写入该文件的两行

alt text

文件输入

现在,我们将获取在上一个示例中编写的文件并将其从磁盘读回。

注意,如果到达文件末尾 (EOF),ifstream 将返回 0。一般来说,我们将利用这一事实来确定要阅读多少内容。

1)初级示范:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <fstream>
#include <iostream>
#include <string>

int main()
{
    // ifstream is used for reading files
    // (1) We'll read from a file called Sample.txt
    std::ifstream inf{ "Sample.txt" };

    // (2) If we couldn't open the output file stream for reading
    if (!inf)
    {
        // Print an error and exit
        std::cerr << "Uh oh, Sample.txt could not be opened for reading!\n";
        return 1;
    }

    // (3) While there's still stuff left to read
    std::string strInput{}; // (3.1) define output stream
    while (inf >> strInput) // (3.2) let file content flood into this output stream
        std::cout << strInput << '\n';

    return 0;

    // (4) When inf goes out of scope, the ifstream
    // destructor will close the file
}

输出结果

Bash
1
2
3
4
5
6
7
8
This
is
line
1
This
is
line
2

很显然这个结果非常不伦不类,按常理我们想把那两个句子直接输出,且泾渭分明。

出现上述的奇怪现象是因为:提取运算符会在空格处中断。为了读取整行,我们必须使用 getline() 函数。

2)改进示范:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <fstream>
#include <iostream>
#include <string>

int main()
{
    // ifstream is used for reading files
    // We'll read from a file called Sample.txt
    std::ifstream inf{ "Sample.txt" };

    // If we couldn't open the input file stream for reading
    if (!inf)
    {
        // Print an error and exit
        std::cerr << "Uh oh, Sample.txt could not be opened for reading!\n";
        return 1;
    }

    // While there's still stuff left to read
    std::string strInput{};
    while (std::getline(inf, strInput))
    std::cout << strInput << '\n';

    return 0;

    // When inf goes out of scope, the ifstream
    // destructor will close the file
}

本质上就是把流读入这一步骤进行了修正:

C++
1
2
inf >> strInput // previous
std::getline(inf, strInput) // fixed now

现在的输出

Bash
1
2
This is line 1
This is line 2

缓冲输出

缓冲是什么

C++ 中的输出可以被缓冲。这意味着输出到文件流的任何内容可能不会立即写入磁盘。相反,多个输出操作可以批量处理并一起处理。

这样做主要是出于性能原因。当将缓冲区中的剩余内容写入磁盘时,这称为刷新缓冲区。

刷新缓冲区的一种方法是关闭文件 —— 缓冲区的内容将被刷新到磁盘,然后文件将被关闭。

为什么要刷新缓冲区

缓冲通常不是问题,但在某些情况下,如果不小心,它可能会导致并发症。这种情况的罪魁祸首是当缓冲区中有数据时,程序立即终止(通过崩溃或调用 exit())。

在这些情况下,文件流类的析构函数不会被执行,这意味着文件永远不会关闭,这意味着缓冲区永远不会刷新。

在这种情况下,缓冲区中的数据不会写入磁盘,因此会永远丢失。这就是为什么 在调用 exit() 之前显式关闭所有打开的文件始终是一个好主意

如何操作(刷新缓冲区)

可以使用 ostream::flush() 函数手动刷新缓冲区或将 std::flush 发送到输出流。这些方法中的任何一种都有助于确保缓冲区的内容立即写入磁盘,以防程序崩溃。

std::endl\n

一个有趣的注释是:

  1. std::endl; 自带刷新输出流的属性
  2. \n 不会自动刷新输出流

因此,在执行刷新成本高昂的缓冲 I/O(例如写入文件)时,过度使用 std::endl(导致不必要的缓冲区刷新)可能会对性能产生影响。

一般来说,注重性能的程序员通常会使用 \n 而不是 std::endl 在输出流中插入换行符,以避免不必要的缓冲区刷新。

例1:不刷新缓冲区,数据会丢失

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <fstream>
#include <thread>
#include <chrono>

int main() {
    // create fileObject and open file
    std::ofstream outFile("example.txt");

    // write data
    for(int i = 1; i <= 5; i++) {
        outFile << "Line " << i << '\n';  // use \n rather than endl
        std::cout << "Writing line " << i << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

    // no close(); prog ending
    std::exit(0);  // data loss
}

命令行输出:

Bash
1
2
3
4
5
Writing line 1
Writing line 2
Writing line 3
Writing line 4
Writing line 5

文件输出:

Text Only
1
<!-- none -->

原因是:

  1. 写入进example.txtofstream是用\n的,因此它需要手动缓冲才能将缓冲区积累的内容输出到文件里
  2. 按常理,如果程序没有故障,它会在析构函数里自动执行(hidden)缓冲函数,将缓冲区的内容输出到文件里
  3. 但是,程序在std::exit(0)时,程序出现“故障”,直接终止!不会执行析构函数,因此缓冲区的内容直接消失

现在我们注意到两个问题

  1. 上述问题如何修复?如何将缓冲区的输出尽可能在程序崩溃前导入到文件中?
  2. 如果我将std::cout << "Writing line " << i << std::endl;改成std::cout << "Writing line " << i << '\n';会如何?
    • 会不会在CLI中也出现“啥也没有”的现象?

例2:修复上述问题,手动刷新缓冲区

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <fstream>
#include <thread>
#include <chrono>

int main() {
    std::ofstream outFile("example.txt");

    for(int i = 1; i <= 5; i++) {
        outFile << "Line " << i << '\n';

        if(i == 3) {  // flush at line 3
            outFile.flush();
            std::cout << "Buffer flushed at line 3" << std::endl;
        }

        std::cout << "Writing line " << i << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

    // outFile.close();  // 正确关闭文件
    exit(0);    
    // return 0;
}

CLI输出:

Bash
1
2
3
4
5
Writing line 1
Writing line 2
Writing line 3
Writing line 4
Writing line 5

文件输出:

Text Only
1
2
3
Line 1
Line 2
Line 3

这次做的改进是在line3手动将当前缓冲区内的内容导入文件,然而line4和5的内容并没有被写入文件,这是因为程序在line5处被终止了,没有执行到文件关闭的操作,因此文件没有被正确关闭,文件的内容也没有被正确写入文件

不过这个例子已经很好的说明了“手动管理缓冲区”的方式

例3: 考察cout那行将 std::endl; 变成 \n 会怎样

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <fstream>
#include <thread>
#include <chrono>

int main() {
    // create fileObject and open file
    std::ofstream outFile("example.txt");

    // write data
    for(int i = 1; i <= 5; i++) {
        outFile << "Line " << i << '\n';  // use \n rather than endl
        std::cout << "Writing line " << i << '\n';
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

    // no close(); prog ending
    std::exit(0);  // data loss
}

CLI输出:

C++
1
2
3
4
5
Writing line 1
Writing line 2
Writing line 3
Writing line 4
Writing line 5

这个例3的任何结果和例1的完全一致

我都将行结尾的 std:endl; 改成 \n,为什么CLI中并没出现“啥也没有”的现象?

文件模式

现在我们会思考:如果我们尝试写入已经存在的文件会发生什么?

上述的例子如果依次执行,会发现:每次运行程序时,原始文件都被完全覆盖。相反,如果我们想将更多数据附加到文件末尾怎么办?

事实证明,文件流构造函数采用可选的第二个参数,该参数允许您指定有关如何打开文件的信息。该参数称为 mode,它接受的有效标志存在于 ios 类中。

Ios file mode Meaning
app Opens the file in append mode
ate Seeks to the end of the file before reading/writing
binary Opens the file in binary mode (instead of text mode)
in Opens the file in read mode (default for ifstream)
out Opens the file in write mode (default for ofstream)
trunc Erases the file if it already exists

例1:将另外两行附加到我们之前创建的 Sample.txt 文件中

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <fstream>

int main()
{
    // We'll pass the ios:app flag to tell the ofstream to append
    // rather than rewrite the file. We do not need to pass in std::ios::out
    // because ofstream defaults to std::ios::out
    std::ofstream outf{ "Sample.txt", std::ios::app };

    // If we couldn't open the output file stream for writing
    if (!outf)
    {
        // Print an error and exit
        std::cerr << "Uh oh, Sample.txt could not be opened for writing!\n";
        return 1;
    }

    outf << "This is line 3\n";
    outf << "This is line 4\n";

    return 0;

    // When outf goes out of scope, the ofstream
    // destructor will close the file
}

现在,如果我们看一下 Sample.txt(使用上述示例程序之一打印其内容,或将其加载到文本编辑器中):

Text Only
1
2
3
4
This is line 1
This is line 2
This is line 3
This is line 4

例2: 使用 open() 显式打开文件

就像可以使用 close() 显式关闭文件流一样,也可以使用 open() 显式打开文件流。

open() 的工作方式与文件流构造函数类似 —— 需要一个文件名和一个可选的文件模式。

格式:

C++
1
std::open(filename, mode);

例子:

C++
1
2
3
4
5
6
7
8
9
std::ofstream outf{ "Sample.txt" };
outf << "This is line 1\n";
outf << "This is line 2\n";
outf.close(); // explicitly close the file

// Oops, we forgot something
outf.open("Sample.txt", std::ios::app);
outf << "This is line 3\n";
outf.close();

more about open() here