首页 > 编程语言 >C++ Primer 学习笔记——第八章

C++ Primer 学习笔记——第八章

时间:2023-07-30 21:14:05浏览次数:42  
标签:文件 strm fstream 第八章 类型 C++ IO 缓冲区 Primer

第八章 IO库

前言

C++语言并不会直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO。这些类型支持从设备中读取数据、向设备写入数据IO操作。设备可以是文件、控制台窗口等,还有一些类型允许内存IO。

IO库定义了读写内置类型值的操作。


8.1 IO类

在之前我们使用的IO类型和对象都是操作char数据且这些对象都是关联到用户的控制台窗口。但在实际开发中还不够,所以在C++的IO操作中还包括一下类型供开发者使用。

在头文件iostream定义了读写流的基本类型、fstream定义了读写命名文件的类型、sstream定义了读写内存string对象的类型。

头文件 类型
iostream istream,wistream 从流读取数据
ostream,wostream向流写入数据
iostream,wiostream读写流
fstream ifstream,wifstream从文件读取数据
ofstream,wofstream向文件写入数据
fstream,wfstream读写文件
sstream istringstream,wistringstream从stream读取数据
ostringstream,wostringstream向string写入数据
stringstream,wstringstream读写string

为了支持宽字符的语言,标准库定义了一组类型与对象来操作wchar_t类型的数据。宽字符版本的类型和函数的名字以一个w开始。

从概念上讲,IO操作并不会因为设备类型和字符大小而受到影响。例如,我希望通过某个文件读取宽字符数据,其与在终端窗口读取普通字符数据其操作都是一致的,都是通过输入运算符>>
。那么这样就存在一个好处,我们可以忽略不同类型的流之间的差异(但并不是不存在差异),使得开发效率得到提高。

这种忽略流差异的技术通过继承机制(inheritance)实现,利用模板,通过使用具有继承关系的类使得我们忽略工作细节。

IO对象无拷贝或赋值

如标题,IO对象不存在拷贝或者赋值初始化的操作:

ofstream of_1,of_2;
of_1=of_2; /* 错误:无法对流对象进行赋值 */
ofstream print(ofstream); /* 错误:无法初始化ofstream参数 */
of_2=print(of_2); /* 错误:无法拷贝流对象 */

由此引申出,无法将返回类型或者形参设置为流类型,同时由于读写一个IO对象会改变其状态,所以常常使用引用方式传递和返回流且此引用不能为const。

状态条件

IO操作并不是万无一失的,其潜在可能发生的错误,有一些错误能够较为容易修复,但是有一些错误其可能在系统层面,其修复的范围远远超过应用层面,这时就需要一些IO操作上的函数或者标志来帮助程序确定IO操作状态,其称为访问和操作流的
条件状态(condition state)。

状态名 解释
strm::iostate iostate是一种机器相关的类型,提供了表达条件状态的完整功能
strm::badbit 指出流已经崩溃
strm::failbit 指出一个IO操作失败
strm::eofbit 指出流已经到达文件结束
strm::goodbit 指出流处于错误状态,此值保证为零
s.eof() 若s流的eofbit置位,则返回true
s.fail() 若s流的failbit或者badbit置位,则返回true
s.bad() 若s流的badbit置位,则返回true
s.good() 若s流处于有效状态,则返回true
s.clear() 将s流中所有条件状态位复位,将流的状态设置为有效,返回void
s.clear(flags) 根据给定的flags标志位,将s流中对应的条件状态位复位。
flags的类型为strm.iostate,返回void
s.setstate(flags) 根据给定的flags标志位,将s流中对应的条件状态位置位。
flags的类型为strm.iostate,返回void
s.rdstate() 返回s流的当前条件状态。
返回值的类型为:strm::iostate

strm是一种IO类型,s为流

当一个流发生错误,那么后续的IO操作都会失败,为了程序的健壮性,通常需要使用流之前判断其是否处于良好状态。最简单的方式:

 while(cin>>word)
    /* ok,next */

当流出现问题,我们肯定希望查询到错误原因,这个时候就需要依赖条件状态,IO库定义了一个与机器相关的iostate类型,其提供表达流状态的完整功能,作为一个位集合使用。通过位运算符进行一次性检测或者设置多个标志位。

