8 IO库

  • 部分IO库设施:
    • istream:输入流类型,提供输入操作。
    • ostream:输出流类型,提供输出操作。
    • cin:istream 对象,从标准输入读取数据
    • cout:ostream 对象,向标准输出写入数据
    • cerr:ostream 对象,向标准错误写入数据。
    • >> 运算符:从 istream 对象读取输入数据。
    • << 运算符:向 ostream 对象写入输出数据。
    • getline 函数:从 istream 对象读取一行数据,写入 string 对象
  • #include :istream、ostream和iostream
  • #include :ifstream、ofstream和fstream
  • #include :istringstream、ostringtream和stringstream
  • C++语言不直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO。这些类型支持从设备读取数据、向设备写入数据的IO操作。
  • 写代码时注意:
    • (1)在使用一个流之前检查它是否处于良好状态
    • (2)对一个文件流进行open是否成功的检测

0. 缓冲区

  • 参考:https://blog.csdn.net/fuhanghang/article/details/109756207
  • 什么是缓冲区
    • 缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。
    • 缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
  • 为什么要引入缓冲区
    • 比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。
    • 又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。
    • 现在您基本明白了吧,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。
  • 缓冲区的类型——分为三种类型:全缓冲、行缓冲和不带缓冲。
    • 1、全缓冲
      • 在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。
    • 2、行缓冲
      • 在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据。
    • 3、不带缓冲
      • 也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。
  • 缓冲区的刷新
    • 下列情况会引发缓冲区的刷新:
      • 1、缓冲区满时
      • 2、执行flush语句
      • 3、执行endl语句
      • 4、关闭文件
  • 可见,缓冲区满或关闭文件时都会刷新缓冲区,进行真正的I/O操作。另外,在C++中,我们可以使用flush函数来刷新缓冲区(执行I/O操作并清空缓冲区),如:
cout<<flush ; // 将显存的内容立即输出到显示器上进行显示
cout<<endl ; // endl控制符的作用是将光标移动到输出设备中下一行开头处,并且清空缓冲区
// 相当于cout<<”\n” <<flush ;

1. IO类

  • 标准库定义的IO类型
    • iostream头文件(针对流):定义了用于读写流的基本类型,从标准流中读写数据
    • fstream头文件(针对文件):定义了读写命名文件的类型,从文件中读写数据
    • sstream头文件(针对string):定义了读写内存中 string 对象的类型。
  • 继承关系
  • 通常可以将一个派生类(继承类)对象当作其基类(所继承的类)对象来使用,这是通过 继承机制(inheritance) 实现的。
  • 类型ifstream和istringstream都继承自istream,所以我们怎样用istream,就可以怎样用ifstream和istringstream。类似的,ofstream和ostringstream都继承自ostream。
  • IO库大致可操作三类数据: 控制台流(stream) ,文件(file) ,字符串 (string)。
  • 操作类型又可分三类:输入(in), 输出(out) ,输入与输出(in/out)。
  • iostream库里面创建了3个标准流对象:
    • cin 表示标准输入的istream对象,cin可以使我们从设备读取数据。
    • cout 表示标准输出的ostream对象,cout可以使我们向设备写入数据。
    • cerr 表示标准错误的ostream对象,cerr是导出程序错误消息的地方,只能向屏幕设备写数据。
  • 标准的流对象都有默认的设备
    • cout << data;  cout默认的设备是显示器缓冲区
    • cin >> data;  cin默认的设备是键盘缓冲区
  • 宽字符版本的IO类型和函数的名字以 w 开始,如 wcin、wcout 和 wcerr 分别对应 cin、cout 和cerr。它们与其对应的普通 char 版本都定义在同一个头文件中,如头文件 fstream 定义了 ifstream 和 wifstream 类型。
    • 例如我们要保存一个字符 'a' 可以定义 char, 但是我们要保存字符 '家' 就无法按使用char 而要使用 wchar_t 了。IO类也有这样的区分,例如我们要在控制台输出中文字符就只能用 wcout << "你好" << endl,想要将中文保存到文本就要用wofstream 或 wfstream 。要正确读取包含中文字符文件要使用 wifstream 类。

