浅谈 C/C++ 的输入输出

2023-09-04 15:05:42 浏览数 (1)

本文最后更新于 189 天前,其中的信息可能已经有所发展或是发生改变。

0. 叠甲,过


本人水平有限,语言组织能力低下,不保证绝佳的阅读体验,也不保证内容完全准确,如有错误和建议,欢迎指出。才怪


1. 谈谈输入输出缓冲区


1.1 基本概念


你先别急,我知道你很急,但是别急,所以你先别急

在了解输入输出输出缓冲区时,需要明确以下几个基本概念:

  • 输入输出流
  • 标准输入输出流
  • 文件输入输出流

输入输出流

  • 输入输出流是一种数据传输的概念。
  • 构成计算机的其中之一部件为 I/O 设备,指的是用于从程序内部向外部设备(屏幕、打印机等)或从外部设备向程序内部传输数据的设备(鼠标、键盘等);
  • 计算机中通过 I/O 设备进行与用户之间的数据交互,而为了适应不同的设备之间数据的传输,提出了输入输出流的概念。
  • 即,输入输出流就是一种统一的数据输入输出协议,为不同的设备之间传递数据时提供一致的接口

标准输入输出流

  • 标准输入输出流是指程序与外部设备(例如键盘和显示器)之间的输入输出
  • C 语言中:
    • C 标准库中,标准输入流输出流分别是 stdinstdout,另外还有标准错误流 stderr
    • 使用 <stdio.h> 头文件里的 scanf() 函数和 printf() 函数。
  • C 语言中:
    • C 标准库中,没有 stdin 这样的标准输入流,而是使用 std::cinstd::out 来进行标准输入和标准输出。
    • 使用 <iostream.h> 头文件里的 getline() 函数或是 >><< 操作符。
  • 综上,在 C 中,输入输出流的使用通常是通过 iostream 库实现的,而在 C 中则是通过 stdio 库实现的。

文件输入输出流

  • 文件输入输出流则是将数据保存在磁盘上的文件中,通过打开和关闭文件,程序可以使用文件输入输出流进行数据的读取和写入。
  • C 语言中:
    • 文件输入输出流使用 C 标准库中的文件指针 FILE* 来实现。
    • 操作函数有 fopen(), fclose(), fread(), fwrite() 等。
  • C 语言中:
    • 文件输入输出流是基于 C 标准库中的文件操作函数封装而成,即 fstream 类。
    • 具体地,通过 std::ifstreamstd::ofstream 类实现,它们是 std::istreamstd::ostream 类的派生类。

相比标准输入输出流,文件输入输出流需要显式地指定要读写的文件,因此使用起来比较繁琐,但也更加灵活:文件输入输出流可以处理任何类型的文件,包括文本文件和二进制文件,而标准输入输出流只能处理字符流。此外,文件输入输出流可以通过随机访问文件的方式读写文件,而标准输入输出流只能顺序读写。


1.2 输入输出缓冲区


什么是输入输出缓冲区?

顾名思义,输入输出缓冲区就是输入输出缓冲的区域

C/C 中,输入输出缓冲区是用来存储输入输出数据的临时存储区域

  • 输入缓冲区是在数据流输入之前存储输入数据的临时存储区域。
  • 输出缓冲区是在数据流输出之前存储输出数据的临时存储区域。

说人话:输入输出缓冲区就是为了保存这些输入输出流而临时开辟出的一块内存


为什么要设置输入输出缓冲区?

众嗦粥汁,因为需要,所以设置

  • 缓冲区是在内存中,而外设则是在硬件中。
  • 相比于从硬件中读取和写入数据,从内存中读取和写入数据更加快速。

因此,当程序需要读取或写入大量数据时,使用缓冲区可以将这些数据先存储到内存中,然后再一次性地写入或读取,避免了频繁访问硬件的开销。此外,缓冲区还可以优化数据的排列和格式,以便更高效地读取和写入数据。

说人话:缓冲区的存在是为了提高输入输出效率,减少对外设的访问次数


C/C 的输入输出缓冲区有何不同?

别急别急别急

