C/C++ 文件操作详解与总结

文件操作是程序与外部世界持久化交互数据的关键。C 语言和 C++ 都提供了各自的文件操作库,C++ 的版本通常更现代、更安全。

一、C 语言文件操作 (stdio.h)

C 语言使用 FILE 结构体和一系列函数来进行文件操作。核心思想是 文件流文件指针

1. 打开文件:fopen

FILE *fopen(const char *filename, const char *mode);
  • filename: 文件路径。
  • mode: 打开模式,决定了如何操作文件。
    • "r":只读(文件必须存在)。
    • "w":只写。如果文件存在,内容被清空;如果不存在,则创建。
    • "a":追加。在文件末尾写入,如果文件不存在则创建。
    • "r+":读写(文件必须存在)。
    • "w+":读写。如果文件存在,内容被清空;如果不存在,则创建。
    • "a+":读写。从文件末尾开始读写,如果文件不存在则创建。
    • 在上述模式后加 b (如 "rb", "wb+") 表示以二进制模式打开文件。在 Windows 上,这可以防止 \n 被转换为 \r\n

返回值:成功返回 FILE* 指针,失败返回 NULL

示例

FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
    perror("Error opening file"); // 打印系统错误信息
    exit(EXIT_FAILURE);
}

2. 关闭文件:fclose

操作完成后必须关闭文件,释放资源并确保数据写入磁盘。

int fclose(FILE *stream);

返回值:成功返回 0,失败返回 EOF

3. 读写文件

字符读写
int fgetc(FILE *stream);  // 读取一个字符,返回读到的字符(转为int),遇到错误或文件结束返回EOF。
int fputc(int char, FILE *stream); // 写入一个字符,成功返回写入的字符,失败返回EOF。
字符串(行)读写
char *fgets(char *str, int n, FILE *stream); // 读取一行(最多n-1个字符),成功返回str,失败或文件结束返回NULL。
int fputs(const char *str, FILE *stream);    // 写入字符串(不自动添加换行符),成功返回非负值,失败返回EOF。
格式化读写

功能强大,类似于 printfscanf

int fprintf(FILE *stream, const char *format, ...); // 格式化输出到文件
int fscanf(FILE *stream, const char *format, ...);  // 从文件格式化输入

示例

fprintf(fp, "Name: %s, Age: %d\n", "Alice", 25);

char name[20];
int age;
fscanf(fp, "Name: %s, Age: %d", name, &age); // 注意&
数据块读写(二进制操作)

用于读写结构体等二进制数据,效率高。

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
  • ptr: 数据内存块的指针。
  • size: 每个数据元素的大小(字节),常用 sizeof 获取。
  • count: 要读写多少个这样的元素。
  • stream: 文件指针。

返回值:成功读写的元素个数(不是字节数!)。

示例(写入和读取一个结构体数组)

typedef struct {
    char name[50];
    int id;
    float score;
} Student;

Student students[2] = {{"Alice", 1, 95.5f}, {"Bob", 2, 88.0f}};

// 写入
FILE *fp = fopen("data.bin", "wb");
if (fp) {
    size_t elements_written = fwrite(students, sizeof(Student), 2, fp);
    fclose(fp);
}

// 读取
Student read_students[2];
fp = fopen("data.bin", "rb");
if (fp) {
    size_t elements_read = fread(read_students, sizeof(Student), 2, fp);
    fclose(fp);
}

4. 文件定位

每个打开的文件都有一个内部位置指针,指示当前读写的位置。

int fseek(FILE *stream, long int offset, int origin); // 移动位置指针
void rewind(FILE *stream); // 将位置指针重置到文件开头 (等价于 fseek(fp, 0, SEEK_SET))
long int ftell(FILE *stream); // 获取当前位置指针相对于文件开头的偏移量(字节数)
int feof(FILE *stream); // 检查是否到达文件末尾。注意:只有在尝试读取**失败后**,这个函数才会返回真。

fseekorigin 参数:

  • SEEK_SET:文件开头。
  • SEEK_CUR:当前位置。
  • SEEK_END:文件末尾。

示例:获取文件大小

fseek(fp, 0, SEEK_END); // 跳到文件末尾
long file_size = ftell(fp); // 获取当前位置(即文件大小)
rewind(fp); // 跳回开头准备读取

二、C++ 文件操作 (fstream)

C++ 通过 fstream, ifstream, ofstream 类来处理文件,它们继承自 iostreamcin, cout 的类),因此用法与标准 I/O 高度一致,更面向对象,更安全。

1. 文件流类

  • ofstream:用于输出(写文件)。
  • ifstream:用于输入(读文件)。
  • fstream:既可输入也可输出。

2. 打开文件

有两种方式:

  1. 构造函数中直接打开
   #include <fstream>
   std::ifstream inFile("input.txt", std::ios::in);
   std::ofstream outFile("output.txt", std::ios::out | std::ios::app);
   std::fstream ioFile("data.bin", std::ios::in | std::ios::out | std::ios::binary);
  1. 先创建对象,再用 .open() 打开
   std::fstream file;
   file.open("data.dat", std::ios::binary | std::ios::in);
   if (!file.is_open()) {
       // 处理错误
   }