1.1 IO对象无拷贝或赋值

  • 不能拷贝或对IO对象赋值
  • IO对象不能存在容器里。
ofstream out1, out2;
out1 = out2;    			// 错误:不能对流对象赋值
ofstream print(ofstream);   // 错误:不能初始化ofstream参数
out2 = print(out2);     	// 错误:不能拷贝流对象
  • 由于IO对象不能拷贝,因此不能将函数形参或返回类型定义为流类型
  • 进行IO操作的函数通常以引用方式传递和返回流
  • 读写一个IO对象会改变其状态,因此传递和返回的引用不能是 const 的。
  • 总结:
    • IO对象无拷贝或赋值
    • 函数形参和返回类型不能是流类型
    • 函数形参和返回类型一般是流的引用
    • 传递和返回的引用不能是const

1.2 条件状态

  • IO操作一个与生俱来的问题是可能发生错误。一些错误是可恢复的,而其他错误则发生在系统深处,已经超出了应用程序可以修正的范围。
  • IO库条件状态:可被任意流类使用的一组标志和函数,用来指出给定流是否可用

  • 注意:上表中,strm代表一种IO类型(如istream),s代表一个流对象
  • 只有当一个流处于无错状态时,我们才可以从它读取数据,向它写入数据
  • 由于流可能处于错误状态,因此代码通常应该在使用一个流之前检查它是否处于良好状态
  • 检查一个流对象的状态的最简单的方法是将它当作一个条件来使用
while (cin >> word)
    // ok: 读操作成功....
  • 将流作为条件使用,只能告诉我们流是否有效,而无法告诉我们具体发生了什么。有时我们也需要知道流为什么失败来做不同的后续处理——通过查询流的状态

  • IO库定义了一个与机器无关的iostate类型,它提供了表达流状态的完整功能。这个类型应作为一个位集合来使用。IO库定义了4个iostate类型的constexpr值,表示特定的位模式。这些值用来表示特定特定类型的IO条件,可以与位运算符一起使用来一次性检测或设置多个标志位

  • 流的状态种类

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

    • good 函数在所有错误均未置位时返回 true。
    • 而 bad、fail 和 eof 函数在对应错误位被置位时返回 true。
    • 此外,在 badbit 被置位时,fail 函数也会返回 true。
    • 因此应该使用 good 或 fail 函数确定流的总体状态,eof 和 bad 只能检测特定错误
  • 管理条件状态

    • 流对象的 rdstate 成员返回一个 iostate 值,表示流的当前状态
    • setstate 成员用于将指定条件置位(叠加原始流状态)。
    • clear 成员的无参版本清除所有错误标志含参版本接受一个 iostate 值,用于设置流的新状态(覆盖原始流状态)。
auto old_state = cin.rdstate();     // 记住cin的当前状态
cin.clear();    			// 使cin有效
process_input(cin);     	// 使用cin
cin.setstate(old_state);    // 将cin置为保存的原有状态

1.3 管理输出缓冲

  • 每个输出流都管理一个缓冲区,用于保存程序读写的数据。

  • 执行输出的代码,文本串可能立即打印出来,也可能被操作系统保存在堆栈内,随后再打印。

  • 注意:如果程序异常终止,输出缓冲区是不会被刷新的。当一个程序崩溃后, 它所输出的数据很可能停留在输出缓冲区中等待打印。

  • 当调试一个已经崩溃的程序时,需要确认那些你认为已经输出的数据确实已经刷新了。否则, 可能将大量时间浪费在追踪代码为什么没有执行上,而实际上代码已经执行了,只是程序崩溃后缓冲区没有被刷新,输出数据被挂起没有打印而已。

  • 导致缓冲刷新(即数据真正写入输出设备或文件)的原因有很多:

  • (1)程序正常结束:作为main函数的return操作的一部分,缓冲刷新被执行

  • (2)缓冲区已满

  • (3)使用操纵符(如 endl)显式刷新缓冲区

    • 操纵符endl:输出一个换行符并重新刷新缓冲区
    • 操纵符flush:刷新缓冲区,但不输出任何额外字符
    • 操纵符ends:向缓冲区插入一个空字符null,然后刷新缓冲区
