C++输入输出流第一弹:标准输入输出流 详解(带测试代码)

发布于:2025-03-19 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

 C++输入输出流

流的四种状态(重点)

标准输入输出流

标准输入流

逗号表达式

1. 逗号表达式的基本规则

示例

2. 图片中的代码分析

关键点解析

3. 常见误区

误区 1:逗号表达式等同于逻辑与 &&

误区 2:忽略输入失败的情况

缓冲机制

标准输出流


 C++输入输出流

  • 输入输出的含义

以前所用到的输入和输出,都是以终端为对象的,即从键盘输入数据,运行结果输出到显示器屏幕上。从操作系统的角度看,每一个与主机相连的输入输出设备都被看作一个文件。除了以终端为对象进行输入和输出外,还经常用磁盘(光盘)作为输入输出对象,磁盘文件既可以作为输入文件,也可以作为输出文件

在编程语言中的输入输出含义有所不同。程序的输入指的是从输入文件将数据传送给程序(内存),程序的输出指的是从程序(内存)将数据传送给输出文件。

  • C++输入输出流机制

C++ 的 I/O 发生在流中,流是字节序列。如果字节流是从设备(如键盘、磁盘驱动器、网络连接等)流向内存,这叫做输入操作。如果字节流是从内存流向设备(如显示屏、打印机、磁盘驱动器、网络连接等),这叫做输出操作。

就 C++ 程序而言, I/O 操作可以简单地看作是从程序移进或移出字节,程序只需要关心是否正确地输出了字节数据,以及是否正确地输入了要读取字节数据,特定 I/O 设备的细节对程序员是隐藏的。

  • C++常用流类型

C++ 的输入与输出包括以下3方面的内容:

(1) 对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准的输入输出,简称标准 I/O 。

(2) 以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件。以外存文件为对象的输入输出称为文件的输入输出,简称文件 I/O 。

(3) 对内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息)。这种输入和输出称为字符串输入输出,简称 I/O 。

常用的输入输出流如下:

类名 作用 头文件
istream 通用输入流 iostream
ostream 通用输出流 iostream
iostream 通用输入输出流 iostream
ifstream 文件输入流 fstream
oftream 文件输出流 fstream
fstream 文件输入输出流 fstream
istringstream 字符串输入流 sstream
ostringstream 字符串输出流 sstream
stringstream 字符串输入输出流 sstream

image-20240311144000818

流的四种状态(重点)

IO 操作与生俱来的一个问题是可能会发生错误,一些错误是可以恢复的,另一些是不可以的。在C++ 标准库中,用 iostate 来表示流的状态,不同的编译器 iostate 的实现可能不一样,不过都有四种状态:

  • badbit 表示发生系统级的错误,如不可恢复的读写错误。通常情况下一旦 badbit 被置位,流就无法再使用了。

  • failbit 表示发生可恢复的错误,如期望读取一个数值,却读出一个字符等错误。这种问题通常是可以修改的,流还可以继续使用。

  • eofbit表示到达流 , 此时eofbit 和 failbit 都会被置位。

  • goodbit 表示流处于有效状态。流在有效状态下,才能正常使用。如果 badbit 、 failbit 和 eofbit 任何一个被置位,则流无法正常使用。

这四种状态都定义在类 ios_base 中,作为其数据成员存在。在 GNU GCC7.4 的源码中,流状态的实现

如下:

image-20240302122820267

通过流的状态函数实现

bool good() const      //流是goodbit状态,返回true,否则返回false
bool bad() const       //流是badbit状态,返回true,否则返回false
bool fail() const      //流是failbit状态,返回true,否则返回false
bool eof() const       //流是eofbit状态,返回true,否则返回false

标准输入输出流

对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准输入输出,简称标准 I/O

C++标准库定义了三个预定义的标准输入输出流对象,分别是 std::cinstd::coutstd::cerr。它们分别对应于标准输入设备(通常是键盘)、标准输出设备(通常是显示器)和标准错误设备(通常是显示器)。

标准输入、输出的内容包含在头文件iostream中。

