What & How & Why

深入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> 处理 char 类型字符)
    • 实际上是类模板实例化的结果

输入和输出

  • 输入输出分为格式化和非格式化两类

非格式化 I/O

  • 输入:get / read/ getline / gcount
  • 输出: put / write
  • 特点:对计算机(性能)友好,不会通过格式化来改变内容的长度(比如输出 float 就只会最多输出 4个字节的内容)

int x;
//对指定地址的内容的字符进行读取
//不解析,直接放入 x
//一共读取 4个字符 100加上回车,对应的就是每个字符的 ascii 值
//注意:read 要求 4个字符,在没有满足条件之前,系统会一直等待用户输入
std::cin.read(reinterpret_cast<char*>(&x), sizeof(x));

格式化 I/O

  • 使用移位操作符 »« 的重载来输入输出
  • 特点:对人友好,根据不同的类型(内建,自定义)进行重载,输出不同的格式:

char c = '0';
// 针对 char 输出字符 0
std::cout << c << std::endl;

// 针对 Int 输出 ascii 48
int ci = static_cast<int>(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|truncout 行为一致。

内存流

都定义于 <sstream> 中:

  • 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:输入序列达到了文件尾部
检测异常状态的方法

bit 的组合与返回结果的关系

// 使用 cin 的成员检测
std::cout << std::cin.good() 
          << std::cin.fail()
          << std::cin.bad()
          << std::cin.eof();
// 转换为 bool 值检测
          <<static_cast<bool>(std::cin) << std::endl;

  • faileof 可能会被同时设置,但意义不同
  • 转换为 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 关闭同步