cout << "hi!" << endl;   // 输出hi和一个换行,然后刷新缓冲区
cout << "hi!" << flush;  // 输出hi,然后刷新缓冲区,不附加任何额外字符
cout << "hi!" << ends;   // 输出hi和一个空字符,然后刷新缓冲区
  • (4)操纵符unitbuf:告诉流接下来每次输出操作后都刷新缓冲区。
    • 如果想在每次输出操作后都刷新缓冲区,可以使用 unitbuf 操纵符。它令流在接下来的每次写操作后都进行一次 flush 操作。而 nounitbuf 操纵符则使流恢复使用正常的缓冲区刷新机制。
    • 默认情况下,对 cerr 是设置 unitbuf 的,因此写到 cerr 的内容都是立即刷新的。
cout << unitbuf;    // 所有输出操作后都会立即刷新缓冲区
// 任何输出都立即刷新,无缓冲
cout << nounitbuf;  // 回到正常的缓冲方式
  • (5)一个输出流可以被关联到另一个流。这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。默认情况下,cin 和 cerr 都关联到 cout,因此,读 cin 或写 cerr 都会刷新 cout 的缓冲区。
    • 当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流
    • 标准库将cout和cin关联在一起,因此下面的语句会导致cout的缓冲区被刷新:
cin >> ival;
  • 交互式系统通常应该关联输入流和输出流。这意味着包括用户提示信息在内的所有输出,都会在读操作之前被打印出来

  • 使用 tie 函数可以关联两个流。它有两个重载版本:

    • 无参版本:返回指向输出流的指针。如果本对象已关联到一个输出流,则返回的就是指向这个流的指针,否则返回空指针。
    • 第二个版本:接受一个指向 ostream 的指针,将本对象关联到此 ostream。
cin.tie(&cout);     // 仅仅是用来展示:标准库将cin和cout关联在一起
// old tie指向当前关联到cin的流(如果有的话)
ostream *old_tie = cin.tie(nullptr); // cin不再与其他流关联
// 将cin与cerr关联;这不是一个好主意,因为cin应该关联到cout
cin.tie(&cerr);     // 读取cin会刷新 cerr而不是cout
cin.tie(old_tie);   // 重建cin和cout间的正常关联
  • 每个流同时最多关联一个流,但多个流可以同时关联同一个 ostream。向 tie 传递空指针可以解开流的关联

2. 文件输入输出:文件内换行也是用endl

  • 头文件 fstream 定义了三个类型来支持文件IO:
    • ifstream 从给定文件读取数据
    • ofstream 向指定文件写入数据
    • fstream 可以同时读写指定文件。
  • 上表中的fstream不是指头文件,而是指头文件fstream定义的一个类型,如fstream/ofstream/ifstream

2.1 使用文件流对象

  • 每个文件流类型都定义了open函数,它完成一些系统操作,定位指定文件,并视情况打开为读或写模式。

  • 创建文件流对象时,如果提供了文件名(可选),open 会被自动调用

ifstream in(ifile);   	// 构造一个ifstream并打开给定文件
ofstream out;   	// 输出文件流未关联到任何文件
  • 在C++11中,文件流对象的文件名可以是 string 对象或C风格字符数组。旧版本的标准库只支持C风格字符数组。

  • 用fstream代替iostream&

    • 在要求使用基类对象的地方,可以用继承类型的对象代替。因此一个接受 iostream 类型引用或指针参数的函数,可以用对应的 fstream 类型来调用。
  • 可以先定义空文件流对象,再调用 open 函数将其与指定文件关联。如果 open 调用失败,failbit 会被置位