有时候会看到通用输入输出流的说法,这是一个更广泛的概念,可以与各种类型的输入输出设备进行交互,包括标准输入输出设备、文件、网络等。

标准输入流

istream 类定义了一个全局输入流对象,即 cin , 代表的是标准输入,它从标准输入设备(键盘)获取数据,程序中的变量通过流提取符 “>>”(输入流符号) 从流中提取数据。

流提取符 “>>” 从流中提取数据时通常跳过输入流中的空格、 tab 键、换行符等空白字符。只有在输入完数据再按回车键后,该行数据才被送入键盘缓冲区,形成输入流,提取运算符 “>>” 才能从中提取数据。需要注意保证从流中读取数据能正常进行。(流的缓冲机制在下一节中学习)(cin << 遇到换行符等会断掉,可以理解为一个 << 流提取符只能提取一次?)

下面来看一个例子,每次从 cin 中获取一个字符:

void printStreamStatus(std::istream & is){ 
    cout << "is's goodbit:" << is.good() << endl;
    cout << "is's badbit:" << is.bad() << endl;
    cout << "is's failbit:" << is.fail() << endl;
    cout << "is's eofbit:" << is.eof() << endl;
}

void test0(){
    printStreamStatus(cin);  //goodbit状态
    int num = 0;    
    cin >> num;   
    cout << "num:" << num << endl;
    printStreamStatus(cin);  //进行一次输入后再检查cin的状态
}

因为这个cin是一个全局变量,不可复制,当不加引用时,传入cin会进行复制,已经将cin的拷贝构造进行删除因为而全局只能有一个输入流对象,所以要加&,不进行值传递,前面的const可加可不加但是这是一个输入流对象可能会对值进行修改所以就不加了。

如果没有进行正确的输入,输入流会进入failbit的状态,无法正常工作,需要恢复流的状态。

查看C++参考文档,需要利用clear和ignore函数配合,实现这个过程

 if(!cin.good()){
        //恢复流的状态
        cin.clear();
        //清空缓冲区,才能继续使用
        cin.ignore(std::numeric_limits<std::streamsize>::max(),'\n');
        cout << endl;
        printStreamStatus(cin);
    }

如果不进行缓冲区的清除就会直接将上面输入的字符输入然后输出。

image-20240311152304114

//下面为测试代码,可以自行测试
#include <iostream>
#include <string>
#include <limits>
using std::istream;
using std::cout;
using std::endl;
using std::cin;
using std::string;

void checkStreamStatus(istream & is)
{
    cout << is.good() << endl;
    cout << is.bad() << endl;
    cout << is.fail() << endl;
    cout << is.eof() << endl;
}

