C++ I/O流探秘:解锁输入输出之门

在编程世界中,输入输出(I/O)是任何程序都不可或缺的一部分。它构成了程序与外界交互的桥梁,让我们的程序能够接收外界的信息,并向外界展示处理结果。在C++中, I/O 系统以其独特的方式,为程序员提供了丰富且强大的工具,用于数据的读取与输出。今天,就让我们一起探秘C++的 I/O 世界,看看它是如何帮助我们更好地掌控数据的流动的。

一、输入输出的含义

我们之前所用到的输入和输出,都是以终端为对象的,即从键盘输入数据,运行结果输出到显示器屏幕上。 从操作系统的角度看,每一个与主机相连的输入输出设备都被看作一个文件。除了以终端为对象进行输 入和输出外,还经常用磁盘(光盘)作为输入输出对象,磁盘文件既可以作为输入文件,也可以作为输出文件。 在编程语言中的输入输出含义有所不同。程序的输入指的是从输入文件将数据传送给程序(内存),程序的输出指的是从程序(内存)将数据传送给输出文件。

二、C++输入输出机制

2.1 C++ 中"流"的概念

C++的 I/O 发生在流中,流是字节序列。如果字节流是从设备(如键盘、磁盘驱动器、网络连接等)流向内存,这叫做输入操作。如果字节流是从内存流向设备(如显示屏、打印机、磁盘驱动器、网络连接 等)这叫做输出操作。 就C++程序而言, I/O 操作可以简单地看作是从程序移进或移出字节,程序只需要关心是否正确地输出了字节数据,以及是否正确地输入了要读取字节数据,特定I/O设备的细节对程序员是隐藏的。

2.2 C++常用流类型

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

  1. 对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准的输入输出,简称标准I/O
  2. 以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件。以外存文件为对象的输入输出称为文件的输入输出,简称文件I/O
  3. 对内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息)。这种输入和输出称为字符串输入输出,简称串I/O

C++标准库提供了一组丰富的具有输入/输出功能的流类型。常用流类如下:
image.png

2.3 流类型之间的关系

image.png
ios是抽象基类,由它派生出istream类和ostream类,iostream类支持输入输出操作,iostream类是从
istream类和ostream类通过多重继承而派生的类。类ifstream继承了类istream,类ofstream继承了类
ostream,类fstream继承了类iostream。类istringstream继承了类istream,类ostringstream继承了类 ostream,类stringstream继承了类iostream。

2.4 I/O对象不能拷贝或赋值

ofstream out1,out2;
out1 = out2; // error:不能对流对象赋值
ofstream out3(ofstream); // error:不能将流对象作为函数参数
out2 = out3(out2); // error:不能拷贝流对象

由于不能拷贝 I/O 对象,因此我们也不能将形参或返回类型设置为流类型,进行 I/O 操作的函数通常以引用方式传递和返回流。读写一个 I/O 对象会改变其状态,因此传递和返回的引用不能是 const 的。

2.5 流的状态

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

  • badbit表示发生系统级的错误,如不可恢复的读写错误。通常情况下一旦badbit被置位,流就无法再使用了。
  • failbit表示发生可恢复的错误,如期望读取一个数值,却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。
  • eofbit表示流到达了文件结束位置,当到达文件的结束位置时,eofbit和 failbit都会被置位。
  • goodbit被置位表示流未发生错误。如果badbit、 failbit和eofbit任何一个被置位,则检测流状态的条件会失败。

这四种状态都定义在类ios_base中,作为其数据成员存在。在GNU GCC11.4.0的源码中,流状态的实现如 下:
image.png
image.png

2.6 管理流的状态

C++标准库还定义了一组成员函数来查询或者操作这些状态。

bool bad() const; //若流的badbit置位,则返回true,否则返回false

bool fail() const; //若流的failbit或badbit置位,则返回true;

bool eof() const; //若流的eofbit置位,则返回true;

bool good() const; //若流处于有效状态,则返回true;

iostate rdstate() const; //获取流的状态

void setstate(iostate state); //设置流的状态

//clear的无参版本会复位所有错误标志位*(重置流的状态)

void clear(std::ios_base::iostate state = std::ios_base::goodbit);

