【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 当 eofbit、failbit、badbit 均未设置时返回 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,因此它同时拥有 上述 ifstream 和 ofstream 的所有接口。它既可以读,也可以写,是最灵活的文件流类。

注意:

  • 如果只写,用 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 提升性能

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

相关推荐
蒸蒸yyyyzwd5 小时前
秋招学习笔记 day46
c++·八股
码匠许师傅8 小时前
【C++三方组件】cpp-httplib:一个头文件起 HTTP 服务
开发语言·c++·http
无名猿9 小时前
new/delete 与 malloc/free:为什么绝对不能混用
c++·内存管理·现代c++·踩坑记录
2601_9622186111 小时前
C++中decltype关键字的实现
开发语言·c++
繁华的地方不一定留下你的脚印12 小时前
C++ std::variant 与 std::visit:安全保存多种类型,写清每个处理分支
开发语言·c++
程序员老陆12 小时前
WIN32_LEAN_AND_MEAN:Windows 头文件里的“精简编译开关”
c++·windows
hold?fish:palm13 小时前
45 二叉树的右视图
开发语言·c++·算法
ShineWinsu13 小时前
对于Redis:Redis的认识以及分布式系统的解析
linux·数据库·c++·redis·缓存·消息队列·分布式架构
布莱克60513 小时前
在C++中,构造函数能不能是虚函数?析构函数为什么要虚?
开发语言·c++·构造函数·析构函数·虚函数
旖旎夜光14 小时前
LeetCode 137 : 只出现一次的数字 II(位运算) —— 题解
c++·笔记·学习·算法·leetcode·力控