打开模式(ios 标志位,可用 | 组合)

  • std::ios::in:读方式打开。
  • std::ios::out:写方式打开(默认会截断文件内容,类似 C 的 "w")。
  • std::ios::app:追加模式(所有写入都在末尾)。
  • std::ios::ate:打开后定位到文件末尾。
  • std::ios::trunc:如果文件存在,先清空(out 的默认行为)。
  • std::ios::binary:二进制模式。

注意fstreamout 模式不会自动包含 in 模式,需要同时读写时必须同时指定 in | out

3. 检查文件是否成功打开

if (!inFile) { // 直接判断流对象的状态
    std::cerr << "Failed to open file!" << std::endl;
    return;
}
// 或者更明确地
if (!inFile.is_open()) {
    // ...
}

4. 关闭文件

文件流对象析构时会自动关闭文件,但可以显式调用 .close()

outFile.close();

5. 读写文件

得益于运算符重载和继承,读写非常简单直观。

文本文件读写

使用流插入运算符 << 和流提取运算符 >>

std::ofstream out("test.txt");
out << "Hello, World! " << 42 << std::endl; // 写入

std::ifstream in("test.txt");
std::string str;
int num;
in >> str >> num; // 读取 "Hello," 到str, 42到num。注意:>> 会以空格为分隔符。
std::cout << str << " " << num; // 输出: Hello, 42

使用 getline 读取整行:

std::string line;
while (std::getline(in, line)) { // 逐行读取,直到文件末尾
    std::cout << line << std::endl;
}
二进制文件读写

使用 .read().write() 成员函数。

// 写入
Student s{"Alice", 1, 95.5f};
std::ofstream bOut("data.bin", std::ios::binary);
bOut.write(reinterpret_cast<const char*>(&s), sizeof(Student));
bOut.close();

// 读取
Student s_read;
std::ifstream bIn("data.bin", std::ios::binary);
bIn.read(reinterpret_cast<char*>(&s_read), sizeof(Student));
bIn.close();

注意:必须使用 reinterpret_cast 进行指针类型转换。

6. 文件定位

与 C 语言类似,但有成员函数形式。

std::fstream file("data.bin", std::ios::binary | std::ios::in | std::ios::out);

// 获取当前位置
std::streampos pos = file.tellg(); // tellg() 用于读位置 (get)
// file.tellp() 用于写位置 (put),在单一流中通常两者同步

// 移动位置
file.seekg(offset, origin); // 移动读指针
file.seekp(offset, origin); // 移动写指针

// origin 可以是:
std::ios::beg // 文件开头
std::ios::cur // 当前位置
std::ios::end // 文件末尾

// 示例:跳到文件开头往后10字节处
file.seekg(10, std::ios::beg);
// 示例:跳到文件末尾
file.seekg(0, std::ios::end);

三、总结与对比

特性C 语言 (stdio.h)C++ (fstream)
核心概念文件指针 (FILE*)文件流对象 (fstream, ifstream, ofstream)
打开方式fopen(filename, mode)构造函数或 .open(),使用 std::ios 标志
错误处理检查返回值 (NULL, EOF)检查流状态 (if (!file)file.fail())
读写接口fprintf, fscanf, fread, fwrite流运算符 <<, >>,成员函数 .read(), .write()
关闭文件显式 fclose析构函数自动调用或显式 .close()
易用性较低,需要手动管理格式和缓冲区,与标准 I/O 用法一致,类型安全
面向对象,支持 RAII(资源获取即初始化)
推荐场景需要与 C 代码兼容,或追求极致轻量绝大多数现代 C++ 项目,代码更清晰、安全

四、最佳实践与常见陷阱

  1. 总是检查文件是否成功打开:这是最常见的错误来源。

  2. 明确文本模式与二进制模式:在 Windows 平台上处理非文本文件(如图片、视频、含特定格式的文本)时,务必使用二进制模式 ("b"std::ios::binary) 以避免换行符转换带来的问题。

  3. C++ 中优先使用 std::fstream 等类:它们更安全,更符合 C++ 的编程风格。

  4. 理解 fread/fwrite 的返回值:它返回的是成功读写的元素个数,而不是字节数。循环读取时应以此作为判断条件。

  5. 小心 fscanf>>:它们会跳过空白字符,并且格式错误会导致输入流进入错误状态。读取字符串时可能有缓冲区溢出的风险(应指定宽度,如 %99s)。

  6. 使用 RAII:在 C++ 中,利用局部对象析构自动关闭文件的特性,将文件操作封装在作用域内。

   { // 进入一个作用域
       std::ofstream file("output.txt");
       file << "Data";
   } // 离开作用域,file 析构,文件自动关闭
  1. 处理路径:使用绝对路径或相对于程序运行当前目录的相对路径。C++17 引入了 <filesystem> 库,可以更方便、跨平台地处理路径。

更多推荐