流对象的rdstate成员返回一个iostate值,对应流的当前状态。setstate操作将给定条件位置位,表示发生了对应错误。clear成员是一个重载的成员:它有一个不接受参数的版本,而另一个版本接受一个iostate类型的参数。
clear不接受参数的版本清除(复位)所有错误标志位。执行 clear() 后,调用good会返回true。我们可以这样使用这些成员:

ios_base::iostate old_state = cin.rdstate(); // 记住cin的当前状态
cin.clear(); // 清除cin的错误状态,使cin有效
process_input(cin); // 使用cin作为输入流
cin.setstate(old_state); // 恢复cin的状态

带参数的clear版本接受一个iostate值,表示流的新状态。为了复位单一的条件状态位,我们首先用rdstate读出当前条件状态,然后用位操作将所需位复位来生成新的状态。例如,下面的代码将failbit和badbit复位,但保持eofbit不变:

// 复位failbit和badbit,保持其他标志位不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);

2.7 流的通用操作

输入输出流同时还提供了一些其他通用的操作:
输入操作:

int_type get(); // 从输入流中读取一个字符,返回其整数值; 到达文件尾时返回EOF

basic_istream & get(char_type & ch); // 从输入流中读取一个字符,存入ch中,返回引用

// 从输入流中读取最多count个字符,并存入s中,直到遇到delim字符或到达文件尾;不包括结束符,结束符默认是换行符
basic_istream & getline(char_type * s, std::streamsize count, char_type delim = '\n');

// 从输入流中读取最多count个字节,并存入s中,读取操作在遇到文件结束标记(EOF)或读取了 count 个字符时停止。返回读取操作后的流对象本身
basic_istream & read(char_type * s, std::streamsize count);

// 最多获取count个字节,返回值为实际获取的字节数
std::streamsize readsome(char_type * s, std::streamsize count);

// 从输入流中字符并丢弃,直到已读取count个字符或遇到delim字符或到达文件尾;
basic_istream & ignore(std::streamsize count = 1, int_type delim = Traits::eof());

// 查看输入流中的下一个字符, 但是并不将该字符从输入流中取走,不会跳过输入流中的空格、回车符; 在输入流已经结束的情况下,返回 EOF。
int_type peek();

// 获取当前流中游标所在的位置
pos_type tellg();

// 将读取位置定位到pos位置(绝对位置)
basic_istream & seekg(pos_type pos);

// 将读取位置定位到off偏移处(相对位置),dir为ios_base::beg或ios_base::cur或ios_base::end
basic_istream & seekg(off_type off, std::ios::seekdir dir);

输出操作:

// 往输出流中写入一个字符
basic_ostream & put(char_type ch);

// 往输出流中写入count个字符,从s中读取
basic_ostream & write(const char_type * s, std::streamsize count);

// 获取当前流中游标所在的位置
pos_type tellp();

// 将写入位置定位到pos位置(绝对位置)
basic_ostream & seekp(pos_type pos);

// 将写入位置定位到off偏移处(相对位置),dir为ios_base::beg或ios_base::cur或ios_base::end
// off 可以取正数,也可以取负数
basic_ostream & seekp(off_type off, std::ios_base::seekdir dir);

// 刷新缓冲区
basic_ostream & flush();

三、缓冲区

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

3.1 为什么要引入缓冲区

比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲 区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大 快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。
又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。
因此缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

3.2 缓冲区要做哪些工作?

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

3.3 缓冲区的类型

缓冲区分为三种类型:全缓冲、行缓冲和不带缓冲。
全缓冲:在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。
行缓冲:在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据。
不带缓冲:也就是不进行缓冲,标准错误cerr/stderr是典型代表,这使得出错信息可以直接尽快地显示出来。

3.4 C++中的流缓冲区

在C++中,流的缓冲区之基类是定义在头文件当中的 streambuf.
在头文件当中,定义着两个类: ios_base和 ios。ios_base是所有I/O类的祖先,提供了状态信息控制 信息内部存储回调等设施。ios继承自ios_base,额外提供了与streambuf的接口;同时允许多个ios 对象绑定同一个streambuf对象。
由于ios_base没有提供与streambuf的接口,ios才是标准库内所有I/O类(模板)事实上的最近共同祖先。ios的成员函数rdbuf是读取和设置流对象(ios的对象)绑定缓冲区的成员函数,它有两个不同的重 载形式,分别如下:

//返回与之关联的streambuf;如果没有,则返回nullptr
streambuf *rdbuf() const;

