======深入IO======
//第 7 章笔记//
----
====IOStream 概述====
* IOstream 把输入输出视作字节流(流式IO)
* 可以在其基础上构建记录 IO
* 流式 IO
* 记录 IO
===需要处理的问题===
* 表示形式的变化:使用格式化 / 解析将数据内部的表示(二级制)转化为字符序列(或反向转换,比如 ''std::cin'')
* 与外部设备的通信:对于不同的外部设备(''IOstream'' 指定的外部设备:比如终端,文件,内存)等会有不同的处理逻辑
int x = 100;
// 通过格式化的方式,将二进制的形式转换成了相应的字符序列的表示
std::cout << x << std::endl;
// 使用一块内存
// 用于保存 x 或者 y
union
{
int x;
float y;
};
x = 100;
// 使用不同的方式去解析同一内存中的内容
// float 与 int 的解析方式不同
std::cout << x << std::endl;
std::cout << y << std::endl;
===涉及的操作===
* 格式化 / 解析:将内容转化为字节序列 / 将字节序列转化为内容
* 缓存:将要输出的内容置于缓存中,在缓存将满的时候一次性输出
* 编码转换:比如 UTF8 到 GB(转换字符所占内存的表示)
* 传输:从缓存中逐一进行读取
==采用的技术==
* ''std::basic_stream'':
* 封装设备特性:通过不断的继承,在每一步的继承中实现不同设备的输出
* 封装字符特性:使用类模板中的参数,通过替换该参数实现不同的流(比如使用 ''std::basic_ifstream'' 处理 ''char'' 类型字符)
* 实际上是类模板实例化的结果
====输入和输出====
* 输入输出分为格式化和非格式化两类
===非格式化 I/O===
* 输入:''get'' / ''read''/ ''getline'' / ''gcount''
* 输出: ''put'' / ''write''
* 特点:对计算机(性能)友好,不会通过格式化来改变内容的长度(比如输出 ''float'' 就只会最多输出 4个字节的内容)
int x;
//对指定地址的内容的字符进行读取
//不解析,直接放入 x
//一共读取 4个字符 100加上回车,对应的就是每个字符的 ascii 值
//注意:read 要求 4个字符,在没有满足条件之前,系统会一直等待用户输入
std::cin.read(reinterpret_cast(&x), sizeof(x));
===格式化 I/O===
* 使用移位操作符 ''>>'' 和 ''<<'' 的重载来输入输出
* 特点:对人友好,根据不同的类型(内建,自定义)进行**重载**,输出不同的格式:
char c = '0';
// 针对 char 输出字符 0
std::cout << c << std::endl;
// 针对 Int 输出 ascii 48
int ci = static_cast(c);
std::cout << ci << std::endl;
==格式控制==
* 位掩码类型:''showpos'',改变的是格式化的行为
char c = '0';
// 显示正负,输出 +48
// 只对数值产生影响
std::cout.setf(std::ios_base::showpos);
* 取值随意的格式化参数:''width()''
// 让输出占10个字符,默认往左边加空格
// 默认只生效一次,读取后会被 reset 为 0
std::cout.width(10);
* 填充字符 ''fill''
// 使用 * 占位
std::cout.fill('*');
===操纵符===
* manipulator:允许直接在输入输出流中使用格式控制:
// 等同与之前的 showpos 用法,输出 +48
// witdh 的替换,需要使用 iomanip 头文件,使用 setw()
// fill 的替换,使用 setfill()
std::cout << std::showpos << std::setw(10) << std::setfill('*') << ci << std::endl;
==其他操纵符==
* ''std::endl''
===输入相关===
* 具有提取操作:
* 会放松对提取的类型限制:比如使用 ''std::cin'' 时,空格+10,+10,+010 都会被提取为 ''10''
* 提取操作有限制(某些信息不能智能进行提取)
==C风格字符串的内存越界==
char y[5];
// 输入长度超过 4字节 (4+\0) 均会导致内存越界
// std::string 存在缓存机制,不受限制
std::cin >> y;
// 使用 setw() 控制输入的长度,提取前 4 个字符
std::cin >> std::setw(5) >> y;
====文件与内存操作====
===文件流===
* ''basic_ifstream'' / ''basic_ofstream'' / ''basic_fstream'':别名 ''i/f stream'',类模板,流中单位为 ''char''
* 输入 / 输出 / 同时打开输入输出
==简单示例==
// 传入参数为文件名
// 输出流
std::ofstream outFile("test_file");
// 输出到文件 test_file
outFile << "hello";
// 输入流
std::ifstream inFile("test_file");
std::string x;
inFile >> x;
std::cout << x << std::endl;
==文件流状态==
* 检测状态 ''std::basic_ostream::isopen()''
* 打开文件,关联流:''outFile.open("test_file");''
* 关闭文件,关闭流:''outFIle.close();''
文件流会处于打开 / 关闭状态:
* 当与文件关联时,才会处于打开状态
* 关联文件之后,无法关联另外一个文件。必须要先关闭流才能与其他文件绑定(再次打开)
* 缺省定义下(无文件参数),文件流默认关闭。
==系统会使用缓存优化文件流==
* 每次读取写入都会非常消耗资源
* 系统会将缓存内的东西积累起来,在关闭流时写入文件
* 未显示关闭文件流时,''ostream'' 的析构函数会在析构其对象时自动销毁
* 可以使用域控制 ''ostream'' 对象的生存周期,使用其析构函数自动关闭文件流
{
// outFile 会在大括号之后自动关闭流
std::ostream outFile.open("test_file");
}
===打开方式===
^ 标记名 ^ 作用 ^
| ''in'' | 打开供读取 |
| ''out'' | 打开供写入 |
| ''ate'' | 起始位置位于文件末尾 |
| ''app'' | 附加文件,总是向文件末尾写入 |
| ''trunc'' | 截断文件,删除文件中内容 |
| ''binary'' | 二进制模式 |
==组合使用的原理==
上述的打开方式都是以二进制的形态设计的,其中每一种打开方式占一位。如果组合使用,则是以两种方式的**按位或**运算进行:
// in & ate
// 按位 或进行组合
std::ios_base::in; // 0010
std::ios_base::ate; // 0001
// 得到 0011,在文件末尾进行读写
==ate 的使用==
* 用于控制文件读取 / 写入的起始位置
* ''in'' / ''out'' 默认起始为文件开始的位置
* ''ate'' 表示起始读写位置处于文件末尾,但该位置可以移动
* ''app'' 表示起始读写位置处于文件末尾,但该位置**不可**移动
==trunc 的使用==
* 写入,并删除之前文件内的内容
// 将文件中的 hello 替代为 word
std::ofstream outFile("test_file", std::ios_base::out | std::ios_base::trunc);
outFile << "word";
==binary==
* 可以禁止系统内定的转换
==常用组合==
^打开方式^效果^加结尾标记^加二进制标记^
|''in''|只读打开(读)|初始位置位于末尾|禁止系统转换|
|''out|trunc / out''|文件存在则覆盖之前内容,否则建立文件(写)|初始位置位于末尾|禁止系统转换|
|''out|app''|在文件末尾写入(写)|初始位置位于末尾|禁止系统转换|
|''in|out''|打开文件供更新使用(读写)|初始位置位于末尾|禁止系统转换|
|''in|out | trunc''|打开文件,删除已存在内容,并建立文件更新使用(读写)|初始位置位于末尾|禁止系统转换|
''out|trunc'' 和 ''out'' 行为一致。
===内存流===
都定义于 '''' 中:
* ''basic_istringstream'':读
* ''basic_ostringstream'':写
* ''basic_stringstream'':读写
* 处理类型单位默认为 ''char'',也受打开模式的影响(''in''/''out''/''ate''/''app'')
==基础用法示例==
* 注意格式化输出会将输入转化为字符串:
int main(int argc, char const *argv[])
{
std::ostringstream myObj;
// 写入内容到流 (整数)
// 转换整数到字符串(格式化)
myObj << 1234;
// 获取 myObj 所对应内存,使用配套的 str() 返回内容(字符串)
std::string ret = myObj.str();
std::cout << ret << std::endl;
//读取字符串,并格式化(转换字符串为 int)
std::istringstream myObj2(ret);
int x;
//将 int 赋值给 x 并打印
myObj2 >> x;
std::cout << x << std::endl;
return 0;
}
==配合模式使用==
// 默认的模式是从起始位置开始替换
// 默认输出流中的内容是 test
// 所有输出都会基于 test 输出
std::ostringstream myObj3("test", std::ios_base::ate);
myObj3 << '1';
// 打印结果为 test1
// stringstream 会自动管理内存
std::cout << myObj3.str();
==str() 的使用注意==
* 返回 ''std::string''
* 不要**间接**使用 ''str().c_str()'' 将字符串转换为 C 风格的字符串:该行为未定义
* ''str()'' 返回的是右值
* ''c_str()'' 指向了返回右值的内部的首地址,由于返回的是临时值,所以该访问未定义
==使用内存流进行拼接优化==
// 类似 vector 的空间申请
// 可能每次都会进行 allocate
std::string s;
s += "hello";
s += "world";
s += "hello";
// 改良
// 利用 ostringstream 的大缓存进行拼接
// 不会频繁的 allocate
std::ostringstream word;
word << "hello";
word << "world";
word << "hello";
// 结果相同
std::cout << s;
std::cout << word.str();
====流的状态====
* 提供额外的信息,使使用者针对状态进行对应的操作
* 使用 ''std::io_base::iostate'' 维护状态
* 非正常的状态的值都不是 ''0'',几种错误可以同时出现(通过 bit 之间的按位或, //BitMaskType//)
===三种异常状态===
* ''badbit'':不可恢复的流错误(比如读写未关联的流)
* ''failbit'':可恢复的流错误:读 / 写类型不匹配且无法转换(格式化错误),双重关闭一个流
* ''eofbit'':输入序列达到了文件尾部
==检测异常状态的方法==
[[https://en.cppreference.com/w/cpp/io/ios_base/iostate|bit 的组合与返回结果的关系]]
// 使用 cin 的成员检测
std::cout << std::cin.good()
<< std::cin.fail()
<< std::cin.bad()
<< std::cin.eof();
// 转换为 bool 值检测
<(std::cin) << std::endl;
* ''fail'' 和 ''eof'' 可能会被同时设置,但意义不同
* 转换为 bool 值时不会考虑 ''eof''
==利用状态==
// 判断之前的输入提取是否成功
if(std::cin >> x) { //}
==复位流的状态==
* ''clear()'' :设置当前流的指定状态(默认为 ''goodbit'')
* ''setstate()'':将某个状态**附加**到指定流上,
std::cin.clear();
==捕获流的异常==
* 可以通过 ''exception()'' 捕获流的异常
====流的定位====
提取写入时,需要考虑提取写入的位置(定位)。C++ 为此引入了流的定位:
* 获取流
* 设置流
===获取流位置===
* ''tellg()'':获取输入流中,接下来要读取的字符的位置
* ''tellp()'':获取输出流中,当前可以写入的位置(读的是 white space)
* 两者返回一个为整数的 ''pos_type'' 类型。获取失败时,返回 ''pos_type(-1)''
* ''fail() == true'' 时
===设置流位置===
* ''seekg()'' / ''seep()'':用于设置输入 / 输出流(覆盖输出)
==两个重载版本==
* 设置绝对位置:接收 ''pos_type'' 的版本
* 设置相对位置:接收基本位置(''beg'',''end'',''cur'')+ 位移
====流的同步====
系统默认的 “缓冲区满再输入到设备的” 行为很可能带来一些问题:
// 如果 some test 所在流没有满,导致没有被输出到终端
std::cout << "some test";
std::string name;
// 导致输入时没有提示
std::cin >> name;
这种情况下需要**刷新缓冲区**,也就是强制输出缓冲区内容送到设备上。
===基于方法的同步===
* ''flush()'':**输出**流同步,刷新缓冲区
// 成员调用
std::cout.flush();
// 操纵符
std::cout << x << std::flush;
* ''sync()'':**输入**流同步,实现由编译器定义
* 同文件同时关联了输入输出流,要对输入流刷新,得到输出流的信息
* 具体行为与编译器实现有关系
* ''std::unitbuf'':
* 行为:大于一个字符的流必须被清除出缓存区
* 结果:自动立即刷新(影响性能)
* 通常与 ''std::cerr'' (标准错误输出)联用,因为错误信息必须及时更新
===基于绑定的同步===
C++ 中,任意流都可以绑定到一个**输出流**上。当绑定时,绑定流会记录被绑定流中的信息。当绑定时:
* 绑定流在每次输入(输出)时,都会刷新**被绑定流**的缓冲区
* 绑定流可以同时与多个输出流绑定
这种方法实际上从另外的角度解决了之前提到过的问题:
// 如果 some test 所在流没有满,导致没有被输出到终端
std::cout << "some test";
std::string name;
// 导致输入时没有提示
std::cin >> name;
此时如果 ''std::cin'' 绑定了 ''std::cout'',那么当 ''std::cin'' 输入时,就会直接情况输出流的缓存,将我们需要的 //some test//
文本输入终端。
===与 C 标准 I/O 的同步===
* 缺省情况下会与 C 同步
* 可以通过 ''sync_with_stdio'' 关闭同步