具体来讲:

  • badbit,系统级错误,例如:不可恢复的读写错误。当badbit被置位,流就无法再使用
  • failbit,发生可恢复错误,例如:期望读取数值结果读取字符,当错误被修复,流还可以使用。
  • 当读取文件结束,eofbit和failbit都会被置位,goodbit值为0,表示流未发生错误。
  • 如果badbit、failbit和eofbit任意一个被置位,则检测状态的条件会失败。

在上述描写到,IO库定义的一系列查询标志位的函数,当错误位被置位时其对应的函数就会返回true,注意一点,无论是badbit还是eofbit还是其本身failbit被置位,都会同时触发fail()
函数,所以在上述判断流状态的条件代码实际等价于!fail()

管理条件状态

在上述列表上介绍了四种管理条件状态的函数,我们可以使用clear()清除所有错误标志位,也可以使用clear(flags)清除指定的错误标志位,例如:

/* 假设cin出现所有的错位状态位 */
/* 希望复位单一状态位 */
cin.clear(cin.rdstate()&~cin.failbit&~cin.badbit);

我们可以通过读取当前状态,例如上述代码,我们就可以复位failbit和badbit,但是eofbit保持不变。

管理输出缓冲

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

缓冲机制的存在可以带来很大的性能提升,操作系统可以将(多个)程序的(多个)输出操作组合成为单一的系统级别写操作。

例如:

cout<<"hello";
cout<<"world";
cout<<endl;
cout<<"!";

在前两行表达式就是将输出操作组合在一起,都存放在缓冲区。第三行则进行缓冲区的刷新,那么第四行表达式中数据就和前两行数据不存放在一起(前两行数据已经被刷新掉了)。

导致缓冲刷新的原因有很多,例如:

  • 程序正常结束,作为main函数的return操作的一部分,缓冲刷新将会被执行
  • 缓冲区已满,后入的数据只有在刷新缓冲区后才能继续写入缓冲区
  • 使用操作符endl显式刷新缓冲区
  • 在每个输出操作之后,使用操作符unitbuf设置流的内部状态,借此清空缓冲区
    • 默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的
  • 一个输出流可能被关联到另一个流
    • 当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,cin和cerr都关联到cout。因此,读cin或者写cerr都会导致cout的缓冲区被刷新。

刷新输出缓冲区

在此之前,我们使用操作符endl来进行换行和刷新缓冲区(当时我们可能还没注意到endl具有刷新缓冲区的功能)。类似的,IO库中还存在flush和ends两种操作符也可以执行刷新操作。

flush刷新缓冲区,但是不输出任何额外的字符(类似endl但不换行);ends向缓冲区插入一个空字符,然后刷新缓冲区。

如果想要在每次输出操作后都执行刷新缓冲区的操作,那么我们可以使用unitbuf操作符。它告诉流在接下来的每次写操作之后都会进行一次flush操作。nounitbuf操作符则是重置流,使其恢复使用正常的系统管理的缓冲区刷新机制。

cout<<unitbuf; /* 下面所有的输出操作均会立即刷新缓冲区 */
/* .... */
cout<<nounitbuf; /* 回到正常的缓冲方式 */

注意

如果程序崩溃,输出缓冲区是不会被刷新的

如果程序异常终止,其缓冲区不会被刷新。其所输出的数据很可能停留在输出缓冲区中等待打印

注意这个细节,如果我们调试一个已经崩溃的程序,需要检查认为已经输出的数据确实已经被刷新了,否则追踪一个没有价值的代码是毫无意义的。

关联输入和输出流

当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。

在IO库中,cin默认已经和cout关联在一起,也就是说当执行cin语句时,在此之前的cout的缓冲区
将会被刷新。

开发

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

我们可以通过tie函数关联流。

cin.tie(&cout); /* 将cin和cout关联 */
cin.tie(nullptr); /* cin不再和其他流关联 */
cin.tie(&cerr); /* cin与cerr关联 */

8.2 文件输入输出

在IO库中,头文件fstream定义了三个类型来支持文件IO操作。

  • ifstream,从给定文件读取数据
  • ofstream,向给定文件写入数据
  • fstream,向给定文件读写数据

ifstream继承于iostream,所以常规操作与cin和cout对象操作类似,同样可以使用IO运算符(<<和>>)来读写文件,也可以使用getline从一个ifstream读取数据。

