【C++】ifstream、ofstream、fstream的基础使用

目录

一、基础概念与文件打开模式

二、打开、关闭与状态检查

[1. 打开文件](#1. 打开文件)

[2. 检查状态 ------ 重要](#2. 检查状态 —— 重要)

[3. 关闭文件](#3. 关闭文件)

[4. 其他通用接口](#4. 其他通用接口)

[① flush() ------ 强制刷新缓冲区](#① flush() —— 强制刷新缓冲区)

[② swap() ------ 交换两个文件流的状态(C++11)](#② swap() —— 交换两个文件流的状态(C++11))

[③ rdbuf() ------ 获取底层缓冲区指针](#③ rdbuf() —— 获取底层缓冲区指针)

三、ifstream (输入文件流)

[1. 核心读取接口](#1. 核心读取接口)

[① 流提取运算符 >>](#① 流提取运算符 >>)

[② getline() ------ 读取整行](#② getline() —— 读取整行)

[③ get() ------ 读取单个字符](#③ get() —— 读取单个字符)

[④ read() ------ 二进制块读取](#④ read() —— 二进制块读取)

[⑤ gcount() ------ 获取上次读取的字节数](#⑤ gcount() —— 获取上次读取的字节数)

[⑥ ignore() ------ 跳过指定字符](#⑥ ignore() —— 跳过指定字符)

[⑦ peek() ------ 查看下一个字符](#⑦ peek() —— 查看下一个字符)

[⑧ putback() / unget() ------ 回退字符](#⑧ putback() / unget() —— 回退字符)

四、ofstream (输出文件流)

[1. 核心写入接口](#1. 核心写入接口)

[① 流插入运算符 <<](#① 流插入运算符 <<)

[② put() ------ 写入单个字符](#② put() —— 写入单个字符)

[③ write() ------ 二进制块写入](#③ write() —— 二进制块写入)

五、fstream (文件流)

[1. fstream 的读接口](#1. fstream 的读接口)

[2. fstream 的写接口](#2. fstream 的写接口)

[3. fstream 的优势:随机访问](#3. fstream 的优势:随机访问)

六、综合示例

建议


在 C++ 编程中,文件操作是一项核心技能。标准库 <fstream> 为我们提供了一套强大易用的文件流类:ifstream(读文件)、ofstream(写文件)和 fstream(读写文件)。


一、基础概念与文件打开模式

在介绍具体类之前,我们需要先了解 文件打开模式 , 这些模式定义在 std::ios 命名空间中,控制文件被打开的方式。
这里的in是读,out是写,个人觉得是有些反直觉的,所以我是这样理解的:

  • 以自身为主体
  • in就是向我输入,即我读取文件
  • out就是我输出,即我向文件写入
模式标志 含义 适用类 关键说明
ios::in 模式打开 ifstream, fstream 文件必须存在,否则打开失败。不能与 ios::trunc 同时使用
ios::out 模式打开 ofstream, fstream - ofstream:默认隐含 ios::trunc,文件不存在则创建,存在则清空。- fstream不隐含 ios::trunc,文件不存在则创建,存在则保留原内容。
ios::app 强制追加模式 ofstream, fstream 所有写入操作强制在文件末尾进行 ,即使通过 seekp() 移动了写指针也无效。需配合 ios::out 使用。
ios::trunc 截断清空模式 ofstream, fstream 打开文件时清空原有内容。必须配合 ios::out 使用
ios::binary 二进制模式 所有类 以字节流形式读写,不做换行符转换(Windows 下 \n\r\n)。随机访问必须使用此模式
ios::ate 初始置尾模式 所有类 打开后立即将读写指针定位到文件末尾,但之后仍可自由移动指针。与 ios::app 本质不同。

使用示例:

cpp 复制代码
// 正确:二进制读写,文件不存在则创建,存在则保留原内容
ios::in | ios::out | ios::binary

// 正确:文本追加写
ios::out | ios::app

// 错误:ifstream 不能用 trunc
// ifstream file("data.txt", ios::in | ios::trunc);

// 错误:trunc 必须配合 out 使用
// fstream file("data.txt", ios::in | ios::trunc);

注意:

fstream 默认模式陷阱 fstream 的默认打开模式是 ios::in | ios::out,但如果文件不存在,默认模式下打开会直接失败 ,不会自动创建文件。要让 fstream 创建不存在的文件,必须显式加上 ios::out(单独加或组合加均可)。


二、打开、关闭与状态检查

1. 打开文件

有两种方式打开文件:构造函数open() 方法。

函数原型:

cpp 复制代码
// 构造函数
explicit ifstream (const char* filename, ios_base::openmode mode = ios_base::in);
explicit ofstream (const char* filename, ios_base::openmode mode = ios_base::out);
explicit fstream  (const char* filename, ios_base::openmode mode = ios_base::in | ios_base::out);

// C++11 支持 std::string
explicit ifstream (const std::string& filename, ios_base::openmode mode = ios_base::in);
explicit ofstream (const std::string& filename, ios_base::openmode mode = ios_base::out);
explicit fstream  (const std::string& filename, ios_base::openmode mode = ios_base::in | ios_base::out);

// C++17 支持 std::filesystem::path
explicit ifstream (const std::filesystem::path& filename, ios_base::openmode mode = ios_base::in);
explicit ofstream (const std::filesystem::path& filename, ios_base::openmode mode = ios_base::out);
explicit fstream  (const std::filesystem::path& filename, ios_base::openmode mode = ios_base::in | ios_base::out);

// open() 方法(参数与构造函数完全一致)
void open(const char* filename, ios_base::openmode mode = default_mode);
void open(const std::string& filename, ios_base::openmode mode = default_mode);
void open(const std::filesystem::path& filename, ios_base::openmode mode = default_mode);

参数:

  • filename: 文件路径(支持 C 风格字符串、std::string 和 C++17 std::filesystem::path)。
  • mode: 打开模式(见上表),有默认值。

示例:

cpp 复制代码
#include <fstream>
#include <string>
#include <filesystem> // C++17

int main()
{
    // 方式 1: 构造时直接打开 (推荐,RAII 风格)
    std::ifstream inFile("data.txt"); 
    
    // 方式 2: 默认构造后调用 open()
    std::ofstream outFile;
    outFile.open("output.txt", ios::out | ios::app); // 追加模式
    
    // C++17 filesystem::path 示例
    std::filesystem::path filePath = "config.ini";
    std::fstream configFile(filePath, ios::in | ios::out); // 文件不存在会失败
    
    return 0;
}

2. 检查状态 ------ 重要

永远不要在不检查文件是否打开成功的情况下就进行读写

状态检查方法:

方法 作用 返回值 说明
is_open() 检查文件是否成功打开 bool (成功为 true) 仅检查文件句柄是否有效
operator bool() (C++11) 检查流是否处于可操作状态 bool (良好为 true) 等价于 !fail(),最推荐用于条件判断
operator!() 检查流是否处于失败状态 bool (失败为 true) 等价于 fail()
good() 检查流是否完全正常 bool eofbitfailbitbadbit 均未设置时返回 true
eof() 检查是否到达文件末尾 bool 仅当读操作尝试读取文件尾之后的数据时才会置位
fail() 检查是否发生可恢复错误 bool 如格式错误、文件不存在等,清除状态后可继续操作
bad() 检查是否发生不可恢复错误 bool 如文件系统损坏、内存不足等,流基本无法再使用
clear() 清除流的所有错误状态 void 必须在错误发生后调用,才能继续使用流
setstate(iostate state) 设置指定的错误状态 void 手动设置流的错误标志

示例(补充错误恢复):

cpp 复制代码
std::ifstream file("data.txt");

// 最推荐的检查方式 (利用 operator bool)
if (!file)
{
    std::cerr << "无法打开文件!错误码: " << file.rdstate() << std::endl;
    return 1;
}

// 错误恢复示例
int num;
file >> num;
if (file.fail())
{
    std::cerr << "读取格式错误!" << std::endl;
    file.clear(); // 清除错误状态
    file.ignore(1024, '\n'); // 跳过错误行
}

3. 关闭文件

函数原型:

cpp 复制代码
void close();

虽然文件流对象在析构时会自动关闭文件,但显式调用 close() 是个好习惯,特别是在需要立即释放文件句柄、重新打开文件或检查关闭是否成功时。

示例:

cpp 复制代码
file.close();

// 检查关闭是否成功(很少需要,但某些场景有用)
if (file.is_open())
{
    std::cerr << "文件关闭失败!" << std::endl;
}

4. 其他通用接口

flush() ------ 强制刷新缓冲区

函数原型:

cpp 复制代码
ostream& flush();

作用: 将输出缓冲区中所有未写入的数据立即写入文件。说明: 输出流默认会缓冲数据,只有当缓冲区满、程序结束或调用 close() 时才会真正写入文件。flush() 可以强制立即写入。

示例:

cpp 复制代码
std::ofstream logFile("log.txt");
logFile << "程序启动成功" << std::endl;
logFile.flush(); // 立即写入日志,确保崩溃前能记录

swap() ------ 交换两个文件流的状态(C++11)

函数原型:

cpp 复制代码
void swap (basic_fstream& other);
// 非成员函数版本
void swap (basic_fstream& lhs, basic_fstream& rhs);

作用: 交换两个文件流的内部状态(包括打开的文件句柄、缓冲区、错误状态等)。

示例:

cpp 复制代码
std::ifstream file1("a.txt");
std::ifstream file2("b.txt");
file1.swap(file2); // 现在 file1 指向 b.txt,file2 指向 a.txt

rdbuf() ------ 获取底层缓冲区指针

函数原型:

cpp 复制代码
basic_filebuf<char_type, traits_type>* rdbuf() const;

作用: 获取指向底层 std::filebuf 对象的指针,用于低级 I/O 操作或流重定向。

示例:

cpp 复制代码
// 将 cout 重定向到文件
std::ofstream file("output.txt");
std::streambuf* oldCoutBuf = std::cout.rdbuf();
std::cout.rdbuf(file.rdbuf());
std::cout << "这句话会写入文件" << std::endl;
std::cout.rdbuf(oldCoutBuf); // 恢复 cout

三、ifstream (输入文件流)

ifstream 继承自 istream,专门用于从文件中读取数据。

1. 核心读取接口

① 流提取运算符 >>

用于格式化读取(自动跳过空格、制表符、换行符等空白字符)。

示例:

cpp 复制代码
std::ifstream in("data.txt");
int a;
double b;
std::string str;

// 像使用 cin 一样使用
in >> a >> b >> str; 

getline() ------ 读取整行

这是最常用的接口之一,读取直到遇到换行符 \n

函数原型:

cpp 复制代码
// 成员函数版本(读取到字符数组)
istream& getline (char* s, streamsize n, char delim = '\n');
// 非成员函数版本(读取到 std::string,推荐)
istream& getline (istream& is, string& str, char delim = '\n');

参数:

  • s / str: 存储结果的字符数组或 std::string
  • n: 最多读取的字符数(含终止符)。
  • delim: 分隔符(默认是换行符),遇到此字符停止读取(该字符会被从流中丢弃,不会存入结果)。

返回值:

  • 返回流对象本身(*this),因此可以链式调用或用于 if / while 判断。

示例:

cpp 复制代码
std::ifstream inFile("poem.txt");
std::string line;

// 经典逐行读取循环
while (std::getline(inFile, line))
{
    std::cout << "读到: " << line << std::endl;
}

get() ------ 读取单个字符

读取流中的下一个字符,不跳过空白字符

函数原型:

cpp 复制代码
int get(); // 返回字符的 ASCII 码,若到文件尾返回 EOF
istream& get (char& c);

示例:

cpp 复制代码
char ch;
while (inFile.get(ch))
{
    // 处理字符 ch(包括空格、换行符)
}

read() ------ 二进制块读取

用于读取二进制数据块,不做任何格式转换

函数原型:

cpp 复制代码
istream& read (char* s, streamsize n);
  • s: 指向内存缓冲区的指针。
  • n: 要读取的字节数。

示例:

cpp 复制代码
std::ifstream binFile("image.png", ios::binary);
char buffer[1024];
binFile.read(buffer, 1024); // 读取 1024 个字节

gcount() ------ 获取上次读取的字节数

作用:返回最后一次非格式化读取操作(如 get, getline, read)实际读取的字符数。

示例:

cpp 复制代码
inFile.read(buffer, 100);
std::cout << "实际读取了 " << inFile.gcount() << " 个字节" << std::endl;

ignore() ------ 跳过指定字符

函数原型:

cpp 复制代码
istream& ignore (streamsize n = 1, int delim = EOF);

参数:

  • n: 最多跳过的字符数。
  • delim: 分隔符,遇到此字符停止跳过(该字符也会被丢弃)。
    作用: 跳过流中的字符,最常用于解决 >>getline() 混用的坑

注意:

>>getline() 混用 当使用 >> 读取数据后,输入缓冲区会留下一个换行符 \n。如果紧接着调用 getline(),它会读到这个换行符,返回一个空字符串。

解决方案:>> 之后调用 ignore() 跳过换行符。

示例:

cpp 复制代码
std::ifstream file("data.txt");
int id;
std::string name;

file >> id;
file.ignore(); // 跳过 >> 留下的换行符
std::getline(file, name); // 正确读取下一行的姓名

peek() ------ 查看下一个字符

函数原型:

cpp 复制代码
int peek();

作用: 返回流中的下一个字符,但不提取它 (指针不移动)。若到文件尾返回 EOF

示例:

cpp 复制代码
// 跳过所有空行
std::string line;
while (inFile)
{
    if (inFile.peek() == '\n')
    {
        inFile.ignore(); // 跳过空行
    }
    else
    {
        std::getline(inFile, line);
        // 处理非空行
    }
}

putback() / unget() ------ 回退字符

函数原型:

cpp 复制代码
istream& putback (char c); // 将指定字符放回流中
istream& unget(); // 将最后读取的字符放回流中

作用: 将字符放回输入流,下一次读取会先读到这个字符。

示例:

cpp 复制代码
char ch;
inFile.get(ch);
if (isdigit(ch))
{
    inFile.putback(ch); // 把数字放回去
    int num;
    inFile >> num; // 读取整个数字
}

四、ofstream (输出文件流)

ofstream 继承自 ostream,专门用于向文件写入数据。

1. 核心写入接口

① 流插入运算符 <<

用于格式化写入

示例:

cpp 复制代码
std::ofstream out("log.txt");
int id = 1001;
double score = 95.5;

// 像使用 cout 一样使用
out << "ID: " << id << ", Score: " << score << std::endl;

注意:endl'\n' 的区别

  • std::endl: 输出一个换行符 并强制刷新缓冲区,频繁使用会严重影响性能。
  • '\n': 仅输出一个换行符,不刷新缓冲区,性能更好。

注: 除非确实需要立即写入,否则使用 '\n' 代替 std::endl


put() ------ 写入单个字符

函数原型:

cpp 复制代码
ostream& put (char c);

示例:

cpp 复制代码
outFile.put('A');
outFile.put('\n');

write() ------ 二进制块写入

用于写入二进制数据块,不做任何格式转换

函数原型:

cpp 复制代码
ostream& write (const char* s, streamsize n);

示例:

cpp 复制代码
std::ofstream binOut("data.bin", ios::binary);
int data[] = {1, 2, 3, 4, 5};
// 将内存中的数组直接写入文件
binOut.write(reinterpret_cast<char*>(data), sizeof(data));

注意:

二进制读写的字节序问题不同平台(x86 是小端,ARM 可能是大端)的字节序不同,直接写入的二进制数据在跨平台读取时会出错。如果需要跨平台,应统一字节序(如网络字节序)。


五、fstream (文件流)

fstream 继承自 iostream,因此它同时拥有 上述 ifstreamofstream 的所有接口。它既可以读,也可以写,是最灵活的文件流类。

注意:

  • 如果只写,用 ofstream;只读,用 ifstream。只有当需要同时读写随机访问 时,才使用 fstream
  • 读写交替操作时,必须在中间插入一个 seekg()seekp()flush() 操作,否则行为未定义(C++ 标准规定)。

1. fstream 的读接口

由于 fstream 继承自 istream,它拥有 ifstream 的所有读取接口:>>getline()get()read()gcount()ignore()peek()putback()unget()

2. fstream 的写接口

由于 fstream 继承自 ostream,它拥有 ofstream 的所有写入接口:<<put()write()flush()

3. fstream 的优势:随机访问

文件流内部有两个独立的指针:读指针 (Get Pointer)写指针 (Put Pointer) ,分别控制当前的读写位置。fstream 允许我们自由移动这两个指针,实现随机读写。

注意:

文本模式下随机访问不可靠 在文本模式(默认)下,由于换行符会被转换(Windows 下 \n 转成 \r\n,占两个字节),tellg()/tellp() 返回的位置与实际字符数不一致,导致 seekg()/seekp() 定位错误。随机访问必须使用二进制模式 ios::binary

接口 作用 函数原型
读指针操作
tellg() 返回当前读指针的位置 streampos tellg();
seekg(pos) 将读指针移动到绝对位置 istream& seekg(streampos pos);
seekg(offset, dir) 从基准位置偏移指定字节 istream& seekg(streamoff offset, ios_base::seekdir dir);
写指针操作
tellp() 返回当前写指针的位置 streampos tellp();
seekp(pos) 将写指针移动到绝对位置 ostream& seekp(streampos pos);
seekp(offset, dir) 从基准位置偏移指定字节 ostream& seekp(streamoff offset, ios_base::seekdir dir);

方向参数 dir

  • ios::beg ------ 文件开头 (Beginning)
  • ios::cur ------ 当前位置 (Current)
  • ios::end ------ 文件末尾 (End)

参数:

  • pos: 绝对位置(通常由 tellg()tellp() 返回)。
  • offset: 偏移量(正数向后,负数向前)。

返回值:

  • tellg() / tellp(): 返回当前指针位置(类型为 streampos)。
  • seekg() / seekp(): 返回流对象本身,支持链式调用。

示例(二进制模式随机访问):

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

int main()
{
    // 必须使用 ios::binary 才能保证随机访问准确
    std::fstream file("random_access.bin", ios::in | ios::out | ios::trunc | ios::binary);
    
    if (!file)
    {
        std::cerr << "文件打开失败!" << std::endl;
        return 1;
    }
    
    // --- 1. 先写入一些数据 ---
    int data[] = {10, 20, 30, 40, 50};
    file.write(reinterpret_cast<char*>(data), sizeof(data));
    
    // --- 2. 随机读取第 3 个整数(索引从 0 开始)---
    file.seekg(2 * sizeof(int), ios::beg); // 移动到第 3 个整数的位置
    int value;
    file.read(reinterpret_cast<char*>(&value), sizeof(int));
    std::cout << "第 3 个整数: " << value << std::endl; // 输出 30
    
    // --- 3. 随机修改第 2 个整数为 200 ---
    file.seekp(1 * sizeof(int), ios::beg);
    int newValue = 200;
    file.write(reinterpret_cast<char*>(&newValue), sizeof(int));
    
    // --- 4. 验证修改结果 ---
    file.seekg(0, ios::beg);
    int result[5];
    file.read(reinterpret_cast<char*>(result), sizeof(result));
    std::cout << "修改后的数组: ";
    for (int i = 0; i < 5; i++)
    {
        std::cout << result[i] << " ";
    }
    // 输出: 10 200 30 40 50
    
    return 0;
}

六、综合示例

下面是一个完整的示例,展示如何使用 ofstream 写入配置,再用 ifstream 读取回来,最后用 fstream 修改中间数据。

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

using namespace std;

int main()
{
    const string filename = "config.txt";
    
    // --- 1. 写入文件 (ofstream) ---
    {
        ofstream outFile(filename);
        
        if (!outFile)
        {
            cerr << "创建文件失败!" << endl;
            return 1;
        }
        
        outFile << "Username: Admin" << endl;
        outFile << "Level: 99" << endl;
        outFile << "HP: 1000.5" << endl;
        
        cout << "数据写入成功。" << endl;
    }

    // --- 2. 读取文件 (ifstream) ---
    {
        ifstream inFile(filename);
        
        if (!inFile)
        {
            cerr << "读取文件失败!" << endl;
            return 1;
        }
        
        string key, colon;
        string name;
        int level;
        double hp;
        
        inFile >> key >> colon >> name;
        inFile >> key >> colon >> level;
        inFile >> key >> colon >> hp;
        
        cout << "\n--- 读取结果 ---" << endl;
        cout << "玩家名: " << name << endl;
        cout << "等级: " << level << endl;
        cout << "血量: " << hp << endl;
    }

    // --- 3. 修改文件 (fstream) ---
    {
        // 文本模式下修改整行,注意长度匹配
        fstream file(filename, ios::in | ios::out);
        
        if (!file)
        {
            cerr << "打开文件失败!" << endl;
            return 1;
        }
        
        string line;
        streampos levelPos = -1;
        while (getline(file, line))
        {
            if (line.find("Level: ") != string::npos)
            {
                // 计算当前行的起始位置(文本模式下仅作近似,推荐二进制模式)
                levelPos = file.tellg();
                levelPos -= (line.length() + 1); // +1 是换行符
                break;
            }
        }
        
        if (levelPos != streampos(-1))
        {
            file.seekp(levelPos);
            // 注意:新内容长度必须 >= 原内容长度,否则会残留旧内容
            file << "Level: 100" << endl;
            cout << "\n等级已修改为 100。" << endl;
        }
    }

    return 0;
}

建议

  1. 优先使用 ifstream 读、ofstream 写,仅在需要同时读写时用 fstream
  2. 永远检查文件是否打开成功
  3. 二进制读写必须加 ios::binary
  4. 随机访问必须使用二进制模式
  5. >> 之后用 ignore() 跳过换行符再用 getline()
  6. 优先使用 '\n' 代替 std::endl 提升性能

感谢阅读。本文如有错漏之处,烦请斧正。

相关推荐
皮卡蛋炒饭.2 小时前
Linux进程信号
开发语言·c++
共享家95272 小时前
C++ 日志类设计
linux·c++·后端
小辉同志2 小时前
208. 实现 Trie (前缀树)
开发语言·c++·leetcode·图论
John.Lewis2 小时前
C++加餐课-stack_queue:反向迭代器
数据结构·c++
云栖梦泽2 小时前
Linux内核与驱动:12.设备树实例分析
linux·c++·单片机
代码改善世界3 小时前
【C++初阶】stack和queue用法详解:常用接口、模拟实现与面试题(附完整代码)
开发语言·c++
承渊政道3 小时前
【递归、搜索与回溯算法】(递归问题拆解与经典模型实战大秘笈)
数据结构·c++·学习·算法·macos·dfs·bfs
少司府3 小时前
C++基础入门:类和对象(下)
开发语言·c++·类型转换·类和对象·友元
tankeven3 小时前
动态规划专题(05):区间动态规划实践(乘法游戏)
c++·算法·动态规划