ifstream in("a.txt");  // 隐式调用open
// 等价于
// ifstream in;
// in.open("a.txt");
// 检测open是否成功
if(!in){
    cerr << "无法打开输入文件" << endl;
    return -1;
}
  • 如果open调用成功,则open会设置流的状态,使得good()为true
  • 一旦一个文件流已经打开,它就保持与对应文件的关联对一个已经打开的文件流调用 open 会失败,并导致 failbit 被置位。随后试图使用文件流的操作都会失败
  • 如果想将文件流关联到另一个文件,必须先调用 close 关闭当前文件,若有必要还可以调用 clear 重置流的条件状态(close 不会重置流的条件状态)。文件成功关闭后,我们才可以打开新的文件:
in.close();  // 关闭文件
in.open("b.txt");  // 打开另一个文件
  • 当一个 fstream 对象离开其作用域时会被销毁(如在for循环内部每个循环步中都要创建和销毁一次),close 会自动被调用,与之关联的文件会自动关闭

2.2 文件模式

  • 每个流都有一个关联的文件模式,用来指出如何使用文件。
  • ate和app区别
    • ate 首次打开文件并定位到末尾,但可以用seekg(Pos)改变位置
    • app 追加的方式打开文件(本来就含有ate的功能),因为是追加的方式,不管你是否移动位置,都会在末尾插入
  • 对文件流指定文件模式有如下限制
    • 只能对 ofstream 或 fstream 对象设定 out 模式。
    • 只能对 ifstream 或 fstream 对象设定 in 模式。
    • 只有当 out 被设定时才能设定 trunc 模式。
    • 只要 trunc 没被设定,就能设定 app 模式。在 app 模式下,即使没有设定 out 模式,文件也是以输出方式打开。
    • 默认情况下,即使没有设定 trunc,以 out 模式打开的文件也会被截断。如果想保留以 out 模式打开的文件内容,就必须同时设定 app 模式,这会将数据追加写到文件末尾;或者同时设定 in 模式,即同时进行读写操作。
    • ate 和 binary 模式可用于任何类型的文件流对象,并可以和其他任何模式组合使用。
  • 每个文件流类型都定义了一个默认的文件模式,当我们未指定文件模式时,就使用此默认模式。与 ifstream 关联的文件默认以 in 模式打开;与 ofstream 关联的文件默认以 out 模式打开;与 fstream 关联的文件默认以 in 和 out 模式打开
  • 总结:
    • ofstream:out模式(默认)
    • fstream:in模式,out模式,in模式+out模式(默认)
    • ifstream:in模式(默认)
    • out模式组合
      • (1)默认截断:清空文件内容
      • (2)+trunc模式:显式截断
      • (3)+app模式:防止文件内容清空,将数据追加写到文件末尾——>等效于单独app模式:即使没有设定 out 模式,文件也是以输出方式打开
      • (4)+in模式:同时进行读写操作
  • 每种文件类型都有定义了自己能用的文件模式:
    • 如:ofstream::out、ofstream::app等
  • 二元或运算符 | 可以用来组合文件模式:mode1 | mode2| mode3....
    • 如:ofstream::out | ofstream::app
  • 直接以out模式打开文件会丢弃已有数据
    • 默认情况下,打开 ofstream 对象时,文件内容会被丢弃,阻止一个 ofstream 清空给定文件内容的方法是同时指定 app 模式**:
// 等效的指定out模式方式:
ofstream out("file1");  // 隐含以out模式打开文件并截断文件
ofstream out2("file1", ofstream::out);  // 隐含地截断文件
ofstream out3("file1", ofstream::out | ofstream::trunc); 
 
// 等效的指定app模式方式:
//为了保留文件内容,必须显式指定app模式
ofstream app("file2", ofstream::app);  // 隐含为输出模式
ofstream app("file2", ofstream::out | ofstream::app);  
  • 保留被 ofstream 打开的文件中已有数据的唯一方法是显式指定 app 或 in 模式
  • 流对象每次打开文件时都可以改变其文件模式。
