C 语言文件操作详解

目录

1. 什么是文件

1.1 文件名

1.2 程序文件

1.3 数据文件

2. 文件的打开和关闭

2.1 流和标准流

2.1.1 流

2.1.2 标准流

2.2 文件指针

2.3 文件的打开和关闭

3. 文件的顺序读写

3.1 顺序读写函数

3.2 使用示例

3.2.1 fgetc 和 fputc

3.2.2 fgets 和 fputs

3.2.3 fscanf 和 fprintf

3.2.4 fread 和 fwrite

3.3 功能对比

3.4 scanf/fscanf/sscanf 与 printf/fprintf/sprintf

3.4.1 sscanf 和 sprintf 使用示例

4. 文件的随机读写

4.1 fseek

4.2 ftell

4.3 rewind

5. 文件读取结束的判定

5.1 文本文件读取结束的例子

5.2 二进制文件读取结束的例子

6. 文件缓冲区

:如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!

:如果你觉得这篇文章还不错,不妨顺手、加入收藏,并分享给更多的朋友噢~!


1. 什么是文件

磁盘(硬盘)上的文件是文件。在程序设计中,一般谈及的文件有两种,即从文件功能角度分类的程序文件和数据文件。

1.1 文件名

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

例如,c : \code\test.txt 

c : \code\ 文件路径

test 文件名主干

.txt 文件后缀

1.2 程序文件

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

1.3 数据文件

根据数据的组织形式,数据文件可分为文本文件或者二进制文件。

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

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

例如整数 10000,以 ASCII 码形式输出到磁盘,在磁盘中占 5 个字节(每个字符一个字节);以二进制形式输出,在磁盘上只占 4 个字节。


2. 文件的打开和关闭

2.1 流和标准流

2.1.1 流

程序的数据需输出到各种外部设备,也需从外部设备获取数据,不同外部设备的输入输出操作不同。为方便程序员对各种设备进行操作,抽象出了流的概念,可将流想象成流淌着字符的河。

C 程序针对数据的输入输出操作都通过流操作。

一般,要向流里写数据或从流中读取数据,都要先打开流,再进行操作。

2.1.2 标准流

C 语言程序启动时,默认打开 3 个流:

  • stdin - 标准输入流,大多数环境中从键盘输入,scanf 函数就是从标准输入流中读取数据。
  • stdout - 标准输出流,大多数环境中输出至显示器界面,printf 函数就是将信息输出到标准输出流中。
  • stderr - 标准错误流,大多数环境中输出到显示器界面。

stdinstdoutstderr 三个流的类型是 FILE*,通常称为文件指针。

C 语言通过 FILE* 的文件指针来维护流的各种操作。

2.2 文件指针

缓冲文件系统的关键概念是“文件指针”(即“文件类型指针”)。

每个使用中的文件会在内存开辟文件信息区,存放文件名字、状态、当前位置等信息,这些信息存于系统声明的 FILE 结构体变量。不同 C 编译器的 FILE 类型内容大致相同。打开文件时,系统自动创建并填充 FILE 结构体变量,用户无需关注细节。

一般用一个 FILE 指针维护该结构体变量。

FILE* pf; 

声明一个 FILE 类型的指针变量,指向文件信息区,通过它能间接找到与它关联的文件。

2.3 文件的打开和关闭

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

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

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

// 打开文件
FILE* fopen ( const char* filename, const char* mode );
// 关闭文件
int fclose ( FILE* stream );

mode 表示文件的打开模式。

常见的文件打开模式如下:

文件打开模式 含义 若指定的文件不存在
“r”(只读) 为了输入数据,打开一个文本文件 出错
“w”(只写) 为了输出数据,打开一个文本文件 创建一个新文件
“a”(追加) 向一个文本文件尾添加数据 创建一个新文件
“rb”(只读) 为了输入数据,打开一个二进制文件 出错
“wb”(只写) 为了输出数据,打开一个二进制文件 创建一个新文件
“ab”(追加) 向一个二进制文件尾添加数据 创建一个新文件
“r+”(读写) 为了读和写,打开一个文本文件 出错
“w+”(读写) 为了读和写,创建一个新文件 创建一个新文件
“a+”(读写) 打开一个文件,在文件尾进行读写 创建一个新文件
“rb+”(读写) 为了读和写,打开一个二进制文件 出错
“wb+”(读写) 为了读和写,新建一个新的二进制文件 创建一个新文件
“ab+”(读写) 打开一个二进制文件,在文件尾进行读写 创建一个新文件