除此之外,fstream还具有一些自己独特的操作:

操作 解释
fstream fstrm; 创建一个未绑定的文件流
fstream fstrm(s); 创建一个fstream,并打开名为s的文件。s可以是string类型,也可以是指向C风格字符串的指针。同时其构造函数都是explicit的。默认的文件模式mode依赖于fstream的类型
fstream fstrm(s,mode); 与上述类似,但是按照指定mode打开文件
fstrm.open(s) 打开名为s的文件,并将文件与fstrm绑定。s的类型与上述类似,默认的文件mode依赖于fstream的类型。返回void
fstrm.close() 关闭与fstrm绑定的文件。返回void
fstrm.is_open() 返回一个bool值,指出与fstrm关联的文件是否成功打开且尚未关闭

使用文件流对象

当打算读取某个文件时,我们需要定义一个文件流对象,并将其与文件关联起来。每个文件流对象都定义了一个open成员函数,其完成一些系统相关的操作(定位给定文件,视情况打开读或写模式)

创建文件流对象,我们可以选择提供文件名(当然,也可以后续提供)。如果提供一个文件名,那么open将会自动调用。

ifstream input(ifile); /* 构造一个ifstream并打开给定文件 */
ofstream output; /* 构造一个ofstream对象,并未关联文件 */

文件名可以是string对象,也可以是C风格字符数组。在C++11版本之前仅允许C风格字符数组。

用fstream代替iostream&

在前文提到,在要求使用基类型对象的地方,我们可以用继承类型的对象来代替。也就是说,在一个接受iostream类型引用(指针)参数的函数,可以使用一个对应的fstream(或者sstream)类型来调用。

成员函数open和close

在前文提到,我们可以定义一个文件流对象,但不将其与文件关联起来。

关联文件使用文件流对象的成员函数open,

ifstream input(ifile); /* 构造一个ifstream并给定文件 */
ofstream output; /* 构造一个ofstream对象,但不关联文件 */
output.open(ifile); /* 关联指定文件 */

但一个文件流已经被打开了,那么其就保持与对应文件的关联。这个时候,希望再次调用成员函数open将会失败且failbit会被置位。如果希望文件流关联另一个文件,需要关闭已经关联的文件。这个时候就需要用到成员函数close。

input.close(); /* 关闭文件 */
input.open(ifile_1); /* 关联另一个文件 */

如果open成功,那么open将会设置流的状态,这个时候IO库条件状态good()将会为true。

当一个fstream对象离开其作用域,与之关联的文件会自动关闭。当一个fstream对象被销毁时,close会自动调用。

开发

由于调用open可能会失败,所以在进行open时应该习惯于进行是否成功的检测。

文件模式

每个文件流都有一个关联的文件模式(file mode),用来指出如何使用文件

文件模式 解释
in 以读方式打开
out 以写方式打开
app 每次写操作前均定位到文件末尾
ate 打开文件后立即定位到文件末尾
trunc 截断文件
binary 以二进制方式进行IO

每个文件流类型都定义了一个默认的文件模式。ifstream默认以in模式打开,ofstream默认以out模式打开,fstream默认以in和out模式打开。

无论那种方式打开文件,我们都可以指定文件模式。当然指定是有限制的:

  • 只可以对ofstream或者fstream对象设定out模式
  • 只可以对ifstream或者fstream对象设定in模式
  • trunc模式的设定前提是out模式被设定
  • app模式设定前提是trunc模式没有被设定。在app模式下,即便没有显式指定out模式,文件也总是以输出方式被打开
  • 默认情况下,out模式打开的文件是会被截断的(即便没有设定trunc),如果想要保留则需要指定app模式,或者同时指定in模式,使打开文件同时进行读写操作
  • ate和binary模式可用用于任何类型的文件流对象,且可以与其他任何文件模式组合使用

在上文我们提到out模式默认会截断,即清空文件已有数据。所以如果希望保留则需要显式指定app或者in模式

8.3 string流

sstream头文件定义了三种类型来支持内存IO。

和fstream和iostream类似,sstream的三种类型分别对string读取数据、写入数据和读取写入数据:istringstream、ostringstream、stringstream。

当然,除了继承iostream的操作,sstream也具有对内存IO类型的特殊操作:

操作 解释
sstream strm; strm是一个未绑定的sstream对象
sstream strm(s); strm作为sstream对象,保存string s的一个拷贝
strm.str() 返回strm所保存的string的拷贝
strm.str(s) 将string s拷贝到strm中。,返回void

当我们的某些工作是对整行文本进行处理,而其他工作是处理行内的单个单词时,通常考虑使用istringstream。

当我们逐步构造输出,希望最后一起打印时,ostringstream通常是我们的一般解。

总结

fstream和sstream都继承于iostream,所以在操作上三者具有很多相同点。

对于声明在语句外的流,可以通过在语句块内部使用clear函数解决EOF问题。

条件状态的特性需要记牢!

标签:文件,strm,fstream,第八章,类型,C++,IO,缓冲区,Primer
From: https://www.cnblogs.com/aaroncoding/p/17592035.html

相关文章

  • C++入门:命名空间
    在C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称都将存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。1.命名空间的定义定义命名空间,需要使用到namespace关键字,......
  • C++入门:缺省参数
    1.缺省参数的概念缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的参数。#include<iostream>usingnamespacestd;voidFunc(inta=0){ cout<<a<<endl;}intmain(){ Func(); Func(1);......
  • 初识C++
    C++对C语言设计不合理的地方进行了优化,下面来看一下到底进行了什么优化。一、C++关键字(C++98)C++总计63个关键字,C语言32个。二、命名空间在C/C++中,变量、函数、和类都是大量存在的,这些变量、函数和类的名称都将存在于全局作用域中,可能会导致很多命名冲突。使用命名空间的目的就是对......
  • C++ Primer Plus 第6版 读书笔记(8)第 8章 函数探幽
    第8章函数探幽本章内容包括:内联函数。引用变量。如何按引用传递函数参数。默认参数。函数重载。函数模板。函数模板具体化。通过第7章,您了解到很多有关C++函数的知识,但需要学习的知识还很多。C++还提供许多新的函数特性,使之有别于C语言。新特性包括内联函数、......
  • C++ assert学习
    转自:https://blog.csdn.net/m0_51913750/article/details/1309864651、介绍  assert是一个宏,用于在运行时检查一个条件是否为真,如果条件不满足,则运行时将终止程序的执行并输出一条错误信息。条件满足为true时,不做任何操作。assert宏的使用可以帮助程序员检查程序的正确性,并......
  • C++虚函数、static_cast、dynamic_cast
        C++虚函数:当一个类中拥有至少一个虚函数,那么编译器就会构建出一个虚函数表来指示这些函数的地址,假如继承该类的子类定义并实现了一个同名并具有同样函数签名的方法重写了基类中的方法,那么虚函数表会将该函数指向新的地址。    此时多态性就体现出来了:当我们将基......
  • C++ error学习
    转自:https://blog.csdn.net/NiuYoohoo/article/details/849304041、介绍errno是用于错误指示的预处理器宏,数个标准库函数通过写入正整数到errno指示错误。程序启动时errno的值为​0​,而且尽管不管在错误发生与否时,允许写入正整数到errno。2、例子#include<iostrea......
  • C++ 算法进阶系列之再聊聊动态规划的两把刷子
    1.前言递归和动态规划是算法界的两个扛把子,想进入算法之门,则必须理解、掌握这两种算法的本质。一旦参悟透这2种算法的精髓,再加上对树、图等复杂数据结构的深入理解,可以解决大部分的算法问题。本文通过几个典型案例,再次聊聊动态规划算法。其实动态规划算法也就2把刷子。找到......
  • C++程序获取python脚本控制台输出的一种方法
    作者:朱金灿为什么大多数人学不会人工智能编程?>>>  最近要使用C++程序调用python脚本,调用方法是通过启动python进程来调用,其中遇到的一个问题是在C++程序中需要获取python脚本的控制台输出信息。经过摸索使用_popen函数实现了。下面用python脚本和C++调用示例程序来说明。py......
  • VS选择Visual C++中的控制台项目和空项目、Windows桌面应用程序三者之间有什么区别?
    在VisualStudio中创建C/C++项目时,可以选择控制台项目、空项目和Windows桌面应用程序,它们有以下区别:控制台项目(ConsoleApplication):这种项目类型适用于命令行应用程序的开发。它提供一个命令行界面,可以在控制台中进行输入和输出操作,通常用于简单的控制台程序,如计算器、文件......