18 IO文件操作
I/O 的基本概念
流与缓冲区
流(Stream) 是一种对连接的抽象。C++ 程序把输入和输出看作字节流:
- 输入时,程序从流中抽取字节;输出时,程序将字节插入到输出流中
- 流充当程序和流源/流目标之间的桥梁,使得程序可以以相同的方式对待键盘输入和文件输入
- 输入流需要两个连接:文件端连接提供流的来源,程序端连接转储流出的数据
缓冲区(Buffer) 是用作中介的内存块,用于更高效地处理输入输出:
- 磁盘等设备以块(如 512 字节)为单位传输数据,而程序通常逐字节读取
- 缓冲区帮助匹配这两种不同的传输速率:磁盘一次性读取大量数据到缓冲区,程序再从缓冲区逐字节读取
- 输入时,数据先进入缓冲区,按回车后才流入程序;输出时,数据先进入缓冲区,缓冲区满或手动刷新(
flush)后才流出 - 程序非正常结束时,缓冲区中未刷新的数据可能丢失
三个标准流
| 流 | 说明 | 缓冲 |
|---|---|---|
stdin(标准输入) |
默认来自键盘 | 有缓冲 |
stdout(标准输出) |
默认流向屏幕 | 有缓冲 |
stderr(标准错误) |
专门输出错误信息 | 无缓冲,立即输出 |
错误流无缓冲是为了避免程序非正常结束时错误信息丢失。
C 与 C++ 的 I/O 特点
C 语言的 I/O
C 语言没有输入输出语句,所有 I/O 功能通过 ANSI C 标准库函数实现,基于 FILE 结构体:
fopen/fclose:打开 / 关闭文件fgetc/fputc:读 / 写一个字符fprintf/fscanf:格式化写 / 读fread/fwrite:二进制读 / 写feof/ferror:文件结束 / 错误检测fseek:随机定位
C++ 的 I/O
C++ 保留了对 C 中 I/O 函数的支持,同时提供了一套基于类层次的 I/O 系统,运用了类、派生类、函数重载、模板和多重继承等特性。一切文件操作函数都是定义在类里的成员函数。
C++ I/O 类层次:
| 类 | 说明 |
|---|---|
streambuf |
为缓冲区提供内存,管理缓冲区的填充、访问、刷新 |
ios_base |
表示流的一般特征(是否可读、文本流/二进制流等) |
ios |
基于 ios_base,包含指向 streambuf 对象的指针 |
istream |
派生自 ios,提供输入方法 |
ostream |
派生自 ios,提供输出方法 |
iostream |
基于多继承自 istream 和 ostream,同时具备输入输出方法 |
cin 是 istream 类的实例,cout 是 ostream 类的实例,通过重载的 >> 和 << 运算符进行输入输出。
C++ 文件 I/O 的方法
文件流类
| 类 | 说明 | 继承自 |
|---|---|---|
ofstream |
写操作(输出)的文件类 | ostream |
ifstream |
读操作(输入)的文件类 | istream |
fstream |
可同时读写的文件类 | iostream |
用法与 cin、cout 完全一样,只是流的目标从键盘/屏幕变为文件。
基本文件操作
文件流对象可通过构造函数或 open() 成员函数关联文件:
#include <fstream>
using namespace std;
int main() {
// 方式1:构造时打开
ifstream source("source-file.txt");
ofstream destination("dest-file.txt");
// 方式2:先定义,再 open
// ifstream source;
// source.open("source-file.txt");
int x;
source >> x; // 从文件读取,类似 cin
source.close(); // 关闭文件,类似 fclose
destination << x; // 写入文件,类似 cout
// destination 析构时会自动调用 close()
return 0;
}
若未显式调用
close(),文件流对象析构时会自动关闭文件。
文本文件读写示例
读取文件 source.txt(内容为 13 3.14 9),计算前两者之积写入 result.txt:
#include <fstream>
using namespace std;
int main() {
int someInt;
float someFloat;
char someChar;
ifstream inFile;
ofstream outFile;
inFile.open("Source.txt");
outFile.open("Result.txt");
inFile >> someInt >> someFloat >> someChar; // 13, 3.14, '9'
outFile << "The answer is: " << someInt * someFloat << endl;
inFile.close();
outFile.close();
return 0;
}
// Result.txt 内容:The answer is: 40.82
字符读取:>> vs get()
使用 >> 读取字符会跳过空白字符(空格、换行、制表符),导致文件复制时丢失空格。应使用 get() 成员函数逐字符读取(包含空白字符):
// 错误:>> 会跳过空白字符
inFile >> c;
while (inFile) {
outFile << c;
inFile >> c;
}
// 正确:get() 读取所有字符
inFile.get(c);
while (inFile) {
outFile << c;
inFile.get(c);
}
文件打开方式
open() 的第二个参数指定打开方式,对应 C 语言 fopen 的模式字符串:
| 打开方式 | 含义 |
|---|---|
ios::in |
以输入(读)方式打开 |
ios::out |
以输出(写)方式打开 |
ios::ate |
初始位置为文件尾 |
ios::app |
所有输出附加在文件末尾(追加) |
ios::trunc |
如果文件已存在,先删除该文件 |
ios::binary |
以二进制方式打开 |
打开方式可用按位或 | 组合(实际是位标志,如 0x10):
ofstream file;
file.open("example.bin", ios::out | ios::app | ios::binary);
// 等价于:
ofstream file("example.bin", ios::out | ios::app | ios::binary);
各文件流类的默认打开方式:
| 类 | 默认方式 |
|---|---|
ofstream |
ios::out | ios::trunc |
ifstream |
ios::in |
fstream |
ios::in | ios::out |
可通过成员函数
is_open()检查文件是否已成功打开,返回bool值。
二进制读写
二进制形式将数据在内存中的原样存储到文件中,不进行 ASCII 转换。使用 read() 和 write() 成员函数:
// 函数原型
istream& read(char* buffer, streamsize size);
ostream& write(const char* buffer, streamsize size);
int a[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
int b[10];
// 写入二进制文件
ofstream outFile("1.dat", ios::binary);
for (int i = 0; i < 10; i++) {
outFile.write((char*)&a[i], sizeof(a[i]));
}
outFile.close();
// 读取二进制文件
ifstream inFile("1.dat", ios::binary);
for (int i = 0; i < 10; i++) {
inFile.read((char*)&b[i], sizeof(b[i]));
}
inFile.close();
read/write的第一个参数必须是char*,因此需要用(char*)强制转换其他类型指针。
文件定位
C++ I/O 系统管理两个与文件关联的指针:
- 读指针:说明输入操作在文件中的位置
- 写指针:说明下次写操作的位置
| 成员函数 | 说明 |
|---|---|
seekg(offset, origin) |
设置读指针位置 |
seekp(offset, origin) |
设置写指针位置 |
tellg() |
返回当前读指针位置 |
tellp() |
返回当前写指针位置 |
origin(基准位置)取值:
| 值 | 含义 |
|---|---|
ios::beg |
文件开头 |
ios::cur |
当前位置 |
ios::end |
文件结尾 |
file1.seekg(1234, ios::cur); // 读指针从当前位置向后移 1234 字节
file2.seekp(1234, ios::beg); // 写指针从文件开头向后移 1234 字节
seekg中的g代表 "get"(读取),seekp中的p代表 "put"(写入)。
动态文件名
可通过 string 接收用户输入的文件名,再用 c_str() 转换为 C 风格字符串传给 open():
string FileName;
cout << "Please enter the output file name";
cin >> FileName;
outFile.open(FileName.c_str(), ios::binary);
C++20 新特性:std::format
C 语言中可用 printf 格式化输出,C++ 中用 cout 拼接较为繁琐:
// C
printf("[ %lf, %lf, %lf ]\n", a[0], a[1], a[2]);
// C++ 传统方式
std::cout << "[ " << a[0] << ", " << a[1] << ", " << a[2] << " ]";
// C++20 std::format(类似 Python 的格式化)
std::cout << std::format("[ {}, {}, {} ]\n", a[0], a[1], a[2]);
std::format 比 cout 更易读,比 printf 更易写,返回 std::string 类型。