//重新设置streambuf
//如果有与先前绑定的streambuf解绑,再绑定传入的streambuf;
//如果传入的是nullptr,则流对象不与任何缓冲区对象绑定
streambuf *rdbuf(streambuf * sb);

streambuf 本身不可以直接创建对象,它是一个抽象类型;但它向下派生出了2个派生类型,如我们在类图中看到的 filebuf 和 stringbuf,这两个类分别是作为文件流和字符串流的子对象的,可以直接创建对象,后面我们会再次看到。

四、标准IO

4.1 标准输入流

stream类定义了1个输入流对象,即cin, 代表的是标准输入,它从标准输入设备(键盘)获取数据,程序中的变量通过流提取符>>从流中提取数据。流提取符>>从流中提取数据时通常跳过输入流中的空格、tab 键、换行符等空白字符。只有在输入完数据再按回车键后,该行数据才被送入键盘缓冲区,形成输入流,提取运算符>>才能从中提取数据。需要注意保证从流中读取数据能正常进行。

4.1.1 从cin中获取一个字符

void test() {
    char ch;
    while((ch = cin.get()) != '\n') {
        cout << ch;
    }
    cout << ch;
}

image.png
在界面输入"123abc回车", 当敲下回车(‘\n’)时,数据就会被存入输入缓冲区中,字符’\n’也一起 存入了缓冲区,然后每次get()操作就会从缓冲区中获取一个字符。

4.1.2 从cin获取一个单词

void test1() {
    int value;
    cin >> value;
    cout << "value = " << value << endl;

    string word;
    cin >> word;
    cout << "word = " << word << endl;
}

首先程序输入123abc,看看程序会发生什么?
image.png
在输入123abc时,cin 会尝试读取尽可能多的字符来形成一个有效的整数。在这个例子中,它会读取123这三个字符,因为它们都是有效的数字字符,然后将它们转换成一个整数123,并存储在value变量中。
接下来,abc这部分字符仍然留在输入缓冲区中,等待后续读取。
cout << "value = " << value << endl; 会输出value = 123到屏幕,并添加一个换行符。
接下来,程序继续执行,尝试从输入流中读取一个字符串。
此时,输入缓冲区中还有abc这部分字符等待读取。因此,cin 会读取这三个字符,并将它们作为一个字符串赋值给word变量。
cout << "word = " << word << endl; 输出word = abc到屏幕,并再次添加一个换行符。
所以,当你输入123abc并调用test1()函数时,你会在屏幕上看到以下输出:

value = 123
word = abc

接下来,我们关注一下流的状态:

#include <iostream>
#include <string>
#include <stdexcept>
#include <unistd.h>

using namespace std;

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

void test() {
    int value;
    printStreamStatus(cin);
    while (cin >> value) {
        cout << "value = " << value << endl;
    }
    printStreamStatus(cin);

    cin.clear();
    printStreamStatus(cin);
    cin.ignore(1024, '\n');

    string word;
    cin >> word;
    cout << "word = " << word << endl;
    printStreamStatus(cin);
}

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

image.png

#include <iostream>
#include <string>
#include <stdexcept>
#include <unistd.h>

using namespace std;

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

void test() {
    int value;
    while(cin >> value, !cin.eof()) {
        if(cin.bad()) {
            throw runtime_error("IO stream is corrupted!");
        } else if(cin.fail()) {
            cerr << "bad data, pls input a valid integer number!" << endl;
            cin.clear();
            cin.ignore(1024, '\n');
            continue;
        } else {
            cout << "value = " << value << endl;
        } 
    } 
}

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

image.png

4.1.3 从cin中获取一行数据

void test4() {
    char buffer[1024];
    cout << "input a string: ";
    cin.getline(buffer, 1024);
    cout << "input string: " << buffer << endl;
}

image.png

4.2 标准输出流

ostream类定义了3个全局输出流对象,即cout,cerr,clog,平常用的最多的就是cout,即标准输出。cout 将数据输出到终端,它与标准C 输出stdout关联。cerr是标准错误流(非缓冲),clog也是标准错误流 (带缓冲)。**注意:**在C语言中,标准输入、标准输出和标准错误分别用0,1,2文件描述符代表。

4.2.1输出字符和字符串