#include 

int main()
{
    FILE* pFile;
    
    pFile = fopen("myfile.txt", "w");
    
    if (pFile != NULL)
    {
        fputs("fopen example", pFile);
        // fputs 函数将字符串 "fopen example" 写入到 pFile 所指向的文件中,且不会自动添加换行符
        
        fclose(pFile);
    }
    return 0;
}


3. 文件的顺序读写

3.1 顺序读写函数

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

上述适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)。

3.2 使用示例

3.2.1 fgetc 和 fputc

#include 

int main() 
{
    FILE *fp1, *fp2;
    int ch;

    // 打开源文件以读取
    fp1 = fopen("source.txt", "r");
    if (fp1 == NULL) 
    {
        perror("无法打开源文件");
        return 1;
    }

    // 打开目标文件以写入
    fp2 = fopen("destination.txt", "w");
    if (fp2 == NULL) 
    {
        perror("无法打开目标文件");
        fclose(fp1);
        return 1;
    }

    // 逐字符读取并写入
    while ((ch = fgetc(fp1)) != EOF) 
    // fgetc(指向用于读取的文件的文件指针)
    {
        fputc(ch, fp2);  // fputc 函数参数依次为 要写入文件的字符、文件指针
    }

    // 关闭文件
    fclose(fp1);
    fclose(fp2);

    return 0;
}

3.2.2 fgets 和 fputs

#include 

#define MAX_LENGTH 100
// 定义一个常量 MAX_LENGTH,用于指定字符数组 line 的最大长度

int main() 
{
    FILE *fp1, *fp2;
    char line[MAX_LENGTH];   // 定义一个字符数组 line,用于存储从源文件中读取的一行文本

    // 打开源文件以读取
    fp1 = fopen("source.txt", "r");
    if (fp1 == NULL) 
    {
        perror("无法打开源文件");
        return 1;
    }

    // 打开目标文件以写入
    fp2 = fopen("destination.txt", "w");
    if (fp2 == NULL) 
    {
        perror("无法打开目标文件");
        fclose(fp1);
        return 1;
    }

    // 逐行读取并写入
    while (fgets(line, MAX_LENGTH, fp1) != NULL) 
    // fgets 函数参数依次为 指向字符数组的指针、要读取的最大字符数、文件指针
    // fgets 函数从 fp1 所指向的文件中读取一行文本,最多读取 MAX_LENGTH - 1 个字符,剩下一个位置存储 '\0'
    // 读取的文本存储在 line 数组中,并在末尾添加字符串结束符 '\0'
    {
        fputs(line, fp2);  // fputs 函数参数依次为 指向 '\0' 结尾的字符串的指针、文件指针
    }

    // 关闭文件
    fclose(fp1);
    fclose(fp2);

    return 0;
}

3.2.3 fscanf 和 fprintf

#include 

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

int main() 
{
    FILE *fp;
    Student s = {1, "Alice", 85.5};

    // 打开文件以写入
    fp = fopen("students.txt", "w");
    if (fp == NULL) 
    {
        perror("无法打开文件");
        return 1;
    }

    // 格式化写入
    // fprintf 函数的参数依次是文件指针、格式化字符串、要写入的数据
    fprintf(fp, "%d %s %.2f", s.id, s.name, s.score);
    fclose(fp);

    // 打开文件以读取
    fp = fopen("students.txt", "r");
    if (fp == NULL) 
    {
        perror("无法打开文件");
        return 1;
    }

    // 格式化读取
    // 定义一个新的 Student 类型的变量 read_student,用于存储从文件中读取的学生信息
    Student read_student;
    // fscanf 函数的参数依次是文件指针、格式化字符串、要存储数据的变量的地址
    fscanf(fp, "%d %s %f", &read_student.id, read_student.name, &read_student.score);
    printf("ID: %d, Name: %s, Score: %.2f\n", read_student.id, read_student.name, read_student.score);

    fclose(fp);

    return 0;
}

