ccpp-inputoutput
Markdown 修正版
0. 叠甲,过
本人水平有限,语言组织能力低下,不保证绝佳的阅读体验,也不保证内容完全准确,如有错误和建议,欢迎指出。才怪。
1. 谈谈输入输出缓冲区
1.1 基本概念
你先别急,我知道你很急,但是别急,所以你先别急。
在了解输入输出缓冲区时,需要明确以下几个基本概念:
- 输入输出流
- 标准输入输出流
- 文件输入输出流
输入输出流
- 输入输出流是一种数据传输的概念。
- 构成计算机的其中之一部件为
I/O设备,指的是用于从程序内部向外部设备(屏幕、打印机等)或从外部设备向程序内部传输数据的设备(鼠标、键盘等); - 计算机中通过
I/O设备进行与用户之间的数据交互,而为了适应不同的设备之间数据的传输,提出了输入输出流的概念。 - 即,输入输出流就是一种统一的数据输入输出协议,为不同的设备之间传递数据时提供一致的接口。
- 构成计算机的其中之一部件为
标准输入输出流
- 标准输入输出流是指程序与外部设备(例如键盘和显示器)之间的输入输出。
- 在
C语言中:- 在
C标准库中,标准输入流、输出流分别是stdin和stdout,另外还有标准错误流stderr。 - 使用
<stdio.h>头文件里的scanf()函数和printf()函数。
- 在
- 在
C++语言中:- 在
C++标准库中,没有stdin这样的标准输入流,而是使用std::cin和std::cout来进行标准输入和标准输出。 - 使用
<iostream>头文件里的getline()函数或是>>和<<操作符。
- 在
- 综上,在
C++中,输入输出流的使用通常是通过iostream库实现的,而在C中则是通过stdio库实现的。
- 在
文件输入输出流
- 文件输入输出流则是将数据保存在磁盘上的文件中,通过打开和关闭文件,程序可以使用文件输入输出流进行数据的读取和写入。
- 在
C语言中:- 文件输入输出流使用
C标准库中的文件指针FILE*来实现。 - 操作函数有
fopen()、fclose()、fread()、fwrite()等。
- 文件输入输出流使用
- 在
C++语言中:- 文件输入输出流是基于
C标准库中的文件操作函数封装而成,即fstream类。 - 具体地,通过
std::ifstream和std::ofstream类实现,它们是std::istream和std::ostream类的派生类。
- 文件输入输出流是基于
- 在
相比标准输入输出流,文件输入输出流需要显式地指定要读写的文件,因此使用起来比较繁琐,但也更加灵活:文件输入输出流可以处理任何类型的文件,包括文本文件和二进制文件,而标准输入输出流只能处理字符流。此外,文件输入输出流可以通过随机访问文件的方式读写文件,而标准输入输出流只能顺序读写。
1.2 输入输出缓冲区
什么是输入输出缓冲区?
顾名思义,输入输出缓冲区就是输入输出缓冲的区域。
在 C/C++ 中,输入输出缓冲区是用来存储输入输出数据的临时存储区域:
- 输入缓冲区是在数据流输入之前存储输入数据的临时存储区域。
- 输出缓冲区是在数据流输出之前存储输出数据的临时存储区域。
说人话:输入输出缓冲区就是为了保存这些输入输出流而临时开辟出的一块内存。
为什么要设置输入输出缓冲区?
众所周知,因为需要,所以设置:
- 缓冲区是在内存中,而外设则是在硬件中。
- 相比于从硬件中读取和写入数据,从内存中读取和写入数据更加快速。
因此,当程序需要读取或写入大量数据时,使用缓冲区可以将这些数据先存储到内存中,然后再一次性地写入或读取,避免了频繁访问硬件的开销。此外,缓冲区还可以优化数据的排列和格式,以便更高效地读取和写入数据。
说人话:缓冲区的存在是为了提高输入输出效率,减少对外设的访问次数。
C/C++ 的输入输出缓冲区有何不同?
别急别急别急
首先别急,其次别急,所以我们先来了解下:输入输出缓冲区的空间由什么来分配?开辟在哪里?何时开辟? 这个问题:
- 输入输出缓冲区的空间通常由操作系统来分配的;
- 一般情况下,是在程序运行时从内存中分配的,在程序运行空间中分配的,不是在操作系统的内核空间中分配;
- 而分配的时机和分配的空间大小会根据具体的实现而不同,一般地,当程序通过输入输出函数向缓冲区写入或者读取数据时,缓冲区就会被分配。
具体地:
分配缓冲区的时机:
- 对于标准输入输出流:缓冲区的空间通常是在程序启动时预先分配好的。
- 对于文件输入输出流:缓冲区的空间是在文件流和流缓冲区对象创建时动态分配的,这些对象通常是在程序开始时被初始化的。
分配缓冲区的大小:
- 缓冲区的大小应该足够容纳输入或输出数据的常规大小,同时又不能过大以致于浪费内存。
- 由实现库来完成对缓冲区大小的分配,具体实现细节可能会因编译器或操作系统的不同而有所差异。
- 一般来说,实现库会通过调用操作系统提供的系统调用或动态内存分配函数来分配缓冲区的空间。
- 在内存空间紧张的情况下,缓冲区的大小可能会被限制,从而可能影响到程序的性能和可靠性。
急急急急急急
我知道你急了,但是你先别急,这部分其实不用太纠结,对吧:
在
C语言中,标准输入输出库<stdio.h>提供了输入输出缓冲区的实现。主要使用了三个函数:
setbuf(),setvbuf(),fflush()。其中,
setbuf()和setvbuf()可以用来设置缓冲区,而fflush()用来清空缓冲区并把缓冲区中的数据输出到文件。因此,
C中的输入输出函数,如scanf()和printf()等,是非类型安全的:它们依赖于格式化字符串来指示输入/输出数据的类型。
如果格式化字符串不正确,就会导致不可预测的结果,如缓冲区溢出和未定义的行为。
在
C++中,<iostream>库提供了输入输出缓冲区的实现。提供了两种不同的缓冲区:
streambuf和filebuf。streambuf是<iostream>库的基类,提供了对输入输出缓冲区的访问;而filebuf是<fstream>库的基类,提供了对文件输入输出缓冲区的访问。但是,
<iostream>库还提供了一些类似setbuf(),setvbuf(),flush()等函数,用来管理输入输出缓冲区。在关闭同步流之后,<iostream>库使用了一种不同于标准输入输出库的机制来提高效率,例如使用字符串流stringstream和缓冲流buffer stream等。因此,
C++中的输入输出函数,如std::cin和std::cout等,是类型安全的:它们使用类型安全的
C++流语义,其中数据类型是静态确定的,而不是动态确定的。这意味着数据类型在编译时就已经确定,而不是在运行时根据格式化字符串动态确定。
这种静态类型检查可以在编译时检测到类型不匹配的错误,从而使
C++的输入输出更加类型安全。
这就是为什么,你仍然可以在 C++ 中使用 scanf() 和 printf(),但是仍建议在 C++ 中使用 <iostream> 库所提供的标准输入输出的原因,以及为什么我们常说 C++ 比 C 更适于面向对象。
总结:这部分真的不用太纠结。中肯的。正确的。理智的。一针见血的。真的。
2. 谈谈输入输出的方式
2.1 C/C++ 的输入和输出
你急了,你急了,你急了,因为你很迷惑,你不明白 stdin、scanf、cin、std::cin、getline、stringstream 还有 stdout、printf、cout、std::cout 这些都是什么什么玩意,对吧?再来明确一下:
stdin是C语言中的标准输入流。cin是C++中的标准输入流,而std::cin是C++标准库命名空间中的标准输入流,cin是使用命名空间std的缩写,即cin是std::cin的别名。scanf()是C语言中的输入函数,而cin和std::cin是C++中的输入流。scanf()的参数需要使用格式化字符串来指定输入数据的类型,而cin和std::cin可以自动识别输入数据的类型。getline()是C++中的输入函数,可以用于从输入流中读取一行文本数据,可以指定分隔符。getline()可以替代scanf()和cin用于读取字符串类型数据。stdout是C语言中的标准输出流。cout是C++中的标准输出流,而std::cout是C++标准库命名空间中的标准输出流。它们之间的区别同cin和std::cin。printf()是C语言中的输出函数,而cout和std::cout是C++中的输出流。printf()的参数需要使用格式化字符串来指定输出数据的类型,而cout和std::cout可以自动识别输出数据的类型。- 至于
stringstream这个东西,我们放到最后细说。
scanf() 和 printf()
因为我们对这两个东西再熟悉不过了,所以我们对这两个东西根本不陌生,这俩是 C 语言中的标准输入和标准输出函数。
对于 printf(),只需要注意下面几点:
- 用法:
printf(format, argument_list);- 用于向控制台输出数据,可以输出多种类型的数据,如整数、浮点数、字符、字符串等。
- 在输出字符串时,需要注意字符串中是否包含特殊字符,如换行符、制表符等,需要使用相应的转义字符来表示。
- 可以使用格式化输出来控制输出的格式,如输出精度、对齐方式等。
而对于 scanf(),除了基本注意点:
- 用法:
scanf(format, argument_list);- 用于从控制台输入数据,可以读取多种类型的数据,如整数、浮点数、字符、字符串等。
scanf()输入数据时要求数据格式与format字符串中指定的格式匹配,否则会产生错误。
还需要注意:scanf() 函数的缓冲区不会自动清空,因此需要使用 fflush(stdin) 语句清空缓冲区,以防止输入的数据被下一个输入函数接收,如果仅仅为了处理掉换行符 \n,可以使用 getchar() 读取,将换行符“吃掉”。
举个栗子:
观察下列代码:
#include <stdio.h>
int main(){
int n; //声明 int 类型变量 n
scanf("%d", &n); //读入 int 类型变量 n
printf("%d\n", n); //输出 int 类型变量 n 并且换行
char c = getchar(); //读入一个字符,并存储在 char 类型变量 c 中
printf("%c", c); //输出 char 类型变量 c
printf("14\n"); //输出 14 并且换行
return 0;
}假设运行并在控制台输入如下内容:
114
5理论上,期望得到输出:
114
514但实际上,控制台输出了如下内容:
114
14甚至,控制台根本没有接收后续输入的 5 这个字符。
在这个例子中,scanf(“%d”, &n) 会读取输入流中的数字 114,并将其存储在变量 n 中。但是,由于输入缓冲区中还有一个换行符 \n(来自用户按下回车键),紧随其后的 getchar() 函数会读取这个换行符,并将其存储在变量 c 中,导致了上述非预期结果。这是因为缓冲区中的数据没有被自动清空,所以控制台没有继续等待用户输入字符 5,程序就已经输出了结果。
观察如下改进后的代码:
#include <stdio.h>
int main(){
int n; //声明 int 类型变量 n
scanf("%d", &n); //读入 int 类型变量 n
printf("%d\n", n); //输出 int 类型变量 n 并且换行
getchar(); //用 getchar() 吃掉缓冲区中的 '\n'
char c = getchar(); //读入一个字符,并存储在 char 类型变量 c 中
printf("%c", c); //输出 char 类型变量 c
printf("14\n"); //输出 14 并且换行
return 0;
}重新编译运行并在控制台输入同样内容:
114
5可以发现控制台输出了:
114
514在改进后的例子中,为了避免缓冲区未清空的情况,在读取完整数后手动调用 getchar() 读取并丢弃了缓冲区里的换行符 \n。这使得用户输入的字符 5 能够被后续的 getchar() 正确读入,最终输出了符合预期的内容。
C++ 中的 cin 和 cout
cin 和 cout 是 C++ 标准库中用于输入和输出的流对象,可以方便地实现控制台的输入输出操作。一般而言,使用 cin 和 cout 时可以通过引入 using namespace std; 来简化代码,避免每次都书写完整的限定名。但也可以不引入该命名空间,而使用完整限定名 std::cin 和 std::cout。
由于 cin 和 cout 的输入输出会自动匹配对应数据类型,所以针对这两者的格式化输入输出并非此处讨论的重点。而在此处,我们需要提及其关于同步流(synchronized stream) 的概念:
- 同步流意味着在程序流中输出数据时,程序必须等到数据完全输出到设备上,然后才能继续执行后面的代码。
- 同样,当程序尝试从输入设备读取数据时,程序会等待用户输入完整的数据,然后才能继续执行后面的代码。
虽然同步流可以确保输入输出的正确性,但是在一些场景下会影响程序的效率,特别是在大量数据输入输出的情况下。
这就是为什么,即使 C++ 宁愿舍弃 scanf() 和 printf() 的高性能,也要得到输入输出流同步所带来的安全性和正确性,这也使得 C++ 更适合面向对象开发。
注意:
scanf()和printf()也存在同步流机制,但其缓冲区的实现更为底层,效率更高。- 除此之外,
cin和cout的类型检查机制以及其他各种操作也是影响其性能的因素之一。
- 除此之外,
getchar() 和 getline()
把这两个放一起纯粹是因为它们长得很像,但是两者天差地别:
getchar()函数从标准输入(stdin)中读取一个字符,返回该字符的ASCII码值。- 通常用于读取单个字符或者字符数组,可以实现简单的输入操作。
- 使用时需要注意的是,由于输入的字符是直接通过键盘输入的,因此需要按下回车键才能将输入的字符送入缓冲区,此时
getchar()才能够读取到输入的内容。
getline()函数从输入流中读取一行文本,并将其存储到一个字符串对象中,可以读取包含空格在内的一整行输入。- 使用时需要注意的是,如果使用默认的分隔符
\n,getline()会将换行符读取到缓冲区,如果下一次使用getline()读取输入,就会导致缓冲区中的换行符被读取,而不是期望的输入。此时可以通过调用cin.ignore()来清除缓冲区中的字符,或者指定其他分隔符。
- 使用时需要注意的是,如果使用默认的分隔符
关于 getchar() 缓冲区的问题已经讲过,下面举个 getline() 的例子:
观察下列代码:
#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;
}假设运行并且在控制台输入如下内容:
114
514理论上,我们期望得到输出:
First: 114
Second: 514但实际上,控制台的输出与期望完全一致:
First: 114
Second: 514你会惊讶地发现输出符合预期,然后你可能会想:“诶,这不是没毛病吗?是不是在开玩笑?”
你先别急,让我先急。
getline() 的第三个参数是分隔符,函数会以该字符为界分割输入。默认情况下省略此参数时,getline() 以 \n(换行符)作为分隔符,相当于调用 getline(cin, s, '\n')。
在第一个例子中,输入 114 后按下回车键,\n 被视为分隔符并从输入流中移除,但随后输入的字符 514 仍保留在缓冲区中。接着第二个 getline() 调用读取了缓冲区中的剩余内容,即 "514",因此输出符合预期。
我们尝试重新指定 getline() 的分隔符,修改得到如下代码:
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
getline(cin, s, ','); // 以逗号为分隔符读入字符串 s
cout << "First: " << s << endl; // 输出 s
getline(cin, s, ','); // 再次读入
cout << "Second: " << s << endl; // 再次输出 s
return 0;
}假设在控制台输入如下内容:
114,
514,理论上期望输出:
First: 114
Second: 514但实际上,控制台输出了:
First: 114
Second:
514这次结果不符合预期。原因在于:输入 114, 后按下回车键,逗号 , 被视为分隔符并从输入流中移除,但换行符 \n 仍保留在缓冲区中。随后第二个 getline() 调用读取了缓冲区中的剩余内容,即 "\n514,",遇到逗号停止,因此读取并存储了 "\n514"。输出时,"\n" 产生了换行,导致 514 被输出到下一行。
为避免此问题,需要手动清除缓冲区中的换行符。可以使用 cin.ignore() 或 cin.get(),更推荐的方法如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
getline(cin, s, ','); // 以逗号为分隔符读入字符串 s
cout << "First: " << s << endl; // 输出 s
// 忽略输入缓冲区中残留的换行符
cin.ignore();
// 也可以使用 cin.get();
getline(cin, s, ','); // 再次读入
cout << "Second: " << s << endl; // 再次输出 s
return 0;
}最终得到符合预期的结果:
First: 114
Second: 514总体而言,getchar() 适用于读取单个字符或者字符数组,而 getline() 适用于读取一整行文本,两者使用时需要 注意不同的输入方式和缓冲区处理。
stringstream
stringstream是C++标准库提供的一种数据流对象,用于在内存中对字符串进行输入输出操作。- 它可以像
cin和cout一样进行输入输出,并且具有和输入输出流相似的接口和方法,例如<<和>>操作符。 - 它提供了将一个字符串转换成一个数据类型的方法,方便程序员进行数据处理。
- 在
C++中,stringstream也是 类型安全的。
- 它可以像
stringstream 和 cin、cout 等输入输出流都有类似的接口和方法,可以进行输入输出操作,但它们的作用域不同。cin、cout 等输入输出流通常用于标准输入输出流,而 stringstream 通常用于字符串的处理。
通常我们可以使用 stringstream 对字符串进行分割、转换、拼接等操作,然后再使用 cin 或 cout 输出到标准输入输出流中:
- 我们可以使用
getline()函数从标准输入读取一行字符串;- 然后使用
stringstream将其转换为数值类型,最后再使用cout输出到标准输出流中。 - 这样的代码既可以处理标准输入输出流,又可以方便地进行字符串操作,有助于提高程序的可扩展性和复用性。
- 然后使用
示例代码:
观察如下代码:
#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() 方法将所有插入的数据转换为一个字符串,并将其打印到标准输出中。
再比如,观察如下代码:
#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;
}编译运行并且在控制台输入如下内容:
Lys is a dog.然后得到如下输出:
Lys
is
a
dog.在这个示例中,我们首先创建了一个 string 类型的 s,并用 getline(cin, s) 读入字符串,然后将字符串 s 转换为了 stringstream 对象 ss,再通过该对象过滤空格后不断赋值给 str,最终将其打印到标准输出中。
2.2 关闭 C++ 标准流同步
前面提到了,由于 cin 和 cout 存在同步流机制和类型检查机制等影响其性能的功能。因此,在面对需要大量输入输出的场景时, scanf() 和 printf() 输入输出的效率显著优于 cin 和 cout,但我们仍然可以通过设置 cin 和 cout 的同步流标志位来关闭同步流,从而提高程序的效率,甚至优于 scanf() 和 printf()。
在 C++ 程序中,添加如下语句以优化输入输出流速度和交互性:
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);ios::sync_with_stdio(false):关闭C++的标准输入输出流与C语言输入输出流的同步,从而加快输入输出的速度。cin.tie(nullptr):解除cin和cout的绑定,从而避免在读取输入时,每次输出缓存区都被刷新的问题。cout.tie(nullptr):cout默认绑定的是nullptr,实际上这句话并没有必要添加 。相关讨论参见 Ok, lets talk about cout.tie once and forever。
需要注意的是,关闭输入输出流同步后,不能再在 C++ 代码中使用 C 语言的输入输出函数了,否则可能会导致输出不完整或者输出顺序错误等问题。此外,解除绑定后,需要手动刷新输出缓存区 ,否则输出的内容可能不完整或者不及时。因此,在使用这些语句时,需要谨慎地考虑使用场景和执行顺序,避免出现不可预料的错误。
下列语句:
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);同样可以达到提高输入输出速度的目的。这种写法比使用 nullptr 更加通用,因为在某些旧的 C++ 编译器中可能不支持 nullptr。
总的来说,这两种写法的区别并不大,只是在解除绑定时所使用的空指针常量不同,但都可以实现提高输入输出速度的效果。
3. 最后的练习
3.1 泛凯撒加密
描述 :
众所周知,在网络安全中分为明文和密文,凯撒加密是将一篇明文中所有的英文字母都向后移动三位(\(Z\) 的下一位是 \(A\)),比如 \(a\) 向后移动三位就变成了 \(d\),\(A\) 向后移动三位就变成了 \(D\),\(Z\) 向后移动三位就变成了 \(C\),但是泛凯撒加密可没有这么简单,它是将明文中的每个字母向后移动k位得到密文,并且在密文的结尾会附加一个 ?,本题想让你通过得到密文反解出原本的明文。
输入格式:
第一行,输入一个正整数 \(k\) 表示字母向后移动的位数。
接下来输入若干行字符串,表示密文,数据输入保证仅密文的最后一个字符是 ?。
输出格式:
输出原本的明文。
数据范围:
\(0 \le k \le 100\)。
样例输入:
2
*eee/peee++?样例输出:
*ccc/nccc++3.2 题解
你已经是一个成熟的 \(\text{ACMer}\) 了,要学会自己分析并解决问题。实在解决不了就解决自己吧。
#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;
}