void test5() {
    cout.put('a');
    cout.put('\n');

    char str[] = "hello,world";
    cout.write(str, sizeof(str));
    cout << endl;
}

image.png

4.2.2 cout与cerr/clog的区别

我们来看一个例子:

void test()
{
    cout << "hello, cout" << endl;
    cerr << "hello, cerr" << endl;
    clog << "hello, clog" << endl;
    sleep(2);
}

假设我们经过编译后得到的可执行程序为test,然后我们在命令行输入:

$ ./test > a.txt

执行完毕后,我们会发现屏幕上输出的是:

hello, cerr
hello, clog

但hello, cout没有输出到屏幕上,但却出现在了文件a.txt之中。
若执行的是:

$ ./test 2>a.txt

我们会发现屏幕上输出的是:

hello, cout

而另外2句被写入了文件a.txt。这就是区别啦。

4.2.3 cerr与clog的区别

cerr和clog都是标准错误流,区别在于cerr不经过缓冲区,直接向终端输出信息,而clog中的信息是存放在缓冲区的,缓冲区满后或遇到endl向终端输出。

4.3、输出缓冲区

输出缓冲区内容刷新的意思是:输出缓冲区的内容写入到真实的输出设备或者文件。
如下几种情况会导致输出缓冲区内容被刷新:

  1. 程序正常结束(有一个收尾操作就是清空缓冲区),作为main函数的return操作的一部分,缓冲刷新被执行;
  2. 缓冲区满(包含正常情况和异常情况),需要刷新缓冲,然后新的数据才能继续写入缓冲区;
  3. 使用操纵符显式地刷新输出缓冲区,如:endl、flush、ends(没有刷新功能);
  4. 在每个输出操作之后,我们可以用操纵符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的;
  5. 输出流与输入流相关联,此时在读输入流时将刷新其关联的输出流的输出缓冲区。

4.3.1 输出缓冲区满的时候

void test6() {
    for (size_t i = 0; i < 1024; ++i) {
        cout << 'a';
    }
    sleep(3);
}

程序执行后等待三秒才会输出1024个a,这是因为当程序结束时会刷新缓冲区。
image.png
image.png

void test7() {
    for (size_t i = 0; i < 1025; ++i) {
        cout << 'a';
    }
    sleep(3);
}

可以看到程序执行后输出1024个a,然后等待3秒后输出一个a,这是因为cout会在输出缓冲区满的时候,刷新输出缓冲区。
image.png
image.png

void test8() {
    for (size_t i = 0; i < 1025; ++i) {
        cout << 'a';
    }
    cout << endl;
    sleep(3);
}

可以看到程序执行后输出1025个a,然后等待3秒后结束程序。这是因为endl会刷新输出缓冲区。

4.3.2 使用操纵符

endl: 用来完成换行,并刷新缓冲区
ends: 在输入后加上一个空字符,但是没有刷新缓冲区(这个需要注意,很多书上说可以刷新缓冲区)
flush: 用来直接刷新缓冲区的
unitbuf: 在每次执行完写操作后都刷新输出缓冲区
nounitbuf: 让流回到正常的缓冲方式

4.3.3 输出流与输入流相关联

当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标 准库将cout和cin关联在一起,可以测试:

void test() {
    auto stream = cin.tie();
    cout << "stream:" << stream << endl;
    cout << "&cout:" << &cout << endl;
    
    cin.tie(nullptr);//解除关联

}

**交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提示信息,都会在读操作之前 被打印出来。 **
用来关联流的操作是tie:

ostream *tie () const;  //返回指向绑定的输出流的指针。

ostream *tie (ostream *os);  //将os指向的输出流绑定的该对象上,并返回上一个绑定的输出
流指针。

五、文件IO

所谓“文件”,一般指存储在外部介质上数据的集合。一批数据是以文件的形式存放在外部介质上的。操 作系统是以文件为单位对数据进行管理的。要向外部介质上存储数据也必须先建立一个文件(以文件名 标识),才能向它输出数据。根据文件中数据的组织形式,可分为ASCII文件二进制文件。 外存文件包括磁盘文件、光盘文件和U盘文件。目前使用最广泛的是磁盘文件
文件流是以外存文件为输入输出对象的数据流。文件输入流是从外存文件流向内存的数据,文件输出流
是从内存流向外存文件的数据。每一个文件流都有一个内存缓冲区与之对应。文件流本身不是文件,而 只是以文件为输入输出对象的流。若要对磁盘文件输入输出,就必须通过文件流来实现。
C++对文件进行操作的流类型有三个: ifstream(文件输入流), ofstream(文件输出流), fstream(文 件输入输出流),他们的构造函数形式都很类似