首先别急,其次别急,所以我们先来了解下:输入输出缓冲区的空间由什么来分配?开辟在哪里?何时开辟?这个问题:

  • 输入输出缓冲区的空间通常由操作系统来分配的;
  • 一般情况下,是在程序运行时从内存中分配的,在程序运行空间中分配的,不是在操作系统的内核空间中分配;
  • 分配的时机和分配的空间大小会根据具体的实现而不同,一般地,当程序通过输入输出函数向缓冲区写入或者读取数据时,缓冲区就会被分配。

具体地

  • 分配缓冲区的时机:
    • 对于标准输入输出流:缓冲区的空间通常是在程序启动时预先分配好的。
    • 对于文件输入输出流:缓冲区的空间是在文件流和流缓冲区对象创建时动态分配的,这些对象通常是在程序开始时被初始化的。
    • 缓冲区的大小通常是由实现细节所决定的,但是一般来说,缓冲区的大小应该足够容纳输入或输出数据的常规大小,同时又不能过大以致于浪费内存。
  • 分配缓冲区的大小:
    • 缓冲区的大小应该足够容纳输入或输出数据的常规大小,同时又不能过大以致于浪费内存
    • 由实现库来完成对缓冲区大小的分配,具体实现细节可能会因编译器或操作系统的不同而有所差异。
    • 一般来说,实现库会通过调用操作系统提供的系统调用或动态内存分配函数来分配缓冲区的空间。
    • 在内存空间紧张的情况下,缓冲区的大小可能会被限制,从而可能影响到程序的性能和可靠性。

急急急急急急

我知道你急了,但是你先别急,这部分其实不用太纠结。的。对吧:

  • C 语言中,标准输入输出库 <stdio.h> 提供了输入输出缓冲区的实现。
    • 主要使用了三个函数:setbuf()setvbuf()fflush()
    • 其中,setbuf()setvbuf() 可以用来设置缓冲区,而 fflush() 用来清空缓冲区并把缓冲区中的数据输出到文件。
    • 因此,C 中的输入输出函数,如 scanf()printf() 等,是非类型安全的
    • 它们依赖于格式化字符串来指示输入/输出数据的类型。
    • 如果格式化字符串不正确,就会导致不可预测的结果,如缓冲区溢出和未定义的行为。
  • C 中,<iostream> 库提供了输入输出缓冲区的实现。
    • 提供了两种不同的缓冲区:streambuffilebuf
    • streambuf<iostream> 库的基类,提供了对输入输出缓冲区的访问;而 filebuf<fstream> 库的基类,提供了对文件输入输出缓冲区的访问。
    • 但是,<iostream> 库还提供了一些类似 setbuf()setvbuf()flush() 等函数,用来管理输入输出缓冲区。在关闭同步流之后,<iostream> 库使用了一种不同于标准输入输出库的机制来提高效率,例如使用字符串流 stringstream 和缓冲流 buffer stream 等。
    • 因此,C 中的输入输出函数,如 std::cinstd::cout 等,是类型安全的
    • 它们使用类型安全的 C 流语义,其中数据类型是静态确定的,而不是动态确定的。
    • 这意味着数据类型在编译时就已经确定,而不是在运行时根据格式化字符串动态确定。
    • 这种静态类型检查可以在编译时检测到类型不匹配的错误,从而使 C 的输入输出更加类型安全。

这就是为什么,你仍然可以在 C 中使用 scanf()printf(),但是仍建议在 C 中使用 <iostream> 库所提供的标准输入输出的原因,以及为什么我们常说 C C 更适于面向对象。

总结:这部分真的不用太纠结。中肯的。正确的。理智的。一针见血的。真的。


2. 谈谈输入输出的方式


2.1 C/C 的输入和输出


