深入浅出C语言——文件操作

文章目录

  • 前言
  • 一、什么是文件?
    • [1. 文件的概念](#1. 文件的概念)
      • [1.1 程序文件](#1.1 程序文件)
      • [1.2 数据文件](#1.2 数据文件)
    • [2. 文件名](#2. 文件名)
      • [2.1 文件路径](#2.1 文件路径)
  • 二、文件的打开和关闭
    • [1. 文件指针](#1. 文件指针)
    • [2. 文件的打开和关闭](#2. 文件的打开和关闭)
      • [2.1 fopen函数](#2.1 fopen函数)
      • [2.2 fclose函数](#2.2 fclose函数)
      • [2.3 文件打开模式](#2.3 文件打开模式)
      • [2.4 基本使用示例](#2.4 基本使用示例)
  • 三、文件的顺序读写
    • [1. 文件读写函数概览](#1. 文件读写函数概览)
    • [2. 字符输入输出函数](#2. 字符输入输出函数)
      • [2.1 fgetc和fputc](#2.1 fgetc和fputc)
    • [3. 文本行输入输出函数](#3. 文本行输入输出函数)
      • [3.1 fgets和fputs](#3.1 fgets和fputs)
    • [4. 格式化输入输出函数](#4. 格式化输入输出函数)
      • [4.1 fscanf和fprintf](#4.1 fscanf和fprintf)
    • [5. 二进制输入输出函数](#5. 二进制输入输出函数)
      • [5.1 fread和fwrite](#5.1 fread和fwrite)
  • 四、文件的随机读写
    • [1. fseek函数](#1. fseek函数)
    • [2. ftell函数](#2. ftell函数)
    • [3. rewind函数](#3. rewind函数)
  • 五、文本文件和二进制文件
    • [1. 文本文件 vs 二进制文件](#1. 文本文件 vs 二进制文件)
    • [2. 数据在内存中的存储](#2. 数据在内存中的存储)
  • 六、文件读取结束的判定
    • [1. 被错误使用的feof](#1. 被错误使用的feof)
    • [2. 文本文件的读取结束判定](#2. 文本文件的读取结束判定)
    • [3. 二进制文件的读取结束判定](#3. 二进制文件的读取结束判定)
    • [4. ferror函数](#4. ferror函数)
  • 七、文件缓冲区
    • [1. 缓冲文件系统](#1. 缓冲文件系统)
    • [2. 缓冲区的刷新](#2. 缓冲区的刷新)
    • [3. fflush函数](#3. fflush函数)
  • 八、文件操作的最佳实践
    • [1. 错误处理](#1. 错误处理)
    • [2. 资源管理](#2. 资源管理)
    • [3. 使用标准输入输出流](#3. 使用标准输入输出流)
    • [4. 文件操作的完整示例](#4. 文件操作的完整示例)
    • [5. 文件操作的常见陷阱](#5. 文件操作的常见陷阱)
      • [5.1 忘记检查文件打开是否成功](#5.1 忘记检查文件打开是否成功)
      • [5.2 忘记关闭文件](#5.2 忘记关闭文件)
      • [5.3 使用错误的文件打开模式](#5.3 使用错误的文件打开模式)
      • [5.4 文本文件和二进制文件混用](#5.4 文本文件和二进制文件混用)
      • [5.5 文件指针移动后忘记重置](#5.5 文件指针移动后忘记重置)
    • [6. 跨平台注意事项](#6. 跨平台注意事项)
      • [6.1 路径分隔符](#6.1 路径分隔符)
      • [6.2 文本模式和二进制模式](#6.2 文本模式和二进制模式)
      • [6.3 文件大小限制](#6.3 文件大小限制)
    • [7. 实用工具函数](#7. 实用工具函数)
      • [7.1 安全打开文件](#7.1 安全打开文件)
      • [7.2 获取文件大小](#7.2 获取文件大小)
      • [7.3 检查文件是否存在](#7.3 检查文件是否存在)
  • 总结

前言

在之前的章节中,我们处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。但这种方式有一个明显的局限性:程序运行结束后,数据就消失了。

在实际开发中,我们经常需要将数据持久化保存,以便程序下次运行时能够读取之前的数据。这就是文件操作的用武之地。通过文件操作,我们可以将数据直接存放在电脑的硬盘上,实现数据的持久化存储。

今天,让我们一起深入探索C语言的文件操作机制,掌握如何读写文件,让我们的程序能够与外部世界进行数据交互。


一、什么是文件?

1. 文件的概念

在程序设计中,我们一般谈的文件有两种:程序文件数据文件(从文件功能的角度来分类的)。

1.1 程序文件

包括源程序文件(后缀为.c)、目标文件(Windows环境后缀为.obj)、可执行程序(Windows环境后缀为.exe)。

这些文件包含了程序的源代码、编译后的目标代码或可执行代码。

1.2 数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

本章讨论的是数据文件。

在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。

2. 文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。

文件名包含3部分:文件路径 + 文件名主干 + 文件后缀

例如c:\code\test.txt

  • 文件路径:c:\code\
  • 文件名主干:test
  • 文件后缀:.txt

为了方便起见,文件标识常被称为文件名。

2.1 文件路径

相对路径和绝对路径

  • 绝对路径:从根目录开始的完整路径

    • Windows:C:\Users\Username\Documents\file.txt
    • Linux/macOS:/home/username/documents/file.txt
  • 相对路径:相对于当前工作目录的路径

    • file.txt:当前目录下的文件
    • ../file.txt:上一级目录下的文件
    • subdir/file.txt:当前目录下子目录中的文件

路径分隔符

  • Windows:使用反斜杠\,但在C字符串中需要转义为\\,或使用正斜杠/(C标准库支持)
  • Linux/macOS:使用正斜杠/

示例

c 复制代码
// Windows路径的两种写法
FILE* fp1 = fopen("C:\\Users\\test\\file.txt", "r");  // 使用转义的反斜杠
FILE* fp2 = fopen("C:/Users/test/file.txt", "r");     // 使用正斜杠(推荐,跨平台)

// 相对路径
FILE* fp3 = fopen("file.txt", "r");                    // 当前目录
FILE* fp4 = fopen("../data.txt", "r");                 // 上一级目录

二、文件的打开和关闭

1. 文件指针

在缓冲文件系统中,关键的概念是"文件类型指针 ",简称"文件指针"。

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字、文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE

文件类型声明(不同编译器可能略有不同):

c 复制代码
struct _iobuf {
    char *_ptr;        // 文件输入的下一个位置
    int _cnt;          // 当前缓冲区的相对位置
    char *_base;       // 指基础位置(文件的起始位置)
    int _flag;         // 文件标志
    int _file;         // 文件的有效性验证
    int _charbuf;      // 检查缓冲区状况,如果无缓冲区则不读取
    int _bufsiz;       // 文件的大小
    char *_tmpfname;   // 临时文件名
};
typedef struct _iobuf FILE;

重要说明

  • 不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异
  • 每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节
  • 一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便

文件指针的使用

c 复制代码
FILE* pf;  // 定义pf是一个指向FILE类型数据的指针变量

pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。

文件指针与文件的关系示意图

复制代码
┌─────────────────────────────────┐
│  程序中的文件指针 (FILE* pf)    │
│         ↓                        │
│  指向文件信息区                  │
│  ┌───────────────────────────┐ │
│  │ FILE结构体变量              │ │
│  │ - 文件名                    │ │
│  │ - 文件状态                  │ │
│  │ - 文件当前位置              │ │
│  │ - 缓冲区信息                │ │
│  └───────────────────────────┘ │
│         ↓                        │
│  关联到磁盘上的实际文件          │
│  ┌───────────────────────────┐ │
│  │  test.txt                  │ │
│  │  (磁盘上的文件)             │ │
│  └───────────────────────────┘ │
└─────────────────────────────────┘

2. 文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。

在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

ANSI C规定使用fopen函数来打开文件,fclose来关闭文件。

2.1 fopen函数

函数原型

c 复制代码
FILE * fopen(const char * filename, const char * mode);

功能:打开文件

参数说明

  • filename:要打开的文件名(包含路径)
  • mode:文件打开模式

返回值

  • 成功:返回指向该文件的FILE*指针
  • 失败:返回NULL指针

2.2 fclose函数

函数原型

c 复制代码
int fclose(FILE * stream);

功能:关闭文件

参数说明

  • stream:指向要关闭的文件的指针

返回值

  • 成功:返回0
  • 失败:返回EOF(-1)

2.3 文件打开模式

文件使用方式 含义 如果指定文件不存在 如果指定文件存在
"r" 只读 为了输入数据,打开一个已经存在的文本文件 出错
"w" 只写 为了输出数据,打开一个文本文件 建立新文件
"a" 追加 向文本文件尾添加数据 建立新文件
"rb" 只读 为了输入数据,打开一个二进制文件 出错
"wb" 只写 为了输出数据,打开一个二进制文件 建立新文件
"ab" 追加 向一个二进制文件尾添加数据 建立新文件
"r+" 读写 为了读和写,打开一个文本文件 出错
"w+" 读写 为了读和写,建立一个新的文件 建立新文件
"a+" 读写 打开一个文件,在文件尾进行读写 建立新文件
"rb+" 读写 为了读和写打开一个二进制文件 出错
"wb+" 读写 为了读和写,新建一个新的二进制文件 建立新文件
"ab+" 读写 打开一个二进制文件,在文件尾进行读和写 建立新文件

重要提示

  • "w""wb"模式会清空已存在文件的内容(如果文件不存在则创建新文件)
  • "a""ab"模式在文件末尾追加,不会清空原有内容(如果文件不存在则创建新文件)
  • 使用"r""r+"模式时,文件必须存在,否则会失败
  • 文本模式和二进制模式的区别主要在于换行符的处理(Windows下\r\n vs \n
  • "r+""w+"的区别:"r+"要求文件存在,"w+"会创建新文件或清空已存在文件
  • "a+"模式可以在文件末尾读写,但写入总是在文件末尾(即使先读取后写入)

2.4 基本使用示例

c 复制代码
#include <stdio.h>

int main()
{
    FILE* pFile;
    
    // 打开文件
    pFile = fopen("myfile.txt", "w");
    
    // 文件操作
    if (pFile != NULL)
    {
        fputs("fopen example", pFile);
        
        // 关闭文件
        fclose(pFile);
        pFile = NULL;  // 关闭后置NULL,避免野指针
    }
    else
    {
        perror("fopen");  // 打印错误信息
        return 1;
    }
    
    return 0;
}

更安全的写法

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    FILE* pFile = NULL;
    
    // 打开文件
    pFile = fopen("myfile.txt", "w");
    if (pFile == NULL)
    {
        perror("fopen");
        return EXIT_FAILURE;
    }
    
    // 文件操作
    fputs("fopen example", pFile);
    
    // 关闭文件
    if (fclose(pFile) != 0)
    {
        perror("fclose");
        return EXIT_FAILURE;
    }
    pFile = NULL;
    
    return 0;
}

三、文件的顺序读写

1. 文件读写函数概览

功能 函数名 适用于
字符输入函数 fgetc 所有输入流
字符输出函数 fputc 所有输出流
文本行输入函数 fgets 所有输入流
文本行输出函数 fputs 所有输出流
格式化输入函数 fscanf 所有输入流
格式化输出函数 fprintf 所有输出流
二进制输入 fread 文件
二进制输出 fwrite 文件

说明 :这里的"流"(stream)是一个抽象的概念,可以理解为数据流动的通道。标准输入流(stdin)、标准输出流(stdout)、标准错误流(stderr)都是文件指针,所以这些函数也可以用于标准输入输出。

2. 字符输入输出函数

2.1 fgetc和fputc

fgetc函数

c 复制代码
int fgetc(FILE * stream);
  • 功能:从指定的流中读取一个字符
  • 返回值:成功返回读取的字符(转换为int),失败或到达文件末尾返回EOF

fputc函数

c 复制代码
int fputc(int character, FILE * stream);
  • 功能:向指定的流中写入一个字符
  • 返回值:成功返回写入的字符,失败返回EOF

示例:复制文件

c 复制代码
#include <stdio.h>

int main()
{
    FILE* src = fopen("source.txt", "r");
    FILE* dst = fopen("dest.txt", "w");
    
    if (src == NULL || dst == NULL)
    {
        perror("fopen");
        if (src != NULL) fclose(src);
        if (dst != NULL) fclose(dst);
        return 1;
    }
    
    int c;
    // 从源文件读取字符,写入目标文件
    while ((c = fgetc(src)) != EOF)
    {
        fputc(c, dst);
    }
    
    fclose(src);
    fclose(dst);
    src = NULL;
    dst = NULL;
    
    return 0;
}

3. 文本行输入输出函数

3.1 fgets和fputs

fgets函数

c 复制代码
char * fgets(char * str, int num, FILE * stream);
  • 功能:从指定的流中读取一行(最多num-1个字符)
  • 参数:
    • str:存储读取数据的缓冲区
    • num:最大读取字符数(包括\0
    • stream:文件指针
  • 返回值:成功返回str,失败或到达文件末尾返回NULL
  • 注意fgets会保留换行符,并在字符串末尾添加\0

fputs函数

c 复制代码
int fputs(const char * str, FILE * stream);
  • 功能:向指定的流中写入一个字符串
  • 返回值:成功返回非负值,失败返回EOF
  • 注意fputs不会自动添加换行符

示例:逐行读取文件

c 复制代码
#include <stdio.h>

int main()
{
    FILE* fp = fopen("test.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    char buffer[256];
    // 逐行读取
    while (fgets(buffer, sizeof(buffer), fp) != NULL)
    {
        printf("%s", buffer);  // fgets已经包含换行符
    }
    
    fclose(fp);
    fp = NULL;
    
    return 0;
}

4. 格式化输入输出函数

4.1 fscanf和fprintf

fscanf函数

c 复制代码
int fscanf(FILE * stream, const char * format, ...);
  • 功能:从指定的流中按格式读取数据
  • 返回值:成功返回成功读取的项数,失败或到达文件末尾返回EOF

fprintf函数

c 复制代码
int fprintf(FILE * stream, const char * format, ...);
  • 功能:向指定的流中按格式写入数据
  • 返回值:成功返回写入的字符数,失败返回负值

示例:读写结构化数据

c 复制代码
#include <stdio.h>

struct Student
{
    char name[20];
    int age;
    float score;
};

int main()
{
    FILE* fp = fopen("students.txt", "w");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    // 写入数据
    struct Student s1 = {"张三", 20, 85.5};
    struct Student s2 = {"李四", 21, 90.0};
    
    fprintf(fp, "%s %d %.2f\n", s1.name, s1.age, s1.score);
    fprintf(fp, "%s %d %.2f\n", s2.name, s2.age, s2.score);
    
    fclose(fp);
    fp = NULL;
    
    // 读取数据
    fp = fopen("students.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    struct Student s;
    while (fscanf(fp, "%s %d %f", s.name, &s.age, &s.score) == 3)
    {
        printf("姓名:%s,年龄:%d,分数:%.2f\n", s.name, s.age, s.score);
    }
    
    fclose(fp);
    fp = NULL;
    
    return 0;
}

5. 二进制输入输出函数

5.1 fread和fwrite

fread函数

c 复制代码
size_t fread(void * ptr, size_t size, size_t count, FILE * stream);
  • 功能:从指定的流中读取二进制数据
  • 参数:
    • ptr:存储读取数据的缓冲区
    • size:每个元素的大小(字节)
    • count:要读取的元素个数
    • stream:文件指针
  • 返回值:成功返回实际读取的元素个数,失败或到达文件末尾返回小于count的值

fwrite函数

c 复制代码
size_t fwrite(const void * ptr, size_t size, size_t count, FILE * stream);
  • 功能:向指定的流中写入二进制数据
  • 参数:同fread
  • 返回值:成功返回实际写入的元素个数,失败返回小于count的值

示例:读写二进制数据

c 复制代码
#include <stdio.h>

int main()
{
    // 写入二进制数据
    int arr[5] = {1, 2, 3, 4, 5};
    FILE* fp = fopen("data.bin", "wb");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    size_t written = fwrite(arr, sizeof(int), 5, fp);
    printf("写入 %zu 个元素\n", written);
    
    fclose(fp);
    fp = NULL;
    
    // 读取二进制数据
    int arr2[5];
    fp = fopen("data.bin", "rb");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    size_t read = fread(arr2, sizeof(int), 5, fp);
    printf("读取 %zu 个元素:", read);
    for (size_t i = 0; i < read; i++)
    {
        printf("%d ", arr2[i]);
    }
    printf("\n");
    
    fclose(fp);
    fp = NULL;
    
    return 0;
}

四、文件的随机读写

顺序读写是按照数据在文件中的顺序依次读写,而随机读写可以让我们在文件的任意位置进行读写操作。

1. fseek函数

函数原型

c 复制代码
int fseek(FILE * stream, long int offset, int origin);

功能:根据文件指针的位置和偏移量来定位文件指针

参数说明

  • stream:文件指针
  • offset:偏移量(字节数)
  • origin:起始位置,可以是:
    • SEEK_SET:文件开头
    • SEEK_CUR:当前位置
    • SEEK_END:文件末尾

返回值

  • 成功:返回0
  • 失败:返回非0值

示例

c 复制代码
#include <stdio.h>

int main()
{
    FILE* pFile;
    pFile = fopen("example.txt", "wb");
    if (pFile == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    fputs("This is an apple.", pFile);
    
    // 将文件指针移动到第9个字节(从开头)
    fseek(pFile, 9, SEEK_SET);
    
    // 从当前位置写入
    fputs(" sam", pFile);
    // 结果:文件内容变为 "This is a sample."
    
    fclose(pFile);
    pFile = NULL;
    
    return 0;
}

2. ftell函数

函数原型

c 复制代码
long int ftell(FILE * stream);

功能:返回文件指针相对于起始位置的偏移量(字节数)

返回值

  • 成功:返回当前位置的偏移量
  • 失败:返回-1L

示例:获取文件大小

c 复制代码
#include <stdio.h>

int main()
{
    FILE* pFile;
    long size;
    
    pFile = fopen("myfile.txt", "rb");
    if (pFile == NULL)
    {
        perror("Error opening file");
        return 1;
    }
    
    // 将文件指针移动到文件末尾
    fseek(pFile, 0, SEEK_END);
    
    // 获取当前位置(即文件大小)
    size = ftell(pFile);
    
    printf("Size of myfile.txt: %ld bytes.\n", size);
    
    fclose(pFile);
    pFile = NULL;
    
    return 0;
}

注意 :使用fseek(pFile, 0, SEEK_END)来获取文件大小在某些系统上可能不是可移植的,但对于大多数系统是可行的。

3. rewind函数

函数原型

c 复制代码
void rewind(FILE * stream);

功能:让文件指针的位置回到文件的起始位置

等价于fseek(stream, 0, SEEK_SET)

示例

c 复制代码
#include <stdio.h>

int main()
{
    int n;
    FILE* pFile;
    char buffer[27];
    
    pFile = fopen("myfile.txt", "w+");
    if (pFile == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    // 写入A-Z
    for (n = 'A'; n <= 'Z'; n++)
    {
        fputc(n, pFile);
    }
    
    // 将文件指针重置到开头
    rewind(pFile);
    
    // 读取26个字符(使用fgetc更合适,但fread也可以)
    int i = 0;
    int c;
    while ((c = fgetc(pFile)) != EOF && i < 26)
    {
        buffer[i++] = (char)c;
    }
    buffer[i] = '\0';
    
    // 或者使用fread(二进制方式读取)
    // fread(buffer, 1, 26, pFile);
    // buffer[26] = '\0';
    
    puts(buffer);  // 输出:ABCDEFGHIJKLMNOPQRSTUVWXYZ
    
    fclose(pFile);
    pFile = NULL;
    
    return 0;
}

五、文本文件和二进制文件

1. 文本文件 vs 二进制文件

根据数据的组织形式,数据文件被称为文本文件 或者二进制文件

区别

  • 二进制文件:数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件
  • 文本文件:如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件

2. 数据在内存中的存储

一个数据在内存中是怎么存储的呢?

  • 字符:一律以ASCII形式存储
  • 数值型数据:既可以用ASCII形式存储,也可以使用二进制形式存储

示例:整数10000的存储

  • ASCII形式 :存储为字符串"10000",占用5个字节(每个字符一个字节)
  • 二进制形式 :存储为int类型的二进制表示,占用4个字节(在大多数系统上)

内存布局对比

复制代码
ASCII形式(文本文件):
┌─────────────────────────────────┐
│ '1' '0' '0' '0' '0'             │
│ 0x31 0x30 0x30 0x30 0x30        │
│ 5个字节                          │
└─────────────────────────────────┘

二进制形式(二进制文件):
┌─────────────────────────────────┐
│ 0x10 0x27 0x00 0x00             │
│ (小端序,10000 = 0x00002710)    │
│ 4个字节                          │
└─────────────────────────────────┘

示例代码

c 复制代码
#include <stdio.h>

int main()
{
    int a = 10000;
    FILE* pf;
    
    // 以二进制形式写入
    pf = fopen("test.bin", "wb");
    if (pf != NULL)
    {
        fwrite(&a, sizeof(int), 1, pf);  // 二进制的形式写到文件中
        fclose(pf);
        pf = NULL;
    }
    
    // 以文本形式写入
    pf = fopen("test.txt", "w");
    if (pf != NULL)
    {
        fprintf(pf, "%d", a);  // 文本形式写入
        fclose(pf);
        pf = NULL;
    }
    
    return 0;
}

文件大小对比

  • test.bin:4字节(二进制形式)
  • test.txt:5字节(ASCII形式,每个字符一个字节)

六、文件读取结束的判定

1. 被错误使用的feof

牢记 :在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束

feof的作用是:当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。

错误用法

c 复制代码
// 错误!不要这样用
while (!feof(fp))
{
    c = fgetc(fp);
    // ...
}

正确用法

  1. 文本文件 读取是否结束,判断返回值是否为EOFfgetc),或者NULLfgets
  2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数

2. 文本文件的读取结束判定

fgetc判断是否为EOF

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int c;  // 注意:int,非char,要求处理EOF
    FILE* fp = fopen("test.txt", "r");
    
    if (fp == NULL)
    {
        perror("File opening failed");
        return EXIT_FAILURE;
    }
    
    // fgetc当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while ((c = fgetc(fp)) != EOF)  // 标准C I/O读取文件循环
    {
        putchar(c);
    }
    
    // 判断是什么原因结束的
    if (ferror(fp))
    {
        puts("I/O error when reading");
    }
    else if (feof(fp))
    {
        puts("End of file reached successfully");
    }
    
    fclose(fp);
    fp = NULL;
    
    return 0;
}

fgets判断返回值是否为NULL

c 复制代码
#include <stdio.h>

int main()
{
    FILE* fp = fopen("test.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    char buffer[256];
    while (fgets(buffer, sizeof(buffer), fp) != NULL)  // 正确判断
    {
        printf("%s", buffer);
    }
    
    // 判断结束原因
    if (feof(fp))
    {
        printf("到达文件末尾\n");
    }
    if (ferror(fp))
    {
        printf("读取错误\n");
    }
    
    fclose(fp);
    fp = NULL;
    
    return 0;
}

3. 二进制文件的读取结束判定

fread判断返回值是否小于实际要读的个数

c 复制代码
#include <stdio.h>

enum { SIZE = 5 };

int main(void)
{
    double a[SIZE] = {1., 2., 3., 4., 5.};
    FILE* fp = fopen("test.bin", "wb");  // 必须用二进制模式
    
    if (fp != NULL)
    {
        fwrite(a, sizeof *a, SIZE, fp);  // 写double的数组
        fclose(fp);
        fp = NULL;
    }
    
    double b[SIZE];
    fp = fopen("test.bin", "rb");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    size_t ret_code = fread(b, sizeof *b, SIZE, fp);  // 读double的数组
    
    if (ret_code == SIZE)
    {
        puts("Array read successfully, contents: ");
        for (int n = 0; n < SIZE; ++n)
        {
            printf("%f ", b[n]);
        }
        putchar('\n');
    }
    else  // error handling
    {
        if (feof(fp))
        {
            printf("Error reading test.bin: unexpected end of file\n");
        }
        else if (ferror(fp))
        {
            perror("Error reading test.bin");
        }
    }
    
    fclose(fp);
    fp = NULL;
    
    return 0;
}

4. ferror函数

函数原型

c 复制代码
int ferror(FILE * stream);

功能:检查文件流是否发生了错误

返回值

  • 如果发生了错误:返回非0值
  • 如果没有错误:返回0

使用场景:在文件读取结束后,判断是否是因为错误而结束的。


七、文件缓冲区

1. 缓冲文件系统

ANSI C标准采用"缓冲文件系统"处理的数据文件,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块"文件缓冲区"。

工作原理

  • 输出数据:从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上
  • 输入数据:如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)

缓冲区的大小根据C编译系统决定。

文件缓冲区示意图

复制代码
程序数据区(变量)  ←→  文件缓冲区  ←→  磁盘文件

写入过程:
程序 → 缓冲区 → 磁盘
      (积累)   (一次性写入)

读取过程:
磁盘 → 缓冲区 → 程序
      (一次性读取) (逐个使用)

2. 缓冲区的刷新

因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。

刷新缓冲区的方法

  1. fflush函数:强制刷新缓冲区
  2. fclose函数:关闭文件时自动刷新缓冲区
  3. 程序正常结束:也会刷新缓冲区

3. fflush函数

函数原型

c 复制代码
int fflush(FILE * stream);

功能:刷新输出缓冲区,将缓冲区中的数据立即写入文件

参数

  • stream:文件指针,如果为NULL,则刷新所有输出流

返回值

  • 成功:返回0
  • 失败:返回EOF

注意fflush在高版本的VS上可能不能使用(对于输入流),但对于输出流通常是可用的。

示例:演示缓冲区的作用

c 复制代码
#include <stdio.h>
#include <windows.h>  // 用于Sleep函数(Windows)

// VS2013 WIN10环境测试
int main()
{
    FILE* pf = fopen("test.txt", "w");
    if (pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    fputs("abcdef", pf);  // 先将代码放在输出缓冲区
    
    printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
    Sleep(10000);  // Windows下的延时函数
    
    printf("刷新缓冲区\n");
    fflush(pf);  // 刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
    
    // 注:fflush在高版本的VS上可能不能使用(对于输入流)
    
    printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
    Sleep(10000);
    
    fclose(pf);  // 注:fclose在关闭文件的时候,也会刷新缓冲区
    pf = NULL;
    
    return 0;
}

关键结论

  • 因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件
  • 如果不做,可能导致读写文件的问题(数据可能还在缓冲区中,没有真正写入磁盘)

八、文件操作的最佳实践

1. 错误处理

总是检查文件打开是否成功

c 复制代码
FILE* fp = fopen("file.txt", "r");
if (fp == NULL)
{
    perror("fopen");  // 打印详细的错误信息
    // 或者
    fprintf(stderr, "无法打开文件: %s\n", "file.txt");
    return 1;
}

2. 资源管理

确保文件被正确关闭

c 复制代码
FILE* fp = fopen("file.txt", "r");
if (fp == NULL)
{
    perror("fopen");
    return 1;
}

// 使用文件
// ...

// 关闭文件
if (fclose(fp) != 0)
{
    perror("fclose");
}
fp = NULL;  // 置NULL,避免野指针

3. 使用标准输入输出流

标准输入输出流也是文件指针

C语言预定义了三个标准流:

  • stdin:标准输入流(通常是键盘)
  • stdout:标准输出流(通常是屏幕)
  • stderr:标准错误流(通常是屏幕,用于错误信息)

这些流在程序启动时自动打开,程序结束时自动关闭,不需要手动打开或关闭。

使用示例

c 复制代码
#include <stdio.h>

int main()
{
    char buffer[256];
    int n;
    
    // 这些函数可以用于标准输入输出
    fgetc(stdin);              // 等同于 getchar()
    fputc('A', stdout);        // 等同于 putchar('A')
    fgets(buffer, sizeof(buffer), stdin);  // 从键盘读取
    fputs("Hello", stdout);    // 输出到屏幕
    fprintf(stdout, "%d\n", 10);  // 等同于 printf("%d\n", 10)
    fscanf(stdin, "%d", &n);   // 等同于 scanf("%d", &n)
    
    // 错误信息应该输出到stderr
    fprintf(stderr, "错误:无法打开文件\n");
    
    return 0;
}

为什么使用stderr?

将错误信息输出到stderr而不是stdout的好处:

  • 可以将正常输出和错误信息分开处理(重定向时)
  • 错误信息会立即显示,不受缓冲区影响
  • 符合Unix/Linux的惯例

示例

bash 复制代码
# 在命令行中,可以分别重定向标准输出和标准错误
./program > output.txt 2> error.txt

4. 文件操作的完整示例

示例:学生信息管理系统(文件版本)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_NAME 20
#define MAX_STUDENTS 100

typedef struct
{
    char name[MAX_NAME];
    int age;
    float score;
} Student;

// 保存学生信息到文件
int save_students(Student* students, int count, const char* filename)
{
    FILE* fp = fopen(filename, "wb");
    if (fp == NULL)
    {
        perror("fopen");
        return 0;
    }
    
    size_t written = fwrite(students, sizeof(Student), count, fp);
    fclose(fp);
    fp = NULL;
    
    return (written == count);
}

// 从文件加载学生信息
int load_students(Student* students, int max_count, const char* filename)
{
    FILE* fp = fopen(filename, "rb");
    if (fp == NULL)
    {
        perror("fopen");
        return 0;
    }
    
    size_t read = fread(students, sizeof(Student), max_count, fp);
    fclose(fp);
    fp = NULL;
    
    return (int)read;
}

int main()
{
    Student students[MAX_STUDENTS];
    int count = 0;
    
    // 添加一些测试数据
    strcpy(students[0].name, "张三");
    students[0].age = 20;
    students[0].score = 85.5;
    
    strcpy(students[1].name, "李四");
    students[1].age = 21;
    students[1].score = 90.0;
    
    count = 2;
    
    // 保存到文件
    if (save_students(students, count, "students.bin"))
    {
        printf("成功保存 %d 个学生信息\n", count);
    }
    
    // 从文件加载
    Student loaded_students[MAX_STUDENTS];
    int loaded_count = load_students(loaded_students, MAX_STUDENTS, "students.bin");
    
    if (loaded_count > 0)
    {
        printf("成功加载 %d 个学生信息:\n", loaded_count);
        for (int i = 0; i < loaded_count; i++)
        {
            printf("姓名:%s,年龄:%d,分数:%.2f\n",
                   loaded_students[i].name,
                   loaded_students[i].age,
                   loaded_students[i].score);
        }
    }
    
    return 0;
}

5. 文件操作的常见陷阱

5.1 忘记检查文件打开是否成功

错误做法

c 复制代码
FILE* fp = fopen("file.txt", "r");
fgets(buffer, 256, fp);  // 如果fopen失败,fp是NULL,这里会崩溃

正确做法

c 复制代码
FILE* fp = fopen("file.txt", "r");
if (fp == NULL)
{
    perror("fopen");
    return 1;
}
fgets(buffer, 256, fp);

5.2 忘记关闭文件

错误做法

c 复制代码
FILE* fp = fopen("file.txt", "w");
fputs("Hello", fp);
// 忘记关闭文件,可能导致数据丢失或资源泄漏

正确做法

c 复制代码
FILE* fp = fopen("file.txt", "w");
if (fp != NULL)
{
    fputs("Hello", fp);
    fclose(fp);
    fp = NULL;
}

5.3 使用错误的文件打开模式

错误示例

c 复制代码
// 想要追加数据,但使用了"w"模式
FILE* fp = fopen("data.txt", "w");  // 错误!会清空文件
fprintf(fp, "新数据\n");

正确做法

c 复制代码
FILE* fp = fopen("data.txt", "a");  // 使用追加模式
if (fp != NULL)
{
    fprintf(fp, "新数据\n");
    fclose(fp);
    fp = NULL;
}

5.4 文本文件和二进制文件混用

错误示例

c 复制代码
// 以文本模式打开,但用fwrite写入二进制数据
FILE* fp = fopen("data.bin", "w");  // 应该是"wb"
int arr[5] = {1, 2, 3, 4, 5};
fwrite(arr, sizeof(int), 5, fp);

正确做法

c 复制代码
FILE* fp = fopen("data.bin", "wb");  // 使用二进制模式
if (fp != NULL)
{
    int arr[5] = {1, 2, 3, 4, 5};
    fwrite(arr, sizeof(int), 5, fp);
    fclose(fp);
    fp = NULL;
}

5.5 文件指针移动后忘记重置

错误示例

c 复制代码
FILE* fp = fopen("file.txt", "r+");
// 读取一些数据
fread(buffer, 1, 100, fp);
// 直接写入,但文件指针已经移动了
fwrite(data, 1, 50, fp);  // 可能不是从期望的位置写入

正确做法

c 复制代码
FILE* fp = fopen("file.txt", "r+");
if (fp != NULL)
{
    fread(buffer, 1, 100, fp);
    // 需要从特定位置写入时,使用fseek
    fseek(fp, 0, SEEK_SET);  // 回到开头
    // 或者
    fseek(fp, 100, SEEK_SET);  // 移动到特定位置
    fwrite(data, 1, 50, fp);
    fclose(fp);
    fp = NULL;
}

6. 跨平台注意事项

6.1 路径分隔符

问题 :Windows使用\,Linux/macOS使用/

解决方案

c 复制代码
// 方法1:使用正斜杠(推荐,C标准库支持)
FILE* fp = fopen("path/to/file.txt", "r");

// 方法2:使用条件编译
#ifdef _WIN32
    FILE* fp = fopen("path\\to\\file.txt", "r");
#else
    FILE* fp = fopen("path/to/file.txt", "r");
#endif

6.2 文本模式和二进制模式

Windows下的区别

  • 文本模式:\r\n(回车换行)会被转换为\n
  • 二进制模式:不进行转换

Linux/macOS下

  • 文本模式和二进制模式通常没有区别(都使用\n

建议

  • 处理文本文件时,使用文本模式("r", "w", "a"等)
  • 处理二进制数据时,使用二进制模式("rb", "wb", "ab"等)

6.3 文件大小限制

ftell的返回值类型

  • ftell返回long int,在某些系统上可能无法处理大于2GB的文件
  • C99引入了ftello(返回off_t)来处理大文件

示例

c 复制代码
// 对于大文件,可能需要使用ftello(如果支持)
#ifdef __USE_LARGEFILE64
    off_t size = ftello(fp);
#else
    long size = ftell(fp);
#endif

7. 实用工具函数

7.1 安全打开文件

c 复制代码
#include <stdio.h>
#include <stdlib.h>

FILE* safe_fopen(const char* filename, const char* mode)
{
    FILE* fp = fopen(filename, mode);
    if (fp == NULL)
    {
        perror("fopen");
        fprintf(stderr, "无法打开文件: %s (模式: %s)\n", filename, mode);
    }
    return fp;
}

// 使用
FILE* fp = safe_fopen("file.txt", "r");
if (fp == NULL)
{
    return 1;
}
// ... 使用文件 ...
fclose(fp);
fp = NULL;

7.2 获取文件大小

c 复制代码
#include <stdio.h>

long get_file_size(const char* filename)
{
    FILE* fp = fopen(filename, "rb");
    if (fp == NULL)
    {
        return -1;
    }
    
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fclose(fp);
    
    return size;
}

// 使用
long size = get_file_size("file.txt");
if (size >= 0)
{
    printf("文件大小: %ld 字节\n", size);
}

7.3 检查文件是否存在

c 复制代码
#include <stdio.h>

int file_exists(const char* filename)
{
    FILE* fp = fopen(filename, "r");
    if (fp != NULL)
    {
        fclose(fp);
        return 1;  // 文件存在且可读
    }
    return 0;  // 文件不存在或无法读取
}

// 使用
if (file_exists("file.txt"))
{
    printf("文件存在且可读\n");
}
else
{
    printf("文件不存在或无法读取\n");
}

注意 :这个函数只能检查文件是否存在且可读。如果文件存在但没有读取权限,fopen会失败,函数会返回0。更精确的文件存在检查需要使用系统特定的函数(如Windows的_access或POSIX的access)。


总结

文件操作是C语言编程中的重要技能,它让我们能够实现数据的持久化存储。掌握好文件操作,不仅能让我们写出更实用的程序,还能处理各种数据交互场景。

关键要点

  1. 文件指针

    • 使用FILE*类型的指针来操作文件
    • 每个打开的文件都有一个FILE结构体变量
  2. 文件打开和关闭

    • 使用fopen打开文件,总是检查返回值
    • 使用fclose关闭文件,关闭后置指针为NULL
  3. 文件读写

    • 字符:fgetc/fputc
    • 文本行:fgets/fputs
    • 格式化:fscanf/fprintf
    • 二进制:fread/fwrite
  4. 随机读写

    • fseek:定位文件指针
    • ftell:获取当前位置
    • rewind:回到文件开头
  5. 文件结束判定

    • 文本文件:检查EOFNULL
    • 二进制文件:检查返回值是否小于预期
    • 使用feofferror判断结束原因
  6. 文件缓冲区

    • 理解缓冲文件系统的工作原理
    • 必要时使用fflush刷新缓冲区
    • 关闭文件时会自动刷新缓冲区

最佳实践

  • 总是检查文件打开是否成功
  • 确保文件被正确关闭
  • 使用适当的文件打开模式
  • 正确处理文件读取结束的情况
  • 理解文本文件和二进制文件的区别
  • 注意缓冲区的存在,必要时刷新

希望这份指南能帮助你深入理解C语言的文件操作,写出更安全、更实用的程序!记住,文件操作看似简单,但细节决定成败。多实践、多调试,才能真正掌握这些知识。

相关推荐
CoderYanger10 小时前
C.滑动窗口-求子数组个数-越长越合法——3325. 字符至少出现 K 次的子字符串 I
c语言·数据结构·算法·leetcode·职场和发展·哈希算法·散列表
点灯master10 小时前
DAC8562的驱动设计开发
c语言·驱动开发·stm32
李绍熹11 小时前
C语言基础语法示例
c语言·开发语言
法号:行颠11 小时前
Chaos-nano协作式异步操作系统(六):`Chaos-nano` 在手持式 `VOC` 检测设备上的应用
c语言·单片机·嵌入式硬件·mcu·系统架构
南棱笑笑生11 小时前
20251213给飞凌OK3588-C开发板适配Rockchip原厂的Buildroot【linux-6.1】系统时适配CTP触摸屏FT5X06
linux·c语言·开发语言·rockchip
南棱笑笑生13 小时前
20251213给飞凌OK3588-C开发板适配Rockchip原厂的Buildroot【linux-6.1】系统时适配type-C0
linux·c语言·开发语言·rockchip
小猪猪屁14 小时前
顺序表与链表:头插法与尾插法详解
c语言·数据结构·c++
历程里程碑14 小时前
C++ 5:模板初阶
c语言·开发语言·数据结构·c++·算法
R-G-B16 小时前
哈希表(hashtable),哈希理论,数组实现哈希结构 (C语言),散列理论 (拉链发、链接发),散列实现哈希结构,c++ 实现哈希
c语言·哈希算法·散列表·哈希表·数组实现哈希结构·散列实现哈希结构·c++ 实现哈希