ifstream();

explicit ifstream(const char *filename, openmode mode = in);

explicit ifstream(const string &filename, openmode mode = in);

ofstream();

explicit ofstream(const char *filename, openmode mode = out);

explicit ofstream(const string &filename, openmode mode = out);

fstream();

explicit fstream(const char *filename, openmode mode = in|out);

explicit fstream(const string &filename, openmode mode = in|out);

5.1 文件模式

根据不同的情况,对文件的读写操作,可以采用不同的文件打开模式。文件模式在GNU GCC11.4.0源码实现中,是用一个叫做openmode的枚举类型定义的,它位于ios_base类中。文件模式一共有六种,它们 分别是: in: 输入,文件将允许做读操作;如果文件不存在,打开失败

  • out: 输出,文件将允许做写操作;如果文件不存在,则直接创建一个
  • app: 追加,写入将始终发生在文件的末尾
  • ate: 末尾,读操作始终发生在文件的末尾
  • trunc: 截断,如果打开的文件存在,其内容将被丢弃,其大小被截断为零
  • binary: 二进制,读取或写入文件的数据为二进制形式

源文件中的形式:
image.png
image.png

image.png

对于短文件,一次性拿到所有的数据

void test() {
    string filename = "file.txt";
    ifstream ifs(filename);
    if (!ifs) {
        cerr << "Error: cannot open file " << filename << endl;
        return;
    }

    // 获取文件中的内容
    // 先获取文件长度,再读取文件内容到内存中
    ifs.seekg(0, ios::end);
    int length = ifs.tellg();
    cout << "File length: " << length << endl;
    // 注意:必须要将文件指针重新定位到文件开头
    ifs.seekg(0, ios::beg);
    char* buffer = new char[length + 1]();

    ifs.read(buffer, length);
    buffer[length] = '\0';
    cout << buffer << endl;
    delete[] buffer;

    ifs.close();
}

六、字符串IO

字符串流是以内存中的字符串类对象或者字符数组为输入输出对象的数据流,也即是将数据输出到字符 串流对象或者从字符串流对象读入数据,也称之为内存流。
C++对字符串进行操作的流类型有三个: istringstream(字符串输入流), ostringstream(字符串输出 流), stringstream(字符串输入输出流),他们的构造函数形式都很类似:

istringstream(): istringstream(ios_base::in) 
{ 
    
}

explicit istringstream(openmode mode = ios_base::in);

explicit istringstream(const string& str, openmode mode = ios_base::in);

ostringstream(): ostringstream(ios_base::out) 
{ 

}

explicit ostringstream(openmode mode = ios_base::out);

explicit ostringstream(const string& str, openmode mode = ios_base::out);

stringstream(): stringstream(in|out) 
{

}

explicit stringstream(openmode mode = ios_base::in|ios_base::out);

explicit stringstream(const string& str, openmode mode = ios_base::in|ios_base::out);

字符串I/O经常用来做格式转换,下面来看一看例子:

void testStringStream() {
    string s1 = "hello, world. This is C++ world";
    istringstream iss(s1);
    string word;
    while(iss >> word) {
        cout << word << endl;
    }
}

string int2str(int number) {
    ostringstream oss;
    oss << number;
    return oss.str();
}

void testStringCat() {
    stringstream ss;
    int idx;
    string prefix = "~/documents/photos/";
    string postfix = ".jpg";
    string filename;
    for(idx = 0; idx < 100; ++idx) {
        ss << prefix << idx << postfix << '\n';
        ss >> filename;
        ss.clear();
        ss.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
        cout << filename << endl;
    }
}

七、结语

C++的 I/O 系统是一个功能强大且灵活的工具集,它不仅支持基础的控制台输入输出,还能处理复杂的内存、文件 I/O 操作。通过熟练掌握C++的 I/O 技术,我们能够更加高效地处理数据,构建出更加健壮和易用的程序。希望这篇文章能为你打开C++ I/O 世界的大门,引领你走向更加广阔的编程天地。

八、参考资料

  1. C++ primer
  2. C++ 标准库
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值