你急了,你急了,你急了,因为你很迷,你不明白 stdinscanfcinstd::cingetlinestringstream 还有 stdoutprintfcoutstd::cout 这些都是什么寄吧玩意,对吧?再来明确一下:

  • stdinC 语言中的标准输入流。
  • cinC 中的标准输入流,而 std::cinC 标准库命名空间中的标准输入流,cin 是使用命名空间 std 的缩写,即cinstd::cin 的别名。
  • scanf()C 语言中的输入函数,而 cinstd::cinC 中的输入流。scanf() 的参数需要使用格式化字符串来指定输入数据的类型,而 cinstd::cin 可以自动识别输入数据的类型。
  • getline()C 中的输入函数,可以用于从输入流中读取一行文本数据,可以指定分隔符。getline() 可以替代 scanf()cin 用于读取字符串类型数据。
  • stdoutC 语言中的标准输出流。
  • coutC 中的标准输出流,而 std::coutC 标准库命名空间中的标准输出流。它们之间的区别同 cinstd::cin
  • printf()C 语言中的输出函数,而 coutstd::coutC 中的输出流。printf() 的参数需要使用格式化字符串来指定输出数据的类型,而 coutstd::cout 可以自动识别输出数据的类型。
  • 至于 stringstream 这个东西,我们放到最后细嗦。

scanf() 和 printf()

因为我们对这两个东西再熟悉不过了,所以我们对这两个东西根本不陌生,这俩是 C 语言中的标准输入和标准输出函数。

对于 printf(),只需要注意下面几点:

  • 用法:scanf(format, argument_list);
  • 用于向控制台输出数据,可以输出多种类型的数据,如整数、浮点数、字符、字符串等。
  • 在输出字符串时,需要注意字符串中是否包含特殊字符,如换行符、制表符等,需要使用相应的转义字符来表示。
  • 可以使用格式化输出来控制输出的格式,如输出精度、对齐方式等。

而对于 scanf(),除了基本注意点:

  • 用法:scanf(format, argument_list);
  • 用于从控制台输入数据,可以读取多种类型的数据,如整数、浮点数、字符、字符串等。
  • scanf() 输入数据时要求数据格式与 format 字符串中指定的格式匹配,否则会产生错误。

还需要注意:scanf() 函数的缓冲区不会自动清空,因此需要使用fflush(stdin)语句清空缓冲区,以防止输入的数据被下一个输入函数接收,如果仅仅为了处理掉换行符 n,可以使用 getchar() 读取,将换行符“吃掉”。

举个栗子

观察下列代码:

代码语言:javascript复制
#include <stdio.h>

int main(){
    int n;                  //声明 int 类型变量 n
    scanf("%d", &n);        //读入 int 类型变量 n
    printf("%dn", n);      //输出 int 类型变量 n 并且换行
    char c = getchar();     //读入一个字符,并存储在 char 类型变量 c 中
    printf("%c", c);        //输出 char 类型变量 c
    printf("14n");         //输出 14 并且换行
    return 0;
}

假设运行并且在控制台输入如下内容:

代码语言:javascript复制
114
5

理论上,我期望得到输出:

代码语言:javascript复制
114
514

但实际上,控制台哼哼哼啊啊啊输出了如下内容:

代码语言:javascript复制
114

14

甚至控制台根本就没有接收你后续输入的 5 这个字符。

在该例子中,scanf("%d", &n)会读取输入流中的数字 114,并将其存储在变量 n 中。但是,由于输入缓冲区中还有一个换行符 ngetchar()函数会读取这个换行符,并存储在变量 c 中,导致产生了这样的结果。在缓冲区中的数据没有被自动清空,这就是为什么控制台根本没有鸟你后续输入的东西,并输出了不符合预期的内容。

那么继续观察如下代码:

代码语言:javascript复制
#include <stdio.h>

int main(){
    int n;                  //声明 int 类型变量 n
    scanf("%d", &n);        //读入 int 类型变量 n
    printf("%dn", n);      //输出 int 类型变量 n 并且换行
    getchar();              //用 getchar() 吃掉缓冲区中的 'n'
    char c = getchar();     //读入一个字符,并存储在 char 类型变量 c 中
    printf("%c", c);        //输出 char 类型变量 c
    printf("14n");         //输出 14 并且换行
    return 0;
}

重新编译运行并在控制台输入如下内容:

代码语言:javascript复制
114
5

可以发现控制台哼哼哼啊啊啊输出了:

代码语言:javascript复制
114
514

