C++文件流操作全解析

引言

在 C 语言中,文件操作依赖于 FILE* 和一系列函数(fopenfreadfwritefprintf 等)。这种方式虽然功能齐全,但存在类型不安全、容易忘记关闭文件、错误处理繁琐等问题。

C++ 引入了**流(Stream)**的概念,将输入输出抽象为"数据流",通过统一的接口操作不同的设备(键盘、屏幕、文件、字符串等)。流体系基于面向对象设计,提供了类型安全、可扩展的 IO 操作方式。

第一部分:IO 流类层次体系

一、完整继承关系

二、常用头文件与类

头文件 包含的类 用途
<iostream> cin, cout, cerr, clog 标准控制台 IO
<fstream> ifstream, ofstream, fstream 文件 IO
<sstream> istringstream, ostringstream, stringstream 字符串 IO
<iomanip> 操纵符函数(setw, setprecision 等) 格式控制

三、四个标准流对象

cpp 复制代码
#include <iostream>
using namespace std;

// cin   --- istream 对象,关联标准输入(键盘)
// cout  --- ostream 对象,关联标准输出(屏幕)
// cerr  --- ostream 对象,关联标准错误(无缓冲)
// clog  --- ostream 对象,关联标准日志(有缓冲)

cerr 和 clog 的区别

cpp 复制代码
cerr << "错误信息" << endl;   // 立即输出(无缓冲)
clog << "日志信息" << endl;   // 缓冲后输出

第二部分:标准输入流详解

一、逐字符读取

cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    char ch;
    
    // 方式1:get() 获取一个字符(返回 istream 引用)
    cin.get(ch);
    
    // 方式2:get() 返回字符的 ASCII 值(int 类型)
    int ch2 = cin.get();
    
    cout << ch << ", " << ch2 << endl;
    return 0;
}

get() 的两种重载对比

重载形式 返回值 用途
cin.get(ch) istream& 读取字符存入 ch
cin.get() int 返回读取字符的 ASCII 值(-1 表示 EOF)

二、逐行读取

cpp 复制代码
char buf[128] = {0};

// 方式1:getline(),遇到换行符结束
cin.getline(buf, 128);

// 方式2:getline() 指定分隔符
cin.getline(buf, 128, '\n');  // 第三个参数是分隔符

// 方式3:read() 读取指定字节数
cin.read(buf, 10);

getline() 与 >> 的区别

方法 遇到空格 遇到换行 读取换行符 安全性
cin >> buf 停止 停止 不读取(留在缓冲区) ❌ 无长度限制
cin.getline(buf, n) 继续 停止 读取并丢弃 ✅ 有长度限制

三、自定义读行函数

cpp 复制代码
#include <string>

// C 风格字符串读取
long readline(char* buf, int maxSize) {
    char ch;
    int len = 0;
    while (true) {
        cin.get(ch);
        if (ch == '\n' || len >= maxSize) break;
        buf[len++] = ch;
    }
    return len;
}

// C++ string 读取
long readString(string& s) {
    char ch;
    while (true) {
        cin.get(ch);
        if (ch == '\n') break;
        s += ch;
    }
    return s.size();
}

四、缓冲区管理

cpp 复制代码
cin.ignore();       // 忽略一个字符(清空缓冲区)
cin.ignore(100);    // 忽略最多 100 个字符
cin.ignore(100, '\n'); // 忽略直到换行符(最多 100 个)
cin.clear();        // 清除错误状态标志

常见场景cin >> n 后残留的换行符需要清理

cpp 复制代码
int n;
cin >> n;
cin.ignore();  // 清除残留的 '\n'
// 然后安全读取下一行
string line;
getline(cin, line);

第三部分:标准输出流详解

一、基本输出方法

cpp 复制代码
// put() --- 输出单个字符
cout.put('A');
cout.put('\n');

// write() --- 输出指定长度的字符串
cout.write("hello", 5);  // 输出5个字符

// flush() --- 强制刷新缓冲区
cout.flush();

// endl --- 输出换行并刷新
cout << endl;  // 等价于 cout << '\n' << flush;

输出缓冲区的四种刷新时机

刷新模式 触发条件
满刷新 缓冲区满时自动刷新
行刷新 遇到换行符 \n 时刷新
程序退出 exit()return 时刷新
强制刷新 调用 flush()endl

二、格式控制 --- 进制输出

cpp 复制代码
#include <iomanip>

int n = 100;

// 使用操纵符
cout << showbase << hex << n << endl;   // 0x64 (十六进制,带前缀)
cout << oct << n << endl;              // 0144  (八进制)
cout << dec << n << endl;              // 100   (十进制)

// showbase --- 显示进制前缀(0x 或 0)
// noshowbase --- 取消进制前缀
操纵符 效果
hex 十六进制输出
oct 八进制输出
dec 十进制输出(默认)
showbase 显示进制前缀
noshowbase 取消进制前缀
uppercase 十六进制字母大写
nouppercase 十六进制字母小写(默认)

