<C++>8 I/O 流
本文最后更新于:2023年6月27日 上午
8 I/O 流
C++ 中没有输入/输出语句,但有一个面向对象的输入/输出软件包,即 I/O 流类库。
流是 I/O 流类库的核心概念。一般意义上说,凡是数据从一处传输到另一处都是流的操作。
#C++ 流类库关系图:
graph TD
a(ios) --> b(istream)
c --> e(ofstream)
a --> c(ostream) --> o(iostream)
b --> o
b --> d(ifstream)
o --> m(fstream)
图中箭头表示继承关系。从 ios 派生 istream 和 ostream 时,均使用了虚继承
istream 类提供了流的大部分输入操作,对系统预定义的所有输入流重载提取运算符 >>
ostream 类对系统预定义的所有输出流重载插入运算符 <<
C++ 的 iostream 类库提供了数百种 I/O 功能。iostream 类库接口部分包含在几个头文件中:
-
iostream:包含操作所有输入/输出流所需的基本信息。大多数 C++ 程序都应该包含该文件。
该文件有 4 个标准流对象,提供了格式化和无格式化的 I/O 功能
-
iomanip:包含格式化 I/O 的带参数流操作符,可用于指定数据输入/输出的格式
-
fstream:包含处理文件的有关信息,提供建立文件、读/写文件的各种操作接口
8.1 标准流对象
C++ 在头文件 iostream 中定义了 4 个标准流对象:cin(标准输入流)、cout(标准输出流)、cerr(非缓冲错误输出流)、clog(缓冲错误输出流)
-
cin:与标准输入设备(键盘)相关联,用于读取数据,可以被重定向
-
cout:与标准输出设备(屏幕)相关联,用于输出数据,可以被重定向
-
cerr:与标准错误信息输出设备(屏幕)相关联(非缓冲),用于输出出错信息,不能重定向
-
clog:与标准错误信息输出设备(缓冲)相关联,用于输出出错信息,不能重定向
其与 cerr 的区别是 cerr 不需要缓存区,直接向显示器输出信息。
程序中可以直接对上述 4 个预定义的标准流进行操作,而无需事先进行 “打开” 操作,也无需在事后进行 “关闭”。系统已经预先为每个程序都隐含了对他们的打开和关闭操作。
#错误状态字
C++ 在 ios 类中定义了相关的错误状态字来反映流的状态。
状态字的每一位对应流的一种错误状态,用一个对应的标识常量标记
标识常量 | 值 | 含义 |
---|---|---|
goodbit | 0x00 | 流状态正常 |
eofbit | 0x01 | 文件结束符 |
failbit | 0x02 | I/O 操作失败,数据未丢失,可以恢复 |
badbit | 0x04 | 非法操作,数据丢失,无法恢复 |
#常用函数:
-
FILE* freopen(const char* path, const char* mode, FILE* stream)
将 stream 按照 mode 指定的模式重定向到 path 指向的文件。mode 可以是 w(写)或 r(读)
重定向发生错误时,将关闭原本的 stream,并返回 NULL
-
int eof() const
返回 eofbit 的值。当文本文件结束时,在输入流中会自动设置 eofbit
抵达结尾时,该值为 1,否则为 0
在标准输入流 cin 中,按下 ctrl + Z 组合键表示输入流的结束
-
int fail() const
返回 failbit 状态,以判断流操作是否失败
-
int good() const
int operator void*()
返回 goodbit 状态,以判断流操作是否正常。
当 eofbit、failbit、badbit 都未被置位时,返回 1,否则返回 0
-
int bad() const
int operator void!()
返回 badbit 状态,以判断流操作是否失败
当 eofbit、failbit、badbit 都未被置位时,返回 0,否则返回 1
-
int rdstate() const
返回流的当前状态
-
void clean(int nState = 0)
更改当前错误状态字
8.2 控制 I/O 格式
不同的应用会对数据输入/输出的格式提出不同的要求。可以在使用 cout 输出时控制格式。
8.2.1 流操作符
为方便使用,C++ 中对所有的基本数据类型均设置了默认的数据输入/数据输出格式。
#默认的输出/输入格式:
I/O 的数据类型 | 默认输入格式 | 默认输出格式 |
---|---|---|
short、int、long | 与整型常数相同 | 一般整数形式,负数前带有负号 |
float、double、long double | 与浮点数相同 | 浮点或指数格式。取决于哪个较短 |
char | 第一个非空白字符 | 单个字符(无引号) |
char* | 从第一个空白字符起,到下一个空白字符结束 | 字符序列(无引号) |
void* | 无前缀的十六进制数 | 无前缀的十六进制数 |
Bool | true 或 1 识别为 true false 或 0 识别为 false |
1 或 0 |
C++ 在 iostream 中提供了一些常用的无参数的流操作符(也称格式控制符)
流操作符可以直接用在 >>
或 <<
后,使用便捷。
#常用的格式控制符:
使用以下格式控制符时要包含头文件 iostream
流操作符 | 作用 | 输入/输出 |
---|---|---|
endl | 输出一个换行符,并清空流 | O |
ends | 输出字符串结束,并清空流 | O |
flush | 清空流缓冲区 | O |
dec | 以十进制形式输入/输出整数(默认设置) | I/O |
hex | 以十六进制形式输入/输出整数 | I/O |
ost | 以八进制形式输入/输出整数 | I/O |
ws | 提取空白字符 | O |
#常用的格式控制流操作符:
使用以下格式控制符时要包含头文件 iomanip
流操作符 | 作用 |
---|---|
fixed | 以普通小数形式输出浮点数 |
scientific | 以科学计数法形式输出浮点数 |
left | 左对齐。即宽度不足时将字符添加到右边 |
right | 右对齐。即宽度不足时将字符添加到在边(默认) |
setbase(int b) | 设置输出时的进制。b 可以是 8、10、16 |
setw(int w) | 仅一次有效:设置输出宽度为 w 字符,或输入时读入 w 个字符。 |
setfill(int c) | 输出指定宽度时,不足宽度的场合以 c 字符填充(默认以空格填充) |
setprecision(int n) | 设置输出浮点数的精度为 n。 非 fixed、scientific 方式的场合,n 为有效数字最多的位数。超过时四舍五入或变为科学计数法并保留最多 n 位 fixed、scientific 方式的场合,n 是小数点后保留的位数 |
setiosflags(fmtflags f) | 通用操作符。将格式标志 f 对应的格式标志位置置为 1 |
resetiosflags(fmtflags f) | 通用操作符。将格式标志 f 对应的格式标志位置置为 0(清除) |
boolapha | 把 true 和 false 输出为字符串 |
noboolapha | 把 true 和 false 输出为 1 和 0(默认) |
showbase | 输出表示数值进制的前缀 |
noshowbase | 不输出表示数值进制的前缀(默认) |
showpoint | 总是输出小数点 |
noshowpoint | 小数存在时才输出小数点(默认) |
showpos | 在非负值中显示 + |
noshowpos | 在非负值中不显示 +(默认) |
skipws | 输入时跳过空白字符(默认) |
noskipws | 输入时不跳过空白字符(默认) |
uppercase | 十六进制数中使用 A ~ E 若输出前缀,则前缀输出 0X、0B 科学计数法中输出 E |
nouppercase | 十六进制数中使用 a ~ e 若输出前缀,则前缀输出 0x、0b 科学计数法中输出 e(默认) |
internal | 数值的符号(正负号)在指定宽度内左对齐,数值右对齐,中间由填充字符填充 |
以下是一个使用示范:
#include<iostream>
#include<iomanip>
using namespace std;
int main() {
cout << uppercase << showbase << setbase(8);
cout << 100 << endl; // 输出 0X64
}
8.2.2 标志字
为满足不同用户对数据输入/输出 格式的要求,C++ 提供了通过 setiosflags() 设置标志字进行格式控制的方式。函数的参数为流的格式标志位
标志字是一个 long 型的数据,由若干系统定义的格式控制标志位组合而成
#常见标志
标志常量名 | 值 | 含义 | 输入/输出 |
---|---|---|---|
ios::skipws | 0x0001 | 跳过输入中的空白 | I |
ios::left | 0x0002 | 按输出域左对齐,用填充字符填充右边 | O |
ios::right | 0x0004 | 按输出域右对齐,用填充字符填充左边(默认) | O |
ios::internal | 0x0008 | 在符号位或基数指示符后填入字符 | O |
ios::dec | 0x0010 | 转换为十进制基数格式(默认) | I/O |
ios::oct | 0x0020 | 转换为八进制基数格式 | I/O |
ios::hex | 0x0040 | 转换为十六进制基数格式 | I/O |
ios::showbase | 0x0080 | 在输出中显示基数指示符 | O |
ios::showpoint | 0x0100 | 输出浮点数时必须带小数点和尾部的 0 | O |
ios::uppercase | 0x0200 | 以大写字母表示十六进制数,科学计数法使用 E | O |
ios::showpos | 0x0400 | 正数前加 + 符号 | O |
ios::scientific | 0x0800 | 以科学计数法表示浮点数 | O |
ios::fixed | 0x1000 | 定点形式表示浮点数 | O |
ios::unitbuf | 0x2000 | 插入操作后立即刷新流 | O |
当要设置多个标志时,使用 | 运算符连接
流格式标志的每一位表示一种格式,标志位间有相互制约关系。设置了某个标志,又要设置与其矛盾的标志时,应使用函数 resetiosflags() 清除原先的标志
为便于清除同类互斥位,ios 定义了几个公有静态符号常量:
ios::basefield | 值为 dec|oct|hex |
ios::adjustifield | 值为 left|right|internal |
ios::floatfield | 值为 scientific|fixed |
8.3 cin/cout 常用函数
#cin
-
long flags(long lFlags)
:用传入关键字替换原先标志字。返回那个原来的标志字long flags()
:仅返回那个原来的标志字long setf(long lFlags)
:用传入关键字置位原先标志字。返回那个原来的标志字long setf(long lFlags, long lMask)
:将关键字指定标志位清零,再置位关键字long unsetf(long lMask)
:将关键字指定标志位清零这些函数和流操作符 setiosflags、resetiosflags 作用相同
-
int width(int nw)
:设置输出宽度,返回旧输出宽度该函数没有持续性。输出一项数据后输出宽度恢复默认值
int width() const
:返回输出宽度这些函数和流操作符 setw 作用相同
-
char fill(char cFill)
:设置填充字符,返回旧填充字符char fill() const
:返回填充字符这些函数和流操作符 setfill 作用相同
-
int percision(int np)
:设置数据显示精度,返回旧显示精度int percision() const
:返回显示精度这些函数和流操作符 setpercision 作用相同
ostream 中还有一些输出流通用的成员函数:
ostream& put(char)
:向流中插入字符ostream& write(const char* pch, int nCount)
:向流中插入 pch 指向的长度为 nCount 的序列
#cout
-
int get()
:从输入流中读取一个字符。遇到结束符时,返回值是系统常量 EOF
-
istream& getline(char* buf, int bufSize)
:从输入流读取字符到缓冲区 buf读取到
\n
为止。一次最多读取 bufSize - 1 个字符。函数会在数据结尾自动添加\0
istream& getline(char* buf, int bufSize, char delim)
:从输入流读取字符到缓冲区 buf读取到 delim 为止。一次最多读取 bufSize - 1 个字符。函数会在数据结尾自动添加
\0
-
bool eof()
:判断是否输入流已结束 -
istream& ignore(int n = 1, int delim = EOF)
:直到遇到 delim 前,跳过流中的 n 个字符 -
int peek()
:返回流中的当前字符,但并不取出该字符
8.4 文件操作
文本类型分为两种:
- 文本文件:以文本的 ASCII码 形式储存在计算机中
- 二进制文件:以文本的 二进制 形式储存在计算机中
操作文件的三大类(需包含头文件 fstream):
- ofstream:写操作
- ifstream:读操作
- fstream:读写操作
在程序中,要使用一个文件,必须包含 3 个步骤:打开文件、操作文件、关闭文件
8.4.1 打开和关闭文件
对文件进行读写操作前,要先打开文件。打开文件有两个目的:
-
建立关联。通过指定文件名,建立起文件与文件流对象的关联。以后进行操作时,可以通过与其相关的流对象来进行
-
说明文件的使用方式和文件格式。
使用方式有:只读、只写、既读又写、在末尾追加
文件格式是:文本方式、二进制方式
#打开文件的方式
-
先建立流对象,再调用 open 函数
ofstream ofs; // 流类名 对象名; ofs.open("D:\1.txt", ios::out | ios::app); // 对象名.open(文件名, 模式);
流类:ifstream(读)、ofstream(写)、fstream(读/写)
文件名:用字符串标识的外部文件。可以是完整路径,也可以是相对路径
模式:是类 ios 中定义的打开方式常量,用来表示文件打开方式
-
使用流类自带的构造函数,在构造流对象时即连接外部文件
ofstream ofs("D:\1.txt", ios::out | ios::app);
可以用文件名是否为 true 以判断是否打开成功
cout << (ofs ? "打开成功" : "打开失败") << endl;
#文件打开模式标记
模式标记 | 适用对象 | 作用 |
---|---|---|
ios::in | ifstream fstream |
以读方式打开文件。 如果文件不存在,则打开出错 |
ios::out | ofstream fstream |
以写方式打开文件。 如果文件不存在,则创建该文件。否则在打开时清除原先内容 |
ios::app | ofstream | 以追加方式打开文件,用于在文件尾部添加数据。 如果文件不存在,则新建该文件 |
ios::ate | ofstream | 打开一个已有文件,文件指针指向末尾 如果文件不存在,则打开出错 |
ios::trunc | ofstream | 删除文件现有内容。单独使用时等同于 ios::out |
ios::binary | ofstream ifstream fstream |
以二进制方式打开文件 若不指定此模式,则以默认的文本方式打开文件 |
标记可以组合使用。组合时,使用 |
符号分隔。如:
ios::in | ios::out
:此时既可以读也可以写,且打开文件时原有内容保持不变
#关闭文件
发出关闭文件指令后,系统会将缓存区数据完整写入文件,并添加文件结束标记,切断流对象与外部文件的连接。
使用 fstream 的成员函数 close 关闭文件
ofstream ofs("D:\1.txt", ios::out | ios::app);
...
ofs.close(); // 关闭了文件
8.4.2 读写文件
每个文件都有一个结束标识。对于文本文件来说,C++ 在 iostream 中定义了一个标识文件结束的标识常量 EOF,其值为 0x1A
的字符。关闭文件流时,该字符被自动加入文件尾部。
在键盘操作时,按下 ctrl + Z 组合键即可在标准输入流 cin 中输入文件结束符
#写文件
ofstream ofs; // [2] 创建流对象
ofs.open("D:/1.txt"); // [3] 打开文件
ofs << "OFS"; // [4] 写入数据
ofs.close(); // [5] 关闭文件
-
包含头文件:
#include<fstream>
-
创建流对象:
ofstream ofs;
-
打开文件:
ofs.open("文件路径", 打开方式);
-
写数据:
ofs << "写入的数据";
也能使用
put(char c)
一次只放入一个字节 -
关闭文件:
ofs.close();
#读文件
ifstream ifs; // [2] 创建流对象
ifs.open("D:/1.txt", ios::in); // [3] 打开文件
if (!ifs.is_open()) return;
char cs[1024]; // [4] 读取文件
while (ifs >> cs) cout << cs << endl;
ifs.close(); // [5] 关闭文件
-
打开文件
其中
ifs.is_open()
用于判断是否打开文件。成功的场合返回 true判断文件是否为空的方法:
char c; ifs >> c; // 读取一个字符 if (ifs.eof()) cout << "文件为空" << endl; // 遇到标识符 EOF 则文件为空
-
读取文件。读取文件有 4 种方法:
-
按数组长度读取至数组
char cs[1024]; while (ifs >> cs) cout << cs << endl;
其中
ifs >> cs
如果读取到文件末尾,则返回 false -
按行读取至数组:
char cs[1024]; while (ifs.getline(cs, sizeof(cs))) cout << cs << endl;
其中
ifs.getline(char*, int)
第一个形参是一个char*
,第二个形参是最大长度。该方法会把读取到的字符写入char*
-
按行读取至字符串:
string str; while (getline(ifs, str)) cout << str << endl;
-
按字符读取(不推荐使用):
使用
get()
方法一次读取一个字节,或者get(char& c)
读取字节并让 c 引用char c; while ((c = ifs.get()) != EOF) cout << c;
-
8.4.3 二进制文件
二进制文件也被称为 类型文件。
二进制文件以基本数据类型的二进制形式存放。其数据存储格式与内存一致,且长度仅与数据类型有关。二进制数据流不会对写入读出的数据做格式转换,便于高速处理数据。
#二进制文件与文本文件的区别
-
文本文件兼容性强。但其存储信息时需要人为添加分隔符,占用空间大。读取时需要在内存、外存间进行格式转换,也不便于对数据进行随机访问 。
-
二进制文件以二进制形式存储数据,便于对数据进行随机访问,读写速度快。但兼容性差。
-
在 Windows 中,以文本方式读取文件,系统会将所有
\r\n
转换成\n
。即如果存在连续字节0x0D0A
,系统会将其转换为0x0A
同样的,写文件时,会将
\n
转换为\r\n
因此,使用文本方式打开二进制文件进行读写,其结果可能与原先存在出入。所以使用二进制方法打开文件总是更为安全。
#读写二进制文件
二进制文件打开方式要指定为 ios::binary
class Ele { // 随便写的一个类
public:
int n = 0;
};
void write_met() {
ofstream ofs("D:/1.txt", ios::out | ios::binary); //[2] 创建输出流对象
Ele e = Ele();
e.n = 100;
ofs.write((char*)&e, sizeof(e)); //[3] 写入文件
ofs.close();
}
Ele* read_met() {
ifstream ifs("D:/1.txt", ios::in | ios::binary); //[4] 创建输入流对象
if (!ifs.is_open()) return NULL;
Ele e; //[5] 读取文件
ifs.read((char*)&e, sizeof(e));
return &e;
}
-
写入文件
ofs.write(const char*, int)
:需求一个对象的 char* 类型的指针,因此要进行强制转型ifs.read(const char*, int)
:同理使用这些方法进行读写时,不需要额外插入分隔符。它们本身都要求指定读写长度
-
能使用
int gcount()
成员函数得到读取的字节数int num = ifs.gcount(); // 上一次读取的字节数
8.4.4 随机访问文件
如果一个文件只能进行顺序存取操作,则称为顺序文件。键盘、显示器、磁带就是典型的顺序文件设备。
能在文件任意位置进行存取操作的,称为随机访问文件。
C++ 中,每个流都有一个 流指针,其由系统控制,随着流的各种操作在字节流中移动。
文件打开后,系统会自动生成一个流指针,指向文件开头(以 ios::ate 形式打开时,指向结尾)。文件的 读/写 操作从指针位置开始。每次 读/写 操作后,指针会移动到下一个 读/写 分量的起始位置。
顺序访问方式下,流指针自动移动,无需人为改变。也能通过函数改变指针位置,实现随机访问。
istream:
-
istream& seekg(long offset, ios::seek_dir dir)
:让文件读指针向指定位置移动其中
ios::seek_dir
是 ios 中定义的一个枚举类型,值是:ios::beg
:流的开始位置。此时 offset 需是非负整数ios::cur
:表示流的当前位置。此时 offset 为负表示向前移动ios::end
:;流的结束位置。此时 offset 需是非正整数
istream& seekg(long pos)
:设置文件读指针的位置等价于
seekg(pos, ios::beg)
-
long tellg()
:返回读指针的当前位置
ostream:
-
ostream& seekp(long offset, ios::seek_dir dir)
:让文件写指针向指定位置移动ostream& seekp(long pos)
:设置文件写指针的位置 -
long tellp()
:返回写指针的当前位置
在 fstream 中,读指针与写指针是同一指针。对其调用上述 读/写 函数是等价的