在该例子中,为了避免上述缓冲区没有清空的情况,我们在读取完数据后手动清空输入缓冲区,利用 getchar() 读取了缓冲区里的换行符 n,使得后续的字符 5 被成功读入,最终输出了符合预期的内容。


cin 和 cout

cincoutC 的输入输出流,可以使用它们来实现控制台的输入输出操作。一般地,使用 cincout 时可以通过引入 using namespace std; 简化代码,但也可以不引入命名空间,使用完整限定名 std::cinstd::cout

由于 cincout 的输入输出会自动匹配对应数据类型,所以针对这两者的格式化输入输出并非此处讨论的重点,而在此处,我们需要提及其关于同步流(synchronized stream)的概念:

  • 同步流意味着在程序流中输出数据时,程序必须等到数据完全输出到设备上,然后才能继续执行后面的代码。
  • 同样,当程序尝试从输入设备读取数据时,程序会等待用户输入完整的数据,然后才能继续执行后面的代码。

虽然同步流可以确保输入输出的正确性,但是在一些场景下会影响程序的效率,特别是在大量数据输入输出的情况下。

这就是为什么,即使 C 宁愿舍弃 scanf()printf() 的高性能,也要得到输入输出流同步所带来的安全性和正确性,这也使得 C 更适合面向对象开发。

注意

  • scanf()printf() 也存在同步流机制,但其缓冲区的实现更为底层,效率更高。
  • 除此之外,cincout 的类型检查机制以及其他各种操作也是影响其性能的因素之一。

getchar() 和 getline()

把这两个放一起存粹是因为他们长得很像,但是两者天差地别:

  • getchar()函数从标准输入(stdin)中读取一个字符,返回该字符的 ASCII 码值。
  • 通常用于读取单个字符或者字符数组,可以实现简单的输入操作。
  • 使用时需要注意的是,由于输入的字符是直接通过键盘输入的,因此需要按下回车键才能将输入的字符送入缓冲区,此时getchar()才能够读取到输入的内容。
  • getline()函数从输入流中读取一行文本,并将其存储到一个字符串对象中,可以读取包含空格在内的一整行输入。
  • 使用时需要注意的是,如果使用默认的分隔符 ngetline() 会将换行符读取到缓冲区,如果下一次使用 getline() 读取输入,就会导致缓冲区中的换行符被读取,而不是期望的输入。此时可以通过调用cin.ignore()来清除缓冲区中的字符,或者指定其他分隔符。

关于 getchar() 缓冲区的问题已经讲过,下面举个 getline() 的栗子:

观察下列代码:

代码语言:javascript复制
#include <iostream>
#include <string>

using namespace std;

int main() {

    string s; 

    getline(cin, s);  //读入 string 类型 s

    cout << "First: " << s << endl;  //输出 s

    getline(cin, s);  //在此读入

    cout << "Second: " << s << endl;  //再次输出 s

    return 0;
}

假设运行并且在控制台输入如下内容:

代码语言:javascript复制
114
514

理论上,我期望得到输出:

代码语言:javascript复制
First: 114
Second: 514

但实际上,控制台哼哼哼啊啊啊输出了如下内容:

代码语言:javascript复制
First: 114
Second: 514

你会惊讶地发现符合期望,然后你想:“诶这不是没毛病垃圾 Lys 玩我呢?”

你先别急,让我先急。

getline() 其参数实际上有三个,第三个参数为分隔符参数,即 getline() 会以该参数分割处理数据,默认缺省该参数的情况下,getline() 会以 n 为分隔符,即默认我们使用的是 getline(cin, s, 'n');

那么在该例子中,输入 114 后按下回车键,该回车键被视为一个分隔符并从输入流中删除,此时 n 仍然留在缓冲区中 。然后第二个 getline() 调用会读取缓冲区中剩余的字符,即 "n514",将其中的 n 删除并存储 514。因此输出符合预期。

我们重新指定一下 getline() 的分隔符,修改得到如下代码:

代码语言:javascript复制
#include <iostream>
#include <string>

using namespace std;