ofstream out;   		// 未指定文件打开模式
out.open("scratchpad");    	// 模式隐含设置为输出和截断
out.close();    		// 关闭out,以便我们将其用于其他文件
out.open("precious", ofstream::app);   // 模式为out和app
out.close();
  • 当 C++ 程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。但程序员应该养成一个好习惯,在程序终止前关闭所有打开的文件。
  • 写入操作:流对象 << 写入内容 (<< endl换行和刷新)
#include <iostream>
#include <fstream>
using namespace std;

int main(){
    //标准输出(写到显示屏幕上)
    char ch[] = "hello world!";
    cout << ch << endl; 
    //文件输入(写到文件里)
    ofstream fout;
    fout.open("data.txt");//将fout对象和文件绑定起来()
    fout << ch << endl;
    return 0;
}
  • 读取操作:流对象 >> 保存内容变量
  • 注意:文件流对象使用完成后需要close()进行关闭
  • 读取 & 写入实例
    • 下面的 C++ 程序以读写模式打开一个文件。在向文件 afile.dat 写入用户输入的信息之后,程序从文件读取信息,并将其输出到屏幕上:

include

include

using namespace std;

int main ()
{
char data[100];

// 以写模式打开文件
ofstream outfile;
outfile.open("afile.dat");
			 
cout << "Writing to the file" << endl;
cout << "Enter your name: "; 
cin.getline(data, 100);
			 
// 向文件写入用户输入的数据
outfile << data << endl;
			 
cout << "Enter your age: "; 
cin >> data;
cin.ignore();
			   
// 再次向文件写入用户输入的数据
outfile << data << endl;
			 
// 关闭打开的文件
outfile.close();
			 
// 以读模式打开文件
ifstream infile; 
infile.open("afile.dat"); 
			 
cout << "Reading from the file" << endl; 
infile >> data; 

// 在屏幕上写入数据
cout << data << endl;
			   
// 再次从文件读取数据,并显示它
infile >> data; 
cout << data << endl; 
			 
// 关闭打开的文件
infile.close();
			 
return 0;

}

3. string流

  • 头文件 sstream 定义了三个类型来支持内存IO:
    • istringstream 从 string 读取数据
    • ostringstream 向 string 写入数据
    • stringstream 可以同时读写 string 的数据。
  • 上表中sstream是头文件sstream中任意一个类型。s是一个string。

3.1 使用istringstream

  • 当某些工作是对整行文本进行处理, 而其他一些工作是处理行内的单个单词时,通常可以使用 istringstream。
// 成员默认为公有
struct PersonInfo
{
    string name;
    vector<string> phones;
};

string line, word;   // 分别保存来自输入的一行和单词
vector<PersonInfo> people;    // 保存来自输入的所有记录
// 逐行从输入读取数据,直至cin遇到文件尾(或其他错误)
while (getline(cin, line))
{
    PersonInfo info;    		// 创建一个保存此记录数据的对象
    istringstream record(line);    	// 将记录绑定到刚读入的行
    record >> info.name;    		// 读取名字
    while (record >> word)  		// 读取电话号码
        info.phones.push_back(word);   // 保持它们
    people.push_back(info);    		// 将此记录追加到people末尾
}

3.2 使用ostringstream

  • 当逐步构造输出, 希望最后一起打印时, ostringstream 是很有用的。
for (const auto &entry : people)
{ 
    // 对people中每一项
    ostringstream formatted, badNums;   // 每个循环步创建的对象
    for (const auto &nums : entry.phones)
    { 
    	// 对每个数
        if (!valid(nums))
        {
            badNums << " " << nums;  	// 将数的字符串形式存入badNums
        }
        else
            // 将格式化的字符串"写入"
            formatted << " " << format(nums);
    }
    if (badNums.str().empty())   	// 没有错误的数
        os << entry.name << " "  	// 打印名字
            << formatted.str() << endl;   // 和格式化的数
    else  // 否则,打印名字和错误的数
        cerr << "input error: " << entry.name
            << " invalid number(s) " << badNums.str() << endl;
}
posted @ 2021-05-31 13:10  夏目的猫咪老师  阅读(791)  评论(0编辑  收藏  举报