3.2.4 fread 和 fwrite

#include 

typedef struct 
{
    int id;
    float value;
} Data;

int main() 
{
    FILE *fp;
    Data data = {1, 3.14};

    // 打开文件以二进制写入
    fp = fopen("data.bin", "wb");
    if (fp == NULL) 
    {
        perror("无法打开文件");
        return 1;
    }

    // 二进制写入
    fwrite(&data, sizeof(Data), 1, fp);
    // 第一个参数:指向要写入数据的指针,这里是 &data,表示 data 结构体的地址
    // 第二个参数:每个数据项的大小,这里使用 sizeof(Data) 来获取 Data 结构体的大小
    // 第三个参数:要写入的数据项的数量,这里是 1,表示写入一个 Data 结构体的数据
    // 第四个参数:文件指针,指定要写入的文件

    fclose(fp);

    // 打开文件以二进制读取
    fp = fopen("data.bin", "rb");
    if (fp == NULL) 
    {
        perror("无法打开文件");
        return 1;
    }

    // 二进制读取
    Data read_data;
    fread(&read_data, sizeof(Data), 1, fp);
    // 第一个参数:指向存储读取数据的缓冲区的指针,这里是 &read_data,表示 read_data 结构体的地址
    // 第二个参数:每个数据项的大小,同样使用 sizeof(Data) 获取 Data 结构体的大小
    // 第三个参数:要读取的数据项的数量,这里是 1,表示读取一个 Data 结构体的数据
    // 第四个参数:文件指针,指定要读取的文件

    printf("ID: %d, Value: %.2f\n", read_data.id, read_data.value);

    fclose(fp);

    return 0;
}

3.3 功能对比

fgetc 和 fputc 读取源文件的每个字符,写入到目标文件,直到文件结束。
fgets 和 fputs 读取源文件的每一行文本,写入到目标文件,直到文件结束。
fscanf 和 fprintf 针对结构化数据,上述是结构体。将一个结构体的信息以格式化的文本形式写入文件,然后从该文件中读取格式化的数据并存储到另一个结构体中。
fread 和 fwrite 针对二进制数据,上述是结构体。将一个结构体的二进制数据写入文件,然后再从该文件中读取二进制数据到另一个结构体中。

3.4 scanf/fscanf/sscanf 与 printf/fprintf/sprintf

函数 功能 适用场景
scanf 标准输入流(通常是键盘)读取格式化数据,按照指定的格式将输入数据存储到对应的变量中。 需要从键盘获取用户输入的格式化数据时
fscanf 指定的输入流(如文件流)读取格式化数据,按照指定的格式将数据存储到对应的变量中。 需要从文件中读取格式化数据时
sscanf 字符串中读取格式化数据,按照指定的格式将字符串中的数据提取并存储到对应的变量中。 需要从一个字符串中解析出特定格式的数据时
printf 将格式化的数据输出到标准输出流(通常是显示器)。 用于向用户显示格式化的信息
fprintf 将格式化的数据输出到指定的输出流(如文件流)。 需要将数据以特定格式写入文件
sprintf 将格式化的数据输出到字符串中,而不是输出到屏幕或文件。 需要将数据格式化为字符串以便后续处理时

3.4.1 sscanf 和 sprintf 使用示例

#include 

int main() 
{
    int age = 25;
    float height = 1.75;
    char name[] = "Alice";

    // 用于存储格式化后字符串的数组
    char buffer[100];

    // sprintf 函数的参数依次为 指向字符数组的指针、指定了输出格式的字符串(包含普通字符和格式说明符)、要输出的数据
    sprintf(buffer, "Name: %s, Age: %d, Height: %.2f", name, age, height);
    printf("格式化后的字符串: %s\n", buffer);

    // 定义变量用于存储从字符串中解析出的数据
    int parsed_age;
    float parsed_height;
    char parsed_name[20];

    // sscanf 函数的参数依次为 指向要从中读取数据的字符串的指针、指定了读取格式的字符串(包含普通字符和格式说明符)、要读取的数据
    sscanf(buffer, "Name: %19s, Age: %d, Height: %f", parsed_name, &parsed_age, &parsed_height);

    printf("解析后的信息:\n");
    printf("姓名: %s\n", parsed_name);
    printf("年龄: %d\n", parsed_age);
    printf("身高: %.2f\n", parsed_height);

    return 0;
}