int main() {

    string s; 

    getline(cin, s, ',');  //读入 string 类型 s,并以 ',' 为分隔符

    cout << "First: " << s << endl;  //输出 s

    getline(cin, s, ',');  //在此读入

    cout << "Second: " << s << endl;  //再次输出 s,并以 ',' 为分隔符

    return 0;
}

假设运行并且在控制台输入如下内容:

代码语言:javascript复制
114,
514,

理论上,我期望得到输出:

代码语言:javascript复制
First: 114
Second: 514

但实际上,控制台哼哼哼啊啊啊输出了如下内容:

代码语言:javascript复制
First: 114
Second: 
514

你会惊讶地发现这次不符合期望了,然后你想:“诶这不废话吗垃圾 Lys 玩我呢?”

你急了,但是你先别急。

在该例子中,输入 114, 后按下回车键,',' 则被视为了一个分隔符并从输入流中删除,但后续输入的 n 保留在了缓冲区中 。然后第二个 getline() 调用会读取缓冲区中剩余的字符,即 "n514,",将其中的 ',' 删除并存储 n514。因此输出了不符合预期的内容。

为了避免这种结果,我们同样需要手动清空缓存区,可以使用 getchar() “吃掉”缓冲区中的 n,但更建议使用如下方法:

代码语言:javascript复制
#include <iostream>
#include <string>

using namespace std;

int main() {

    string s; 

    getline(cin, s, ',');  //读入 string 类型 s

    cout << "First: " << s << endl;  //输出 s

    // 使用 cin.ignore() 忽略掉输入缓冲区中的换行符
    // 也可以使用 cin.get() 读取缓冲区中的换行符
    cin.ignore();
    // cin.get();

    getline(cin, s, ',');  //在此读入

    cout << "Second: " << s << endl;  //再次输出 s

    return 0;
}

最终得到了符合预期的结果。

代码语言:javascript复制
First: 114
Second: 514

总体而言,getchar()适用于读取单个字符或者字符数组,而getline()适用于读取一整行文本,两者使用时需要注意不同的输入方式和缓冲区处理


stringstream

  • stringstreamC 标准库提供的一种数据流对象,用于在内存中对字符串进行输入输出操作。
  • 它可以像 cincout 一样进行输入输出,并且具有和输入输出流相似的接口和方法,例如 <<>> 操作符。
  • 它提供了将一个字符串转换成一个数据类型的方法,方便程序员进行数据处理。
  • C 中,stringstream 也是类型安全的

stringstreamcincout 等输入输出流都有类似的接口和方法,可以进行输入输出操作,但它们的作用域不同。cincout 等输入输出流通常用于标准输入输出流,而 stringstream 通常用于字符串的处理。

通常我们可以使用 stringstream 对字符串进行分割、转换、拼接等操作,然后再使用 cincout 输出到标准输入输出流中:

  • 我们可以使用 getline() 函数从标准输入读取一行字符串;
  • 然后使用 stringstream 将其转换为数值类型,最后再使用 cout 输出到标准输出流中。
  • 这样的代码既可以处理标准输入输出流,又可以方便地进行字符串操作,提高了程序的可扩展性和复用性。

举个栗子

观察如下代码:

代码语言:javascript复制
#include <iostream>
#include <string>
#include <sstream>

using namespace std;

int main() {
    stringstream s;
    string name = "Lys";
    int age = 13;
    double height = 1.86;
    string status = "is a dog";

    s << "Name: " << name << ", Age: " << age << ", Height: " << height << ", Status: " << status;
    string str = s.str();

    cout << str << endl;

    return 0;
}

在这个示例中,我们首先创建了一个 stringstream 对象 s,然后使用<<运算符将字符串、整数和浮点数和一个字符串插入到 s 中,最后使用 str() 方法将所有插入的数据转换为一个字符串,并将其打印到标准输出中。

再比如,观察如下代码:

代码语言:javascript复制
#include <iostream>
#include <string>
#include <sstream>

using namespace std;

int main() {

    string s;
    getline(cin, s);

    stringstream ss(s);
    string str;

    while(ss >> str){
        cout << str << endl;
    }

    return 0;
}