三、格式控制 --- 使用 flags

cpp 复制代码
// 获取当前标志
ios::fmtflags old_flags = cout.flags();

// 设置新标志(会覆盖旧标志)
cout.flags(ios::showbase | ios::hex);
cout << 90 << endl;  // 0x5a

// 恢复旧标志
cout.flags(old_flags);

常用格式标志

标志 含义
ios::showbase 显示进制前缀
ios::hex 十六进制
ios::oct 八进制
ios::dec 十进制
ios::left 左对齐
ios::right 右对齐
ios::fixed 固定小数位
ios::scientific 科学计数法

四、格式控制 --- 宽度和填充

cpp 复制代码
cout.width(20);        // 设置输出宽度为 20(只影响下一个输出)
cout.fill('*');        // 设置填充字符为 '*'
cout << left;          // 左对齐
cout << "hi,disen!" << endl;
// 输出:hi,disen!***********

// 使用操纵符
cout << setw(20) << setfill('*') << left << "hi,disen!" << endl;
方法/操纵符 作用 生效范围
width(n) / setw(n) 设置输出宽度 只影响下一个输出
fill(c) / setfill(c) 设置填充字符 持久生效
left 左对齐 持久生效
right 右对齐 持久生效

五、格式控制 --- 浮点数精度

cpp 复制代码
#include <iomanip>

double d = 1.2345678;

// 设置精度
cout << setprecision(3) << d << endl;  // 1.23
cout << setprecision(1) << 2.459 << endl;  // 2

// 固定小数位模式
cout << fixed << setprecision(2) << 1.345678 << endl;  // 1.35

// 科学计数法模式
cout << scientific << 123.456 << endl;  // 1.234560e+02

// 默认模式
cout << defaultfloat << 99.2389 << endl;
操纵符 精度含义
defaultfloat 有效数字位数(默认)
fixed 小数点后位数
scientific 小数点后位数(科学计数法)

第四部分:文件流详解

一、文件打开模式

cpp 复制代码
#include <fstream>

// 定义在 ios 中
ios::in        // 读模式(ifstream 默认)
ios::out       // 写模式(ofstream 默认)
ios::app       // 追加模式(写入到文件末尾)
ios::ate       // 打开时定位到文件末尾
ios::trunc     // 打开时清空文件内容
ios::binary    // 二进制模式

组合使用

cpp 复制代码
// 读写模式,二进制
fstream fs("data.dat", ios::in | ios::out | ios::binary);

// 写模式,追加
ofstream fs("log.txt", ios::out | ios::app);

二、文件写入

cpp 复制代码
#include <fstream>
#include <cstring>

int main() {
    ofstream fs("a.txt", ios::out);
    if (!fs.good()) {
        cout << "打开文件失败" << endl;
        return -1;
    }
    
    char line[128] = {0};
    while (true) {
        cin.getline(line, 128);
        if (strlen(line) == 0) break;
        
        fs.write(line, strlen(line));
        fs.write("\n", 1);
    }
    
    fs.close();
    return 0;
}

三、文件读取

cpp 复制代码
#include <fstream>
#include <string>

int main() {
    ifstream fs("a.txt");  // 默认 ios::in
    if (!fs.good()) return -1;
    
    // 方式1:逐行读取
    string line;
    while (getline(fs, line)) {
        cout << line << endl;
    }
    
    // 方式2:逐词读取
    string word;
    while (fs >> word) {
        cout << word << endl;
    }
    
    fs.close();
    return 0;
}

注意 :代码中 while (!fs.eof()) 存在一个常见陷阱:

cpp 复制代码
// ❌ 有问题的写法
while (!fs.eof()) {
    fs.getline(buf, 128);
    cout << buf << endl;  // 可能输出两次最后一行
}

// ✓ 正确的写法(将读取操作放在循环条件中)
while (fs.getline(buf, 128)) {
    cout << buf << endl;
}

原理eof() 只在尝试读取失败后才变为 true,不是"预知"文件结束。

四、二进制文件读写

cpp 复制代码
struct Person {
    int pid;
    char name[32];
    
    void hi() {
        cout << "pid: " << pid << ", name: " << name << endl;
    }
};

int main() {
    Person p1{1001, "Lucy"}, p2{1002, "Disen"};
    
    // 二进制写入
    fstream fs("b.dat", ios::out | ios::binary);
    if (!fs.good()) return -1;
    fs.write(reinterpret_cast<char*>(&p1), sizeof(Person));
    fs.write(reinterpret_cast<char*>(&p2), sizeof(Person));
    fs.close();
    
    // 二进制读取 --- 先获取文件大小
    fstream ifs("b.dat", ios::in | ios::binary);
    if (!ifs.good()) return -1;
    
    ifs.seekg(0, ios::end);  // 移到文件末尾
    auto len = ifs.tellg();  // 获取当前位置(即文件大小)
    int n = len / sizeof(Person);
    cout << "Person 个数: " << n << endl;
    
    ifs.seekg(0, ios::beg);  // 移回文件开始
    
    for (int i = 0; i < n; i++) {
        Person p;
        ifs.read(reinterpret_cast<char*>(&p), sizeof(Person));
        p.hi();
    }
    
    ifs.close();
    return 0;
}

