C++输入输出流

流的四种状态

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

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

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

  • eofbit 表示到达流结尾位置, 流在正常输入输出的情况下结束,会被置为eofbit状态。

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

标准输入输出流

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

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

标准输入流

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

流提取符 >> 从流中提取数据时通常跳过输入流中的空格、 tab 键、换行符等空白字符。只有在输入完数据再按回车键后,该行数据才被送入键盘缓冲区,形成输入流,提取运算符 >> 才能从中提取数据。需要注意保证从流中读取数据能正常进行。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的状态
}

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

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

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

缓冲机制

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

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

为什么要引入缓冲区?

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

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

缓冲区要做哪些工作?

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

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

缓冲机制

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

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

标准输出流

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

  1. 程序正常结束

马上输出了 1025 个 a

cout1.png
  1. 缓冲区满

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

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

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

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

cout3.png

 

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

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

文件输入输出流

文件输入流

文件输入流对象的创建

首先我们要明确使用文件输入流的信息传输方向:文件 --》 文件输入流对象的缓冲区 --》 程序中的数据结构

根据上述的说明,我们可以将输入流对象的创建分为两类:

  1. 可以使用无参构造创建ifstream对象,再使用open函数将这个文件输入流对象与文件绑定(若文件不存在,则文件输入流进入failbit状态
1
2
3
4
#include <fstream>

ifstream ifs;
ifs.open("path.txt");
  1. 也可以使用有参构造创建ifstream对象,在创建时就将流对象与文件绑定,后续操作这个流对象就可以对文件进行相应操作
1
ifstream ifs2("path.txt");

 

用这个输入流去读取文件中的内容,默认以换行符或空格作为间隔符,一次读取一个字符串。

1
2
3
4
5
string word;
while(ifs >> word){
cout<< word << endl;
}
ifs.close();

 

根据不同的情况,对文件的读写操作,可以采用不同的文件打开模式。文件模式一共有六种,它们分别是:

in : 输入,文件将允许做读操作;如果文件不存在,打开失败

out : 输出,文件将允许做写操作;如果文件不存在,则直接创建一个

app : 追加,写入将始终发生在文件的末尾

ate : 末尾,写入最初在文件的末尾

trunc : 截断,如果打开的文件存在,其内容将被丢弃,其大小被截断为零

binary : 二进制,读取或写入文件的数据为二进制形式

按行读取

使用 ifstream 类中的成员函数 getline,这种方式是兼容 C 的写法,但我们通常不推荐,而是采用 <string> 提供的 getline 方法,工作中更常用

1
2
3
istream& getline (istream& is, string& str, char delim);

istream& getline (istream& is, string& str);

默认是换行符分割,你可以通过第三个参数指定分隔符。下面看看常见使用方法:

1
2
3
4
5
string line;
while(getline(ifs,line)){
cout << line << endl;
}
ifs.close();

读取指定字节数

read函数 + seekg函数 + tellg函数 组合起来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
read 函数:用于从文件中读取指定数量的字节,常用于二进制文件读取

istream& read(char* s, streamsize n);
参数:
s:目标缓冲区指针,用于存储读取的数据。
n:要读取的字节数。

------------------------------------------------------------------------------------

seekg 函数:用于在文件中移动读取位置(文件指针),可以从文件头、当前位置、或文件末尾进行定位

istream& seekg(streampos pos);
istream& seekg(streamoff off, ios_base::seekdir dir);
参数:
pos:绝对位置(以字节为单位)
off:相对偏移量(可以是正数或负数)
dir:偏移的参考点,可取值为 ios::beg(文件头)、ios::cur(当前位置)、ios::end(文件尾)

------------------------------------------------------------------------------------

tellg 函数:用于获取当前的文件读取位置

streampos tellg();

我们有这样一种应用场景,就是希望知道文件的字节大小,好让我们一下子申请对应空间大小的数组,而不需要返回申请空间,而是一下子就申请完成。我们的做法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
string filename = "test.cc";
ifstream ifs(filename);

// 读取一个文件的所有内容先要获取文件的大小,将游标放到了文件的最后(尾后)
ifs.seekg(0, std: :ios: :end);
long length = ifs.tellg(); // 获取尾后下标,实际就是总的字符数

char * pdata = new char[length + 1]();

// 需要将游标再放置到文件开头
ifs.seekg(0, std: :ios: :beg);
ifs.read(pdata, length);

// content包含了文件的所有内容,包括空格、换行
string content(pdata);
cout << "content:" << content << endl;

delete[] pdata;
ifs.close();

文件输出流

文件输出流对象的创建

文件输出流的作用是将流对象保存的内容传输给文件,ofstream 对象的创建与 ifstream 对象的创建类似。

1
2
3
4
5
6
#include < fstream >

ofstream ofs; // 无参构造
ofs.open("testA.cc");

ofstream ofs2("testB.cc"); // 有参构造

通过输出流运算符写内容

1
2
3
4
5
6
7
string filename = "test3.cc";
ofstream ofs3(filename,std::ios::app);

string line("hello,world!\n");
ofs << line; // 写入文件

ofs.close();

特别注意:为了实现在文件流结尾追加写入内容的效果,可以在创建流对象时指定打开模式为 std::ios::app。因为默认情况不是追加模式,创建方式会使打开模式默认为 std::ios::out每次都会清空文件的内容

通过write函数写内容

除了使用输出流运算符<< 将内容传输给文件输出流对象(传给 ofstream 对象就是将内容传到其绑定的文件中),还可以使用write函数进行传输。

1
2
char buff[100] = "hello,world!";
ofs.write(buff,strlen(buff));

工具:动态查看指令

为了更方便地查看多次写入的效果(动态查看文件的内容)可以使用指令

1
2
3
tail 文件名 -F   //动态查看文件内容

ctrl + c //退出查看

字符串输入输出流

通常就是用来进行类型转换,其他类型转换为字符串(输出流),字符串转换为其他类型(输入流)。

C++ 对字符串进行操作的流类型有三个:

istringstream (字符串输入流)

ostringstream (字符串输出流)

stringstream (字符串输入输出流)

它们的构造函数形式都很类似:

1
2
3
4
5
6
7
8
9
10
11
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);

字符串输入流

将字符串的内容传输给字符串输入流对象,再通过这个对象进行字符串的处理(解析)

创建字符串输入流对象时传入 C++ 字符串,字符串的内容就被保存在了输出流对象的缓冲区中。之后可以通过输入流运算符将字符串内容输出给不同的变量,起到了字符串分隔的作用。

1
2
3
4
5
6
7
8
void test(){
string s("123 456");
int num = 0;
int num2 = 0;
//将字符串内容传递给了字符串输入流对象
istringstream iss(s);
iss >> num >> num2;
}

因为输入流运算符会默认以空格符作为分隔符,字符串123 456中含有一个空格符,那么传输时会将空格前的 123 传给 num,空格后的 456 传给 num2,因为 num 和 num2 是 int 型数据,所以编译器会以 int 型数据来理解缓冲区释出的内容,将 num 和 num2 赋值为 123 和 456。

字符串输出流

通常的用途就是将各种类型的数据转换成字符串类型。

1
2
3
4
5
6
7
void test0(){
int num = 123, num2 = 456;
ostringstream oss;
//把所有的内容都传给了字符串输出流对象
oss << "num = " << num << " , num2 = " << num2 << endl;
cout << oss.str() << endl;
}

将字符串、int 型数据、字符串、int 型数据统统传给了字符串输出流对象,存在其缓冲区中,利用它的 str 函数,全部转为 string 类型并完成拼接。