编译运行并且在控制台输入如下内容:

代码语言:javascript复制
Lys is a dog.

然后得到如下输出:

代码语言:javascript复制
Lys
is
a
dog.

在这个示例中,我们首先创建了一个 string 类型的 s,并用 getline(cin, s) 读入字符串,然后将字符串 s 转换为了stringstream 对象 ss,再通过该对象过滤空格后不断赋值给 str,最终将其打印到标准输出中。


2.2 关闭 C 标准流同步


前面提到了,由于 cincout 存在同步流机制和类型检查机制等影响其性能的功能。因此,在面对需要大量输入输出的场景时, scanf()printf() 输入输出的效率显著优于 cincout,但我们仍然可以通过设置 cincout 的同步流标志位来关闭同步流,从而提高程序的效率,甚至优于 scanf()printf()

C 程序中,添加如下语句以优化输入输出流速度和交互性:

代码语言:javascript复制
 ios::sync_with_stdio(false);
 cin.tie(nullptr);
 cout.tie(nullptr);
  • ios::sync_with_stdio(false):关闭 C 的标准输入输出流与 C 语言输入输出流的同步,从而加快输入输出的速度。
  • cin.tie(nullptr):解除 cincout 的绑定,从而避免在读取输入时,每次输出缓存区都被刷新的问题。
  • cout.tie(nullptr)cout 默认绑定的是 nullptr实际上这句话并没有必要添加。相关讨论参见 Ok, lets talk about cout.tie once and forever。

需要注意的是,关闭输入输出流同步后,不能再在 C 代码中使用 C 语言的输入输出函数了,否则可能会导致输出不完整或者输出顺序错误等问题。此外,解除绑定后,需要手动刷新输出缓存区,否则输出的内容可能不完整或者不及时。因此,在使用这些语句时,需要谨慎地考虑使用场景和执行顺序,避免出现不可预料的错误。

下列语句:

代码语言:javascript复制
 ios::sync_with_stdio(false);
 cin.tie(0);
 cout.tie(0);

同样可以达到提高输入输出速度的目的。这种写法比使用 nullptr 更加通用,因为在某些旧的 C 编译器中可能不支持 nullptr

总的来说,这两种写法的区别并不大,只是在解除绑定时所使用的空指针常量不同,但都可以实现提高输入输出速度的效果。


3. 最后的练习


3.1 泛凯撒加密


Original Link

描述

众所周知,在网络安全中分为明文和密文,凯撒加密是将一篇明文中所有的英文字母都向后移动三位(Z 的下一位是 A),比如 a 向后移动三位就变成了 dA 向后移动三位就变成了 DZ 向后移动三位就变成了 C,但是泛凯撒加密可没有这么简单,它是将明文中的每个字母向后移动k位得到密文,并且在密文的结尾会附加一个 ?,本题想让你通过得到密文反解出原本的明文。

输入格式

第一行,输入一个正整数 k 表示字母向后移动的位数。

接下来输入若干行字符串,表示密文,数据输入保证仅密文的最后一个字符是 ?

输出格式

输出原本的明文。

数据范围

0 le k le 100

样例输入

代码语言:javascript复制
2
*eee/peee  ?

样例输出

代码语言:javascript复制
*ccc/nccc  

3.2 题解


你已经是一个成熟的 text{ ACMer } 了,要学会自己分析并解决问题。实在解决不了就解决自己吧。

代码语言:javascript复制
#include <iostream>
#include <cstring>

using namespace std;

void solve(){
    int k; cin >> k;
    string s;
    k %= 26;
    getchar();  //清空缓冲区中的 'n'
    while(getline(cin, s)){
        for(int i = 0; i < s.size(); i   ){
            char st = s[i];
            if(st >= 'a' && st <= 'z') cout << char(st - k < 'a' ? st - k   26 : st - k);
            else if(st >= 'A' && st <= 'Z') cout << char(st - k < 'A' ? st - k   26 : st - k);
            else if(st == '?') break;
            else cout << st;
        }
        cout << endl;
    }
}

int main(){
    solve();
    return 0;
}

0 人点赞