输入/输出库概述
在这节课中,我们将更多地讨论我们在 Hello world! 程序中使用的 std::cout,它用于将文本 Hello world! 输出到控制台。我们还将探索如何从用户那里获取输入,这将使我们的程序更具交互性。
输入/输出库基础
输入/输出库(io库)是处理基本输入和输出的C++标准库的一部分。我们将使用这个库中的函数从键盘获取输入并将数据输出到控制台。iostream中的io代表输入/输出。
包含iostream头文件
要使用iostream库中定义的功能,我们需要在任何使用iostream内容的代码文件顶部包含iostream头文件,如下所示:
#include <iostream>
// 使用iostream功能的其余代码在这里
std::cout 输出流
std::cout基本用法
iostream库包含了一些预定义的变量供我们使用。其中一个最有用的变量是std::cout,它允许我们将数据发送到控制台以文本形式打印。cout代表"字符输出"。
Hello world示例
作为提醒,这是我们的 Hello world程序:
#include <iostream> // 用于std::cout
int main()
{
std::cout << "Hello world!"; // 将Hello world!打印到控制台
return 0;
}
打印数字
std::cout不仅可以打印文本,还可以打印数字:
#include <iostream> // 用于std::cout
int main()
{
std::cout << 4; // 将4打印到控制台
return 0;
}
这将产生结果:
4
打印变量值
它也可以用来打印变量的值:
#include <iostream> // 用于std::cout
int main()
{
int x{ 5 }; // 定义整数变量x,初始化值为5
std::cout << x; // 将x的值(5)打印到控制台
return 0;
}
这将产生结果:
5
连接多个输出
要在同一个行上打印多个内容,插入操作符(«)可以在单个语句中多次使用,以连接(链接在一起)多个输出片段。例如:
#include <iostream> // 用于std::cout
int main()
{
std::cout << "Hello" << " world!";
return 0;
}
这个程序两次使用«操作符,首先输出Hello,然后输出world。 因此,这个程序打印:
Hello world!
操作符形象化
提示 可能有助于想象«操作符(和»操作符)像传送带一样,按照指示的方向移动数据。在这种情况下,当内容传送到std::cout时,它会被输出。
打印文本和变量
这是另一个在同一语句中打印文本和变量值的例子:
#include <iostream> // 用于std::cout
int main()
{
int x{ 5 };
std::cout << "x is equal to: " << x;
return 0;
}
这个程序打印:
x is equal to: 5
命名空间相关内容
相关内容 我们在第2.9课中讨论了std::前缀的实际作用——命名冲突和命名空间简介。
使用std::endl输出新行
连续输出问题
您期望这个程序打印什么?
#include <iostream> // 用于std::cout
int main()
{
std::cout << "Hi!";
std::cout << "My name is Alex.";
return 0;
}
您可能会对结果感到惊讶:
Hi!My name is Alex.
单独的输出语句并不会导致控制台上的输出分成不同的行。
新行的概念
如果我们想要在控制台上打印不同的行,我们需要告诉控制台将光标移动到下一行。我们可以通过输出一个新行来做到这一点。新行是一个操作系统特定的字符或字符序列,它将光标移动到下一行的开始。
使用std::endl
输出新行的一种方法是输出std::endl(代表"end line"):
#include <iostream> // 用于std::cout和std::endl
int main()
{
std::cout << "Hi!" << std::endl; // std::endl将导致光标移动到下一行
std::cout << "My name is Alex." << std::endl;
return 0;
}
这将打印:
Hi!
My name is Alex.
std::endl的额外用途
提示 在上面的程序中,第二个std::endl技术上是不必要的,因为程序随后立即结束。然而,它有几个用途。 首先,它有助于表明输出行是一个"完整的想法"(与稍后在代码中完成的部分输出相对)。在这种意义上,它的功能类似于标准英语中使用句号。 其次,它将光标定位在下一行,因此如果我们稍后添加额外的输出行(例如让程序说"再见!"),这些行将出现在我们期望的位置(而不是附加到之前的输出行)。 第三,在从命令行运行可执行文件后,一些操作系统在显示命令提示符之前不会输出新行。如果我们的程序没有在光标位于新行时结束,命令提示符可能会出现在之前的输出行上,而不是像用户期望的那样出现在新行的开始。
输出新行的最佳实践
最佳实践 每当输出行完成时,就输出一个新行。
std::cout缓冲机制
缓冲的概念
考虑您最喜欢的游乐园中的过山车。乘客以某种变化的速率出现并排队。定期,一列火车到达并载客(最多可容纳火车的最大容量)。当火车满员,或者过了足够的时间,火车就会带着一批乘客离开,游乐设施开始。任何无法登上当前火车的乘客都等待下一列。
缓冲区工作原理
这个类比类似于C++中发送到std::cout的输出通常是如何处理的。我们程序中的语句请求将输出发送到控制台。然而,该输出通常不会立即发送到控制台。相反,请求的输出"排队",并存储在专门收集此类请求的内存区域(称为缓冲区)中。定期,缓冲区被刷新,这意味着缓冲区中收集的所有数据都被传输到目的地(在这种情况下,是控制台)。
缓冲区刷新
作者注 使用另一个类比,刷新缓冲区有点像冲厕所。所有收集的"输出"都被转移到……它接下来要去的地方。Eew。
这也意味着,如果您的程序崩溃、中止或暂停(例如,出于调试目的),在缓冲区刷新之前,任何仍在缓冲区中等待的输出都不会显示。
缓冲与无缓冲
缓冲输出的相反是无缓冲输出。使用无缓冲输出时,每个单独的输出请求直接发送到输出设备。 将数据写入缓冲区通常很快,而将一批数据传输到输出设备相对较慢。缓冲可以通过批量处理多个输出请求来显著提高性能,从而最小化输出必须发送到输出设备的次数。
std::endl与\n比较
std::endl的效率问题
使用std::endl通常效率低下,因为它实际上做了两个工作:它输出一个新行(将光标移动到控制台的下一行),并刷新缓冲区(这很慢)。如果我们输出多行文本以std::endl结尾,我们将得到多次刷新,这既慢又可能不必要。
自动刷新机制
当向控制台输出文本时,我们通常不需要自己显式刷新缓冲区。C++的输出系统设计为定期自刷新,让它自己刷新既简单又高效。
使用\n字符
要输出一个新行而不刷新输出缓冲区,我们使用\n(在单引号或双引号内),这是一个特殊符号,编译器将其解释为新行字符。\n将光标移动到控制台的下一行,而不会引起刷新,因此它通常会表现得更好。\n也更简洁,可以嵌入到现有的双引号文本中。
\n的使用示例
这是一个使用\n的不同方式的例子:
#include <iostream> // 用于std::cout
int main()
{
int x{ 5 };
std::cout << "x is equal to: " << x << '\n'; // 单引号(单独)(传统)
std::cout << "And that's all, folks!\n"; // 嵌入到双引号字符串中(方便)
return 0;
}
std::cin
std::cin是iostream库中的另一个预定义变量。与std::cout使用插入操作符«提供数据打印到控制台不同,std::cin(代表"字符输入")从键盘读取输入。我们通常使用提取操作符»将输入数据放入变量(然后可以在后续语句中使用)。
#include <iostream> // 用于std::cout和std::cin
int main()
{
std::cout << "Enter a number: "; // 要求用户输入一个数字
int x{}; // 定义变量x以保存用户输入(并值初始化它)
std::cin >> x; // 从键盘获取数字并存储在变量x中
std::cout << "You entered " << x << '\n';
return 0;
}
尝试编译并运行这个程序。当您运行程序时,第5行将打印"Enter a number: “。当代码到达第8行时,您的程序将等待您输入。一旦您输入一个数字(并按回车),您输入的数字将被分配给变量x。最后,在第10行,程序将打印"You entered “,然后是您刚刚输入的数字。 例如(输入值为4):
Enter a number: 4
You entered 4
这是从用户那里获取键盘输入的简单方法,我们将在许多示例中使用它。
提示 请注意,在接收一行输入时,您不需要输出’\n’,因为用户需要按回车键才能接受他们的输入,这将把光标移动到控制台的下一行。
如果您的屏幕在输入数字后立即关闭,请参见第0.8课——一些常见的C++问题以获得解决方案。
如果您使用的是CLion,并且"You entered"之前有一个空格,这是CLion中的一个错误。请参阅CLion错误跟踪器以获取解决方法。
就像可以在一行中输出多个文本一样,也可以在一行中输入多个值:
#include <iostream> // 用于std::cout和std::cin
int main()
{
std::cout << "Enter two numbers separated by a space: ";
int x{}; // 定义变量x以保存用户输入(并值初始化它)
int y{}; // 定义变量y以保存用户输入(并值初始化它)
std::cin >> x >> y; // 获取两个数字并分别存储在变量x和y中
std::cout << "You entered " << x << " and " << y << '\n';
return 0;
}
这将产生输出:
Enter two numbers separated by a space: 5 6
You entered 5 and 6
输入的值应该用空白字符(空格、制表符或新行)分隔。
关于是否有必要在通过另一个来源(例如std::cin)提供用户值之前立即初始化变量,存在一些争议,因为用户提供的值将覆盖初始化值。根据我们之前的建议,变量应该始终初始化,最佳实践是首先初始化变量。
C++ I/O库不提供一种方法来接受键盘输入,而不需要用户按回车。如果您需要这样做,您必须使用第三方库。对于控制台应用程序,我们推荐pdcurses、FXTUI、cpp-terminal或notcurses。许多图形用户界面库都有自己的函数来执行这种事情。
std::cin是缓冲的
在前面的部分中,我们指出输出数据实际上是一个两阶段过程:
每个输出请求的数据被添加(到输出缓冲区的末尾)。
之后,输出缓冲区中的数据(缓冲区的前端)被刷新到输出设备(控制台)。
向缓冲区的末尾添加数据,并从缓冲区的前端移除数据,确保数据以添加的顺序被处理。这有时被称为FIFO(先进先出)。
同样,输入数据也是一个两阶段过程:
您输入的每个字符都被添加到输入缓冲区(在std::cin内部)的末尾。回车键(按下以提交数据)也被存储为’\n’字符。
提取操作符’»‘从输入缓冲区的前端移除字符,并将它们转换为值,该值通过复制赋值分配给相关变量。然后,该变量可以在后续语句中使用。
输入缓冲区中的每行输入数据都以’\n’字符结束。
我们将使用以下程序来演示这一点:
#include <iostream> // 用于std::cout和std::cin
int main()
{
std::cout << "Enter two numbers: ";
int x{};
std::cin >> x;
int y{};
std::cin >> y;
std::cout << "You entered " << x << " and " << y << '\n';
return 0;
}
这个程序将输入到两个变量(这次作为单独的语句)。我们将运行这个程序两次。 运行#1:当遇到std::cin » x;时,程序将等待输入。输入值4。输入4\n进入输入缓冲区,值4被提取到变量x。 当遇到std::cin » y;时,程序将再次等待输入。输入值5。输入5\n进入输入缓冲区,值5被提取到变量y。最后,程序将打印You entered 4 and 5。 这次运行没有什么令人惊讶的。
运行#2:当遇到std::cin » x时,程序将等待输入。输入4 5。输入4 5\n进入输入缓冲区,但只有4被提取到变量x(提取在空格处停止)。
当遇到std::cin » y时,程序不会等待输入。相反,输入缓冲区中仍然存在的5被提取到变量y。然后程序打印You entered 4 and 5。
请注意,在运行2中,当提取到变量y时,程序没有等待用户输入额外的输入,因为输入缓冲区中已经有可以用于提取的先前输入。
std::cin之所以是缓冲的,是因为它允许我们将输入的输入与输入的提取分开。我们可以一次性输入,然后对它执行多个提取请求。
基本提取过程 以下是操作符»对输入的工作方式的简化视图。
如果std::cin不在良好状态(例如,之前的提取失败,std::cin尚未清除),则不尝试提取,提取过程立即中止。
输入缓冲区前端的前导空白字符(空格、制表符和输入行前端的换行符)被丢弃。这将丢弃之前输入行中剩余的未提取的换行符。
如果输入缓冲区现在为空,操作符»将等待用户输入更多数据。输入的数据中的任何前导空白都被丢弃。
然后操作符»提取尽可能多的连续字符,直到遇到换行符(表示输入行的结束)或对被提取到的变量无效的字符。
提取过程的结果是:
如果提取在步骤1中中止,则没有发生提取尝试。没有发生其他事情。
如果上述步骤4中提取了任何字符,则提取成功。提取的字符被转换为值,然后通过复制赋值给变量。
如果在上述步骤4中无法提取任何字符,则提取失败。被提取到的对象被复制赋值为0(从C++11开始),任何未来的提取将立即失败(直到std::cin被清除)。
任何未提取的字符(包括新行)都保留供下一次提取尝试使用。
相关内容 我们将在第9.5课——std::cin和处理无效输入中讨论如何检测和处理提取失败、处理多余的输入以及清除std::cin。
例如,给定以下片段:
int x{};
std::cin >> x;
在三种不同的输入情况下会发生什么:
如果用户输入5a并按回车,5a\n将被添加到缓冲区。5将被提取,转换为整数,并分配给变量x。a\n将保留在输入缓冲区供下一次提取。
如果用户输入’b’并按回车,b\n将被添加到缓冲区。因为b不是有效的整数,无法提取任何字符,所以这是提取失败。变量x将被设置为0,直到输入流被清除,未来的提取将失败。
如果由于之前的提取失败导致std::cin不在良好状态,这里什么也不会发生。变量x的值没有改变。
我们将在下面的测验中探讨更多案例。 操作符«与操作符» 新程序员经常混淆std::cin、std::cout、插入操作符(«)和提取操作符(»)。这里有一个简单的记忆方法:
std::cin和std::cout总是在操作符的左侧。
std::cout用于输出值(cout = 字符输出)。
std::cin用于获取输入值(cin = 字符输入)。
<< 与std::cout一起使用,并显示数据移动的方向。std::cout << 4将值4移动到控制台。
>> 与std::cin一起使用,并显示数据移动的方向。std::cin >> x将用户从键盘输入的值移动到变量x。
顺便说一句… 如果您想知道为什么C++使用std::cout和std::cin而不是其他东西,请参阅https://en.wikipedia.org/wiki/Standard_streams。