五、文件流状态检查

cpp 复制代码
ifstream fs("test.txt");

// 方法1:good()
if (fs.good()) {
    // 文件打开成功,且没有错误
}

// 方法2:is_open()
if (fs.is_open()) {
    // 文件已成功打开
}

// 方法3:直接用作布尔值
if (!fs) {
    // 文件打开失败
}
状态函数 含义
good() 流状态正常,无任何错误
eof() 到达文件末尾
fail() 操作失败(可恢复)
bad() 严重错误(不可恢复)
is_open() 文件是否打开

六、文件指针定位

cpp 复制代码
ifstream fs("data.txt");

// seekg() --- 移动读指针
fs.seekg(0, ios::beg);   // 移到开头
fs.seekg(0, ios::end);   // 移到末尾
fs.seekg(10, ios::cur);  // 从当前位置后移10字节

// tellg() --- 获取读指针位置
auto pos = fs.tellg();

// seekp() --- 移动写指针(ofstream)
// tellp() --- 获取写指针位置
定位标志 含义
ios::beg 相对于文件开头
ios::cur 相对于当前位置
ios::end 相对于文件末尾

第五部分:C 与 C++ IO 对照表

操作 C 语言 C++ 流
打开文件 fopen("a.txt", "r") ifstream fs("a.txt")
关闭文件 fclose(fp) fs.close() 或析构自动
读字符 fgetc(fp) fs.get(ch)
读一行 fgets(buf, n, fp) fs.getline(buf, n)
写字符 fputc(ch, fp) fs.put(ch)
写一行 fputs(str, fp) fs << str
二进制读 fread(buf, sz, n, fp) fs.read(buf, n)
二进制写 fwrite(buf, sz, n, fp) fs.write(buf, n)
文件指针 fseek(fp, 0, SEEK_END) fs.seekg(0, ios::end)
获取位置 ftell(fp) fs.tellg()
错误检查 检查返回值 fs.good() / !fs
格式化输出 fprintf(fp, "%x", n) fs << hex << n

总结

一、IO 流体系核心类

二、文件操作通用流程

  1. 创建流对象

ifstream fs("filename", ios::in);

  1. 检查是否打开成功

if (!fs.good()) { /* 错误处理 */ }

  1. 读取/写入操作

fs >> data; 或 fs.read(buf, size);

  1. 关闭文件

fs.close();

// 或者依赖析构函数自动关闭

三、关键记忆点

要点 说明
cin.ignore() 清除缓冲区残留
cout.width(n) 只影响下一个输出
cout.fill(c) 持久生效
while (getline(fs, line)) 正确的逐行读取方式
reinterpret_cast<char*>(&obj) 二进制读写结构体时的类型转换
fs.seekg(0, ios::end) 计算文件大小
RAII 特性 流对象析构时自动关闭文件

C++ IO 流体系是一个设计精巧的面向对象框架,它将控制台、文件、字符串等不同设备的输入输出统一到一个继承层次中。理解流体系的关键在于:

  1. 继承层次istream / ostream 是所有输入输出流的基类

  2. 格式控制:通过操纵符(manipulator)控制输出格式

  3. RAII 机制:流对象析构时自动关闭文件,避免资源泄漏

  4. 状态检查 :始终检查 good() 或直接使用流对象的布尔值判断

相关推荐
Forget_85501 小时前
RHEL——Kubernetes容器编排平台(二)
java·开发语言
Achou.Wang1 小时前
go语言中使用等待组(waitgroups)和内存屏障(barriers)进行同步
开发语言·后端·golang
MATLAB代码顾问1 小时前
【智能优化】鹈鹕优化算法(POA)原理与Python实现
开发语言·python·算法
lsx2024061 小时前
C 标准库 - `<stdio.h>`
开发语言
得闲喝茶1 小时前
JavaScript在数据处理的应用
开发语言·前端·javascript·经验分享·笔记
嵌入式×边缘AI:打怪升级日志1 小时前
转换模块(十二):实现 RGB 转 RGB + 项目整合与上机实验
开发语言·ios·swift
研究点啥好呢1 小时前
凯捷 自动化测试(Java+Selenium)面试题精选:10道高频考题+答案解析
java·开发语言·python·selenium·测试工具·求职招聘
会周易的程序员2 小时前
aiDgeScanner架构与实现
c++·ide·物联网·架构·node.js·aiot
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之字符串 --【字符串排序】:生日
c++·字符串·csp·高频考点·信奥赛·生日·字符串排序