void test0()
{
    int num = 10;
    cout << "执行输入操作前检查流的状态" << endl;
    checkStreamStatus(cin);

    cout << endl;
    cin>> num;
    cout << "执行输入操作后检查流的状态" << endl;
    checkStreamStatus(cin);
    cout << endl;

    if(!cin.good())
    {
        //恢复流的状态
        cin.clear();
        //还需要清除缓冲区,才能使用
        //在limits的头文件里面
        cin.ignore(std::numeric_limits<std::streamsize>::max(),'\n');
        // checkStreamStatus(cin);

    }
    string str;
    cin >> str;
    cout << "str :" << str <<endl;
    cout << "num:" << num << endl;

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

ignore函数,如果只写释出多少个字符,如果多了再次再缓冲区输入字符也会被释出,因为直到释出那么多字符才会开始输入,所以格式就变成释出字符变为流的最大size,然后再加一个参数,表示直到这个字符,释出结束,就可以解决上面的问题了。

这里提到直到并包含delim说明'\n'也会被释出这就是为什么输出没有的原因。

  • 终止字符 '\n' 会被一并跳过:当 ignore() 遇到 '\n' 时,会将其从输入流中提取并丢弃。

  • 流中剩余的字符:'\n' 之后的内容(如果有)会保留在流中。

思考,如何完成一个输入整型数据的实现(如果是非法输入则继续要求输入)

这里为什么使用逗号表达式,cin >> number 这个表达式的返回值就是这个while的判断条件,当给num成功赋值就是good状态,只有good状态时可以视为true,还可以继续正常工作,这就是为什么使用逗号表达式,要不然错误输入就会结束循环

image-20240311153501817

下面为测试代码,可自行测试验证

//完成输入一个整型数据的实现
#include <iostream>
#include <string>
#include <limits>
using std::istream;
using std::cout;
using std::endl;
using std::cin;
using std::string;

void InputInt(int &num)
{
    cout << "请输入一个int型数据"<<endl;
    //逗号表达式整体的值为最后一个逗号之后的表达式的值
    while(cin >> num,!cin.eof()) //切换到eof()状态 Ctrl+D 循环结束
    {
        if(cin.bad())
        {
            cout << "cin has broken !" << endl;
            return;
        }
        else if(cin.fail())
        {
            cin.clear();
            cin.ignore(std::numeric_limits<std::streamsize>::max(),'\n');
            cout << "请输入一个int型数据" << endl;
        }
        else{
            //输入是合法的
            cout << "num : " << num << endl;
            break;
        }
    }
}

int main()
{
    int num;
    InputInt(num);
    return 0;
}

   

if(cin.good())
{
	cout << "hello " << endl;
}
//cin还没用状态为good,可以继续使用
if(cin)
{
	cout << "hello" << nedl;
}

     

逗号表达式

以下是关于 C++ 中 逗号表达式 的详细讲解,结合你提供的图片中的代码场景:


1. 逗号表达式的基本规则
  • 定义:逗号表达式由多个子表达式组成,形式为 表达式1, 表达式2, ..., 表达式N

  • 求值顺序:从左到右依次执行所有子表达式。

  • 最终值:整个逗号表达式的值为 最后一个子表达式 的值。

示例
int a = (10, 20, 30);  // a = 30
int b = (printf("A"), printf("B"), 5);  
// 输出 "AB",b = 5

2. 图片中的代码分析

根据你描述的代码片段:

// 逗号表达式整体的值为最后一个逗号之后的表达式的值
while (cin >> num, !cin.eof()) {
    if (cin) {
        // 处理输入...
    }
}
关键点解析
  1. 逗号表达式的作用

    • 子表达式1:cin >> num(从输入流读取数据到 num)。

    • 子表达式2:!cin.eof()(检查输入流是否未到达文件末尾)。

    • 整个条件的值:由 !cin.eof() 决定,但 cin >> num 会先执行。

  2. 循环逻辑

    • 每次循环前:先执行 cin >> num,再检查是否未到达文件末尾。

    • 循环继续条件:只要未到达文件末尾(!cin.eof()true),循环继续。


3. 常见误区
误区 1:逗号表达式等同于逻辑与 &&
// 错误理解:认为 while(cin >> num && !cin.eof())
// 实际行为:逗号表达式与 && 的短路逻辑不同!
  • 区别

    • &&:若左侧表达式为 false,右侧不执行。

    • 逗号表达式:所有子表达式都会执行,无论前面的结果如何。

误区 2:忽略输入失败的情况
  • 问题:如果 cin >> num 失败(如输入非数字),cin 会进入错误状态,但 !cin.eof() 可能仍为 true,导致死循环。

  • 改进方法:在条件中同时检查输入是否成功:

    while ((cin >> num) && !cin.eof()) {  // 使用 && 而非逗号
        // 处理有效输入...
    }

对代码进行测试,发现当我们输入3.4时,这时num为3,我们这是可能会猜测,是不是都读取到了然后将数据类型转化了呢,然后我们又测试了一个整型加一个字符串,我们发现没有错误,num值为整型值,说明当读到.4和后面的字符值时发现已经不为整型了,就不继续读取了,剩下的就被留在了缓存区。 那么要如何解决需要后面字符串的输入流

image-20240311160550126

缓冲机制

在标准输入输出流的测试中发现,流有着缓冲机制。缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区输出缓冲区

输入或输出的内容会存在流对象对应的缓冲区,在特定情景下会从缓冲区释出。

  • 为什么要引入缓冲区?

    比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。

    又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的 CPU 可以处理别的事情。因此缓冲区就是一块内存区,它用在输入输出设备和 CPU 之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU 能够协调工作,避免低速的输入输出设备占用 CPU,解放出 CPU,使其能够高效率工作。

  • 缓冲区要做哪些工作?

从上面的描述中,不难发现缓冲区向上连接了程序的输入输出请求,向下连接了真实的 I/O 操作。作为中间层,必然需要分别处理好与上下两层之间的接口,以及要处理好上下两层之间的协作。

输入或输出的内容会存在流对象对应的缓冲区,在特定情景下会从缓冲区释出。

  • 缓冲机制

    缓冲机制分为三种类型:全缓冲、行缓冲和不带缓冲

    全缓冲:在这种情况下,当填满缓冲区后才进行实际 I/O 操作。全缓冲的典型代表是对磁盘文件的读写。

    行缓冲:在这种情况下,当在输入和输出中遇到换行符时,执行真正的 I/O 操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的 I/O 操作。典型代表是cin。

    不带缓冲:也就是不进行缓冲,有多少数据就刷新多少。标准错误输出 cerr是典型代表,这使得出错信息可以直接尽快地显示出来。

cout既有全缓冲的机制,又有行缓冲的机制;cin通常体现行缓冲机制;cerr属于不带缓冲机制,通常用于处理错误信息。

标准输出流

ostream 类定义了全局输出流对象 cout,即标准输出,先将要输出的内容传给了cout对象,在缓冲区刷新时将数据输出到终端。

如下几种情况会导致输出缓冲区内容被刷新:

  1. 程序正常结束

马上输出了1025个a

image-20240311161037164

  1. 缓冲区满

马上输出了1024个a,等待2秒后输出了最后一个a

(说明在实验环境中cout对象的默认缓冲区大小是1024个字节,缓冲区满了就刷新出了所有内容,后面还有一个字符,就要等程序正常结束时刷新出来)

  1. 使用操纵符显式地刷新输出缓冲区,如endl ;

加上endl这种操作符,直接输出了5个a,等待2秒程序结束;如果不加endl,等待2秒程序结束时才会输出5个a

image-20240311161725760

—— 查看ostream头文件中endl的定义刷新缓冲区 + 换行

image-20240311162040137

image-20240311162014391

来看一个简单的例子:在使用cout时,如果在输出流语句末尾使用了endl函数,会进行换行,并刷新缓冲区

void test0(){
    for(int i = 0; i < 1025; ++i){
        cout << 'a' << endl; 
    }
}

如果在使用cout时,没有使用endl函数,键盘输入的内容会存在输出流对象的缓冲区中,当缓冲区满或遇到换行符时,将缓冲区刷新,内容传输到终端显示。可使用sleep函数查看缓冲的效果。

#include <unistd.h>
void test0(){
    for(int i = 0; i < 1024; ++i){
        cout << 'a'; 
    }
    sleep(2);
    cout << 'b';
    sleep(2);
}

GCC中标准输出流的默认缓冲区大小就是1024个字节。

上面的输出结果:1024个字节,缓冲区没有满,在加一个字节才会满出来所以现象就是sleep 2秒当再次加入b时缓冲区满了,将1024个a输出,再等待2秒钟,等程序结束输出b。

如果不用sleep函数,即使没有endl或换行符,所有内容依然是直接输出

——因为程序执行完时也会刷新缓冲区。

  • 关于操作符

endl : 用来完成换行,并刷新缓冲区

ends : 在输入后加上一个空字符('\0'),然后再刷新缓冲区

flush : 用来直接刷新缓冲区的 cout.flush();

  • 标准错误流

ostream 类还定义了全局输出流对象 cerr,标准错误流(不带缓冲)

试试看如下的代码运行会有什么效果

#include <unistd.h>
void test1(){
	cerr << 1;
	cout << 3;
	sleep(2);                                                           
}


网站公告

今日签到

点亮在社区的每一天
去签到