4. 文件的随机读写

4.1 fseek

fseek 函数根据文件指针的当前位置和指定的偏移量,重新定位文件指针的位置,就好像在文本编辑器中移动光标到指定的位置一样。

#include 

int main ()
{
    FILE * pFile;
    pFile = fopen ( "example.txt" , "wb" );
    fputs ( "This is an apple." , pFile );

    fseek ( pFile , 9 , SEEK_SET );
    // fseek 函数的参数依次为 文件指针、偏移量、偏移量的起始位置

    fputs ( " sam" , pFile );
    fclose ( pFile );

    return 0;
}

4.2 ftell

ftell 函数获取当前文件指针相对于文件起始位置的偏移量。

通过调用 ftell 函数,可以知道当前文件指针在文件中的具体位置。

使用场景

  • 确定文件大小:将文件指针移动到文件末尾(使用 fseek 函数),然后调用 ftell 函数,返回的偏移量就是文件的大小(以字节为单位)。
  • 记录和恢复文件位置:在进行复杂的文件操作时,可能需要暂时记住文件指针的位置,之后再恢复到该位置继续操作。可以使用 ftell 记录位置,使用 fseek 恢复位置。
#include 

int main ()
{
    FILE * pFile;
    long size;
    pFile = fopen ("myfile.txt","rb");
    if (pFile==NULL)
        perror ("Error opening file");
    else
    {
        fseek (pFile, 0, SEEK_END);

        size = ftell (pFile);   // ftell(文件指针)

        fclose (pFile);
        printf ("Size of myfile.txt: %ld bytes.\n",size);
    }

    return 0;
}

4.3 rewind

调用 rewind 函数会将文件指针位置重置到文件的起始处。这意味着之后再对该文件进行读写操作时,将从文件的开头开始。

使用场景

  • 重复读取文件内容:当需要多次读取文件内容时,每次读取完后可以使用 rewind 函数将文件指针重置到起始位置,以便重新开始读取。
  • 在文件开头写入或修改内容:如果要在文件开头写入或修改一些信息,可先使用 rewind 函数将文件指针移到开头,再进行相应操作。
#include 

int main ()
{
    int n;
    FILE * pFile;
    char buffer [27];
    pFile = fopen ("myfile.txt","w+");

    // 使用 for 循环将大写字母 A 到 Z 写入文件
    for ( n='A' ; n<='Z' ; n++)
        fputc ( n, pFile);

    rewind (pFile);   // rewind (文件指针)

    fread (buffer,1,26,pFile);   // 文件指针已重置到文件的起始位置,读取文件的全部内容

    fclose (pFile);
    buffer[26]='\0';
    printf(buffer);

    return 0;
}


5. 文件读取结束的判定

feof 函数用于在文件读取结束后,判断结束原因是否为遇到文件尾。在文件读取时,不能直接用 feof 函数返回值判断文件是否结束。

  • 文本文件读取结束判断:用 fgetc 时看返回值是否为 EOF,用 fgets 时看返回值是否为 NULL
  • 二进制文件读取结束判断:判断返回值是否小于实际要读的个数。

5.1 文本文件读取结束的例子

#include 
#include    // 这里主要因为使用 EXIT_FAILURE 这个宏
// 它通常被定义为一个非零值,用于表示程序异常退出的状态码

int main(void)   // void 表示该函数不接受任何参数
{
    int c; // 注意:int,非 char
    // fgetc 读取失败或遇到文件结束时,都会返回 EOF
    // EOF 是一个宏,其值通常为 -1,char 类型无法表示 -1,而 int 类型可以

    FILE* fp = fopen("test.txt", "r");
    if(!fp) 
    {
        perror("File opening failed");
        return EXIT_FAILURE;
    }

    while ((c = fgetc(fp)) != EOF) 
    // 此为标准 C I/O 读取文件的循环
    // fgetc 读取失败或遇到文件结束时,都会返回 EOF
    // 读取的字符存于变量 c,若未到文件末尾(即 c 不等于 EOF),则循环继续
    {
        putchar(c);
    }

    // 判断文件读取结束是因为正常到达文件末尾,还是因为读取过程中发生 I/O(输入/输出)错误
    if (ferror(fp))
        puts("I/O error when reading");

    else if (feof(fp))
        puts("End of file reached successfully");

    fclose(fp);
}

  • ferror 函数主要用于检查文件流是否发生了 I/O 错误。若 ferror 函数返回非零值,说明文件流在读取过程中发生了 I/O 错误。
  • perror 函数用于输出系统调用的错误信息,结合 errno 打印具体的错误描述。
    perror( NULL 或空字符串 );   // 仅打印错误信息
    perror("File opening failed");
    // 或者 perror("打开文件失败");
    // 若文件打开失败,输出 双引号中内容:具体错误描述
  • 两者可结合使用,在文件操作中先使用 ferror 检查文件流错误,若系统调用失败则使用 perror 输出详细错误信息。

5.2 二进制文件读取结束的例子

#include 
enum { SIZE = 5 };   // 定义一个枚举常量 SIZE

int main(void)
{
    double a[SIZE] = {1.,2.,3.,4.,5.};
    FILE *fp = fopen("test.bin", "wb"); 
    if (fp == NULL) 
    {
        perror("Failed to open file for writing");
        return 1;
    }

    fwrite(a, sizeof *a, SIZE, fp); 
    fclose(fp);

    double b[SIZE];
    fp = fopen("test.bin","rb");
    if (fp == NULL) 
    {
        perror("Failed to open file for reading");
        return 1;
    }
    size_t ret_code = fread(b, sizeof *b, SIZE, fp); 

    if(ret_code == SIZE) 
    {
        puts("Array read successfully, contents: ");
        for(int n = 0; n < SIZE; ++n)
            printf("%f ", b[n]);
        putchar('\n');
    } 
    else 
    { 
        if (feof(fp))
            printf("Error reading test.bin: unexpected end of file\n");
        
        else if (ferror(fp)) 
        {
            perror("Error reading test.bin");
        }
    }

    fclose(fp);
}


6. 文件缓冲区

ANSI C 标准使用“缓冲文件系统”处理数据文件。系统会自动在内存为每个正在使用的文件开辟“文件缓冲区”。

从内存向磁盘(文件)输出数据时,先存入缓冲区,满后再一起写入磁盘;

从磁盘向计算机读入数据时,先将数据读入缓冲区,填满后,再逐个送至程序数据区(如程序变量)。

缓冲区大小由 C 编译系统决定。

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

  • 手动刷新缓冲区:使用fflush函数强制将缓冲区数据写入文件。

  • 自动刷新缓冲区:调用fclose函数关闭文件时,会自动刷新缓冲区。

// VS2022 ,Windows 11 环境测试

#include 
#include    // 这里主要因为使用 Sleep 函数

int main()
{
    FILE*pf = fopen("test.txt", "w");
    fputs("abcdef", pf); // 先将代码放在输出缓冲区
    printf("睡眠 10 秒 - 已经写数据了,打开 test.txt 文件,发现文件没有内容\n");   // 由于数据还在缓冲区,文件中不会有内容
    Sleep(10000);   // 程序暂停执行 10000 毫秒(即 10 秒),给用户时间打开 test.txt 文件查看
    printf("刷新缓冲区\n");
    fflush(pf); // 刷新缓冲区时,才将输出缓冲区的数据写到文件
    // 注:fflush 在高版本的 VS 上不能使用了

    printf("再睡眠 10 秒 - 此时,再次打开 test.txt 文件,文件有内容了\n");
    Sleep(10000);

    fclose(pf);

    pf = NULL;
    return 0;
}

你可能感兴趣的:(c语言,开发语言)