【C语言】文件操作

目录

1 什么是文件

2 文件的打开和关闭

 2.1 文件指针

 2.2 文件的打开和关闭

3 文件的顺序读写

3.1 fgetc(字符输入函数)

3.2 fputc(字符输出函数)

3.3 fgets (文本行输入函数)

3.4 fputs(文本行输出函数)

3.5 fscanf(格式化输入函数)

3.6 fprintf(格式化输出函数)

3.7 fread(二进制输入函数)

3.8 fwrite (二进制输出函数)

3.9 sscanf(字符串输入函数)

3.10 sprintf(字符串输出函数)

4 文件的随机读写

4.1 fseek(文件指针重定位函数)

4.2 ftell

4.3 rewind

5 文本文件和二进制文件

6 文件读取结束的判定

6.1 feof的错误使用

6.2 feof的正确使用

6.3 关于feof的实现

7 文件缓冲区


前    言

当我们想实现数据持久化,可以采用将数据存放在磁盘文件、或者数据库中等方式。

接下来将要介绍的内容即是如何通过C语言进行文件相关操作以实现数据持久化。

1 什么是文件

要操作文件,首先要了解什么是文件。磁盘上的文件就是文件。在程序设计中,从文件功能角度来看通常将文件分为两种:程序文件数据文件。

程序文件

①源程序文件(后缀为.c)

②目标文件(windows环境后缀为.obj)

③可执行程序(windows环境后缀为.exe)

数据文件

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

接下来我们所要讨论的操作对象即为数据文件。

在以往我们使用类似scanf、printf等函数所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,将运行结果显示到显示器上。但除此之外,我们还可以将信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这时所处理数据的输入输出则是以磁盘上文件为对象的。

既然要操作文件,那就要清楚我们要操作的是哪一个文件,而区分文件,依靠的则是文件的唯一标识。文件标识包含3个部分:文件路径+文件名主干+文件后缀。为了方便起见,文件标识也常被称为文件名,因此要注意之后所说的文件名不仅仅是我们常看到的文件的一个单独的名字。

2 文件的打开和关闭

以我们平常操作的word文件来说,文件的操作流程:在文件所属路径下打开文件->在文件中进行内容的增删改查->关闭文件。

 2.1 文件指针

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

例如,在VS2013编译环境提供的stdio.h头文件中就有以下的文件类型声明:

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结构的变量以方便使用。

FILE* pf;  //文件指针变量

这里定义的pf即是一个指向FILE类型数据的指针变量。如下图所示,当使pf指向某个文件的文件信息区(如上所说是一个结构体变量),通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件

 2.2 文件的打开和关闭

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

ANSIC规定使用fopen函数来打开文件,fclose来关闭文件,如下为两个函数的声明:

//打开文件
FILE * fopen ( const char * filename, const char * mode );
其中:
①filename为文件的名字;
②mode表示打开文件的方式;
③FILE*表示在调用函数打开文件的同时,会返回一个 FILE*的指针变量指向该文件,即相当于建立了指针和文件的关系,如果文件打开失败则会返回一个空指针。
//关闭文件
int fclose ( FILE * stream );
其中:
①stream(流)为指向所要关闭文件的文件指针;
②int表示函数返回值类型,如果文件关闭成功则返回0,关闭失败则返回EOF(-1)。

文件的打开方式:

文件使用方式含义

如果指定文件不存在

"r"(只读)为了输入数据,打开一个已经存在的文本文件出错
"w" (只写)
为了输出数据,打开一个文本文件
建立一个新文件
"a" (追加)
向文本文件尾添加数据
建立一个新文件
"rb"(只读)
为了输入数据,打开一个二进制文件
出错
"wb"(只写)
为了输出数据,打开一个二进制文件
建立一个新文件
"ab"(追加)
向一个二进制文件尾添加数据
出错
"r+"(读写)
为了读和写,打开一个文本文件
出错
"w+"(读写)
为了读和写,建议一个新的文件
建立一个新文件
"a+"(读写)
打开一个文件,在文件尾进行读写
建立一个新文件
"rb+"(读写)
为了读和写打开一个二进制文件
出错
"wb+"(读写)
为了读和写,新建一个新的二进制文件
建立一个新文件
"ab+"(读写)
打开一个二进制文件,在文件尾进行读和写
建立一个新文件

对于任意一个C程序,只要运行起来,就默认打开三个流:

stdin — 标准输入流 — 键盘

stdout — 标准输出流 — 屏幕

stderr — 标准错误流 — 屏幕

以上这些流都是FILE*型的。

以下为文件打开和关闭的一个示例:

#include <stdio.h>
int main ()
{
         FILE * pFile;
         //打开文件
         pFile = fopen ("myfile.txt","w");
         //文件操作
         if (pFile!=NULL)
        {
                fputs ("fopen example",pFile);  //文件写入
                //关闭文件
                fclose (pFile);
                pFile = NULL;
        }
        return 0;
}

3 文件的顺序读写

以下是一些常用的输入输出函数,通过这些函数我们可以实现文件的顺序读写。

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

3.1 fgetc(字符输入函数)

从流中获取字符

函数声明:

int fgetc (FILE* stream);

说明:

①函数返回指定流的内部文件位置指针当前指向的字符(提升为int型),接着文件内部文件指针指向下一个字符;

②如果调用函数时,文件内部指针已经指向文件末尾,则函数返回EOF并为流设置文件末尾指示符(feof:检查关联文件的文件结束指示符)

③如果读取发生错误,函数返回EOF,并为流设置错误指示符(ferror:检查是否设置了与流关联的错误指示符,否返回0,是返回非0值)

示例:

/* fgetc example: money counter */

//读取文件内容,计算文件中有多少个'*'字符
#include <stdio.h>
int main()
{
    FILE* pFile;
    int c;
    int n = 0;
    pFile = fopen("myfile.txt", "r");
    if (pFile == NULL) perror("Error opening file");
    else
    {
        do {
                      c = fgetc(pFile);
                      if (c == '*') n++;
        } while (c != EOF);
        fclose(pFile);

        pFile = NULL;
        printf("The file contains %d '*'.\n", n);
    }
    return 0;
}

3.2 fputc(字符输出函数)

向流中写入一个字符

函数声明:

int fputc (int character, FILE* stream);

说明:

①将要写入的字符提升为int型,写入时再在内部转换为无符号字符。字符写在内部位置指针指向的位置,成功写入后指针指向下一个位置。

②写入成功,则返回写入的字符;如果写入错误,则返回EOF并设置文件错误指示符。

示例:

/* fputc example: alphabet writer */

//向alphabet.txt文件中写入A-Z的26个大写字母
#include <stdio.h>

int main()
{
    FILE* pFile;
    char c;

    pFile = fopen("alphabet.txt", "w");
    if (pFile != NULL) {

        for (c = 'A'; c <= 'Z'; c++)
                fputc(c, pFile);

                fclose(pFile);

                pFile = NULL;
    }
    return 0;
}

3.3 fgets (文本行输入函数)

从流中获取字符串

函数声明:

char* fgets (char* str, int num, FILE* stream);

说明:

①从流中读取(num - 1)个字符到str指向的字符数组(字符串)中,并返回指向该字符串的指针。

②如果遇到换行符'\n'或者文件结尾,则停止读取,但函数会将换行符'\n'视为有效字符读取到str指向的字符串中。

③文件读取完成后,函数自动在str指向的字符串中读取的最后一个字符后添加'\0'。

>读取成功,函数返回str;

>若在读取字符时遇到文件结尾,则函数返回str并设置文件结尾指示符;

>若函数在一开始读取时就遇到文件结尾,则函数返回一个空指针(str指向的字符串中的原有内容保持不变);

>若发生读取错误,则设置错误指示符并返回空指针(此时str指向的字符串可能已更改)。

示例:

/* fgets example */

//从文件中读取前99个字符或第一行(以先到者为准)
#include <stdio.h>

int main()
{
    FILE* pFile;
    char mystring[100];

    pFile = fopen("myfile.txt", "r");
    if (pFile == NULL) perror("Error opening file");
    else {
        if (fgets(mystring, 100, pFile) != NULL)
                puts(mystring);
                fclose(pFile);

                pFile = NULL;
    }
    return 0;
}

3.4 fputs(文本行输出函数)

将字符串写入流

函数声明:

int fputs (const char* str, FILE* stream);

说明:

①函数将str指向的字符串写入流。

②函数起始地址str开始将字符复制到流中,直到遇到'\0'终止,且'\0'不会被写入到流中。

③fputs与puts函数不同之处不仅在于fputs可以指定目标流,而且fputs不会写入额外的字符,而puts函数会在写入结束后自动在末尾附加一个换行符。

④写入成功时,函数将返回一个非负值(成功写入的字符数);若发生写入,函数将返回EOF并设置错误指示。

示例:

/* fputs example */

//可从键盘读取255个字符到字符数组中

//程序每执行一次就写入一行数据到流中,且每次只能写入输入的第一行数据,并不会自动换行,需要自行添加换行符
#include <stdio.h>

int main()
{
    FILE* pFile;
    char sentence[256];

    printf("Enter sentence to append: ");
    fgets(sentence, 256, stdin);
    pFile = fopen("mylog.txt", "a");
    fputs(sentence, pFile);
    fclose(pFile);

    pFile = NULL;
    return 0;
}

3.5 fscanf(格式化输入函数)

从流中读取格式化数据

函数声明:

int fscanf (FILE* stream, const char* format, ...);

说明:

①省略号代表附加参数,函数从流中读取数据并根据参数格式将数据分别存储到附加参数指向的位置。

②函数读取非空白字符,并忽略在下一个非空白字符之前遇到的任何空白字符(包括空格、换行、制表符)。

③若使用函数从流中读取字符串时,遇到空格和换行符时就会结束对当前字符串的格式说明符的匹配读取,并开始匹配下一个格式说明符对应的数据。

>读取成功时,函数将返回成功读取到附加参数指向位置的项目数;如果在有成功读取数据的前提下发生读取错误或到达文件末尾,函数将返回小于预期的计数值(甚至为0)。

>若发生读取错误,函数将设置错误指示符;或者读取时达到文件末尾,函数将设置文件末尾指示符,而如果在未成功读取到任何数据之前就发生这两种情况的话,函数将返回EOF。

示例:

/* fscanf example */

//先按照一定的格式向文件中写入数据

//再按照一定的格式从文件中读取数据并写入到屏幕上显示
#include <stdio.h>

int main()
{
    char str[80];
    float f;
    FILE* pFile;

    pFile = fopen("myfile.txt", "w+");
    fprintf(pFile, "%f %s", 3.14, "PI");
    rewind(pFile);
    fscanf(pFile, "%f", &f);
    fscanf(pFile, "%s", str);
    fclose(pFile);

    pFile = NULL;
    printf("I have read: %f and %s \n", f, str);
    return 0;
}

3.6 fprintf(格式化输出函数)

向流中写入格式化数据

函数声明:

int fprintf (FILE* stream, const char* format, ...);

说明:

①函数将format指向的字符串写入指定流中,如果该字符串中包含格式说明符,则附加参数将被格式化并插入到结果字符串中以替换其各自的说明符,因此函数至少需要有与format指向的字符串中的格式说明符对应的附加参数。

②写入成功时,函数将返回成功写入的字符总数

③如果发生写入错误,函数将设置错误指示符并返回一个负数。

示例:

/* fprintf example */

//从键盘输入三次名字,并将名字以固定长度写入指定文件中
#include <stdio.h>

int main()
{
    FILE* pFile;
    int n;
    char name[100];

    pFile = fopen("myfile.txt", "w");
    for (n = 0; n < 3; n++)
    {
        puts("please, enter a name: ");
        gets(name);

        //%-10.10s表示左对齐,最少10个字符,最多10个字符
        fprintf(pFile, "Name %d [%-10.10s]\n", n + 1, name);
    }
    fclose(pFile);

    pFile = NULL;

    return 0;
}

3.7 fread(二进制输入函数)

从流中读取数据块

函数声明:

size_t fread (void* ptr, size_t size, size_t count, FILE* stream);

说明:

①从指定流中读取包含count个元素的数组,每个元素的大小为size个字节,并将读取到的数据存储到ptr指向的内存块中。

②流的内部位置指针按照读取的字节总数(size * count)前进。

③读取成功,函数将返回成功读取的元素总数。

④如果size或count为0,则函数返回0并且流的状态与ptr指向的内容都保持不变。

⑤如果读取时发生读取错误或到达文件末尾,函数将返回与预期计数不同的值,并设置对应的错误指示符和文件末尾指示符(可通过ferror和feof函数检查)。

示例:

/* fread example: read an entire file */
#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE* pFile;
    long lSize;
    char* buffer;
    size_t result;

    pFile = fopen("myfile.bin", "rb");
    if (pFile == NULL) { fputs("File error", stderr); exit(1); }

    // obtain file size
    fseek(pFile, 0, SEEK_END);
    lSize = ftell(pFile);
    rewind(pFile);

    // allocate memory to contain the whole file
    buffer = (char*)malloc(sizeof(char) * lSize);
    if (buffer == NULL) { fputs("Memory error", stderr); exit(2); }

    // copy the file into the buffer
    result = fread(buffer, 1, lSize, pFile);
    if (result != lSize) { fputs("Reading error", stderr); exit(3); }

  

    /* the whole file is now loaded in the memory buffer. */

    // terminate
    fclose(pFile);

    pFile = NULL;
    free(buffer);

    buffer = NULL;
    return 0;
}

3.8 fwrite (二进制输出函数)

向流中写入数据块

函数声明:

size_t fwrite (const void* ptr, size_t size, size_t count, FILE* stream);

说明:

①函数将ptr指向的内存块中的包含count个元素的数组写入指定流中,每个元素的大小为size个字节,并且流的内部位置指针按照读取的字节总数(size * count)前进。值得注意的是函数直接将内存中存储的二进制数据写入流中,所以当我们用记事本打开文件时,往往会显示乱码,但这是正常的。只有用对应的二进制编码器才能看到实际写入的数据。

②写入成功,函数将返回成功写入的元素个数。

③若发生写入错误,函数将设置错误指示符,此时函数返回值将不同与预期的写入计数。

④如果size或count为0,则函数返回0并且错误指示符保持不变。

示例:

/* fwrite example : write buffer */

//将缓冲区中的xyz三个字符写入文件中

#include <stdio.h>

int main()
{
    FILE* pFile;
    char buffer[] = { 'x' , 'y' , 'z' };
    pFile = fopen("myfile.bin", "wb");
    fwrite(buffer, sizeof(char), sizeof(buffer), pFile);
    fclose(pFile);

    pFile = NULL;
    return 0;
}

3.9 sscanf(字符串输入函数)

从字符串中读取格式化数据

函数声明:

int sscanf (const char* s, const char* format, ...);

说明:

①函数从字符串中读取数据并根据参数格式将其存储到附加参数给定的位置,与scanf函数不同的是,sscanf函数是从字符串中读取数据,scanf函数是从标准输入流stdin中读取数据。

②读取成功时,函数将返回成功读取以替换说明符的项目数。

③若在有成功读取数据的前提下发生读取错误,函数将返回小于预期的计数值(甚至为0)。

④若在无任何数据读取成功时就读取失败,函数将返回EOF。

示例:

/* sscanf example */
#include <stdio.h>

int main()
{
    char sentence[] = "Lily is 12 years old";
    char str[20];
    int i;

    sscanf(sentence, "%s %*s %d", str, &i);
    printf("%s -> %d\n", str, i);

    return 0;
}

输出结果:Lily -> 12

3.10 sprintf(字符串输出函数)

将格式化数据写入字符串

函数声明:

int sprintf (char* str, const char* format, ...);

说明:

①函数将format指向的字符串写入指定字符串中,如果该字符串中包含格式说明符,则附加参数将被格式化并插入到结果字符串中以替换其各自的说明符,因此函数至少需要有与format指向的字符串中的格式说明符对应的附加参数。

②函数将在字符串写入完成后自动添加'\0'。

③写入成功,函数将返回成功写入的字符总数,此计数不包括自动添加在字符串末尾的'\0'。

④写入失败,则函数返回一个负数。

示例:

/* sprintf example */

//将字符串"5 plus 3 is 8"写入buffer中并输出,同时输出成功写入的字符总数
#include <stdio.h>

int main()
{
    char buffer[50];
    int n, a = 5, b = 3;
    n = sprintf(buffer, "%d plus %d is %d", a, b, a + b);
    printf("[%s] is a string %d chars long\n", buffer, n);
    return 0;
}

输出结果:[5 plus 3 is 8] is a string 13 chars long

4 文件的随机读写

上述我们提到的函数都是按照顺序进行相关读写操作的,那有没有什么办法可以从文件的任意位置开始进行读写呢?其实在上面的内容中我们可以发现文件的读写位置与文件内部指针有关,那如果可以改变文件内部指针的位置,使其指向我们想要进行读写的位置,是不是就可以实现文件的随机读写了?没错,C语言为我们提供了以下函数可操作文件内部指针以实现文件的随机读写。

4.1 fseek(文件指针重定位函数)

重新定位文件指针

函数声明:

int fseek (FILE* stream, long int offset, int origin);

说明:

①若成功,则文件指针指向的新位置为起始位置origin加上偏移量offset后所在的位置,其中偏移量是以字节为单位的,偏移量为正值表示指针相对起始位置前进,偏移量为负值表示指针相对起始位置后退。这里需要注意的是对于存在汉字这样超过一个字节的文本文件,若指针定位在汉字前个字节位置,这时对文件进行写入可能会破坏文件原有的不期望被修改的内容。

②origin的取值需在以下范围中:

                        SEEK_SET  —  文件开头

                        SEEK_CUR  —  文件指针的当前位置

                        SEEK_END  —  文件结束

③若成功,函数返回0;否则返回非0值。

示例:

/* fseek example */
#include <stdio.h>

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

    //从文件开头向后偏移9个字节的位置(字符n处)开始写入" sam"
    fseek(pFile, 9, SEEK_SET);
    fputs(" sam", pFile);
    fclose(pFile);

    pFile = NULL;
    return 0;
}

执行结果(文件内容):

未使用fseek函数时:This is an apple. sam

使用fseek函数时:This is a sample.

4.2 ftell

获取当前文件指针位置

函数声明:

long int ftell (FILE* stream);
说明:

①成功时,函数返回文件指针当前指向位置相对于起始位置的偏移量(以字节为单位)。对于文本流来说,返回值可能没有意义(例如文本文件中存在汉字,而一个汉字占两个字节,如果返回的是指针刚好指向某个汉字前一个字节的位置时的偏移量,那这个返回值即是无意义的)。

②失败时,函数返回-1L,并将errno设置为系统特定的正值。

示例:

/* ftell example : getting size of a file */

//程序以字节为单位输出myfile.txt中内容的大小
#include <stdio.h>

int main()
{
    FILE* pFile;
    long size;

    pFile = fopen("myfile.txt", "rb");
    if (pFile == NULL) perror("Error opening file");
    else
    {
        fseek(pFile, 0, SEEK_END);   // non-portable
        size = ftell(pFile);
        fclose(pFile);

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

4.3 rewind

使文件指针位置回到文件内容的起始位置

函数声明:

void rewind (FILE* stream);

说明:

①函数无返回值,成功调用函数后,与文件相关的文件结束标志和错误标志将被清除。

示例:

/* rewind example */

//进行数据写入后文件指针会位于文件的末尾,此时通过rewind函数将文件指针置回文件开头

//将26个大写字母读取到字符数组buffer中并输出到屏幕上。

#include <stdio.h>

int main()
{
    int n;
    FILE* pFile;
    char buffer[27];

    pFile = fopen("myfile.txt", "w+");
    for (n = 'A'; n <= 'Z'; n++)
        fputc(n, pFile);
    rewind(pFile);
    fread(buffer, 1, 26, pFile);
    fclose(pFile);

    pFile = NULL;
    buffer[26] = '\0';
    puts(buffer);
    return 0;
}

5 文本文件和二进制文件

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

数据在外存中的存储方式:

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

以整数10000为例,如下图所示,如果以ASCII形式输出到磁盘,则其在磁盘中占用5个字节(每个数字字符一个字节);而以二进制形式输出,则其在磁盘上只占4个字节。

测试代码如下:

#include <stdio.h>

int main()
{
    int a = 10000;
    FILE* pf = fopen("test.txt", "wb");
    fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
    fclose(pf);
    pf = NULL;
    return 0;
}

使用VS中的二进制编码器查看测试结果:

6 文件读取结束的判定

检查文件结束指示符

声明:

int feof (FILE* stream);

说明:

①函数检查是否设置了文件结束指示符,如果是,则返回一个不同于0的值;否则,返回0。该文件结束指示符通常是根据先前对流的操作是否有试图读取文件末尾或超过文件末尾来设置的。这里要注意的是,即使当前文件指针指向的是文件结尾后的位置,但如果没有对其进行读取的话,文件结束指示符可能也不会设置,直到有操作尝试读取这个超出文件结尾位置上的内容,发现此时已到达文件结尾时才会设置文件关联的文件结束指示符。

基于以上特性,这里需要强调的是:在文件读取过程中,不能用feof函数的返回值直接判断文件是否结束。而应当用于文件读取结束的时候,判断是读取失败结束,还是遇到文件结尾结束。

6.1 feof的错误使用

以下举一个直接错误使用feof函数返回值来直接判断文件是否结束的例子:

//错误示例

#include <stdio.h>

//下面代码原目的是为了打印写入到test.txt文件中的abcd四个字母字符

int main()
{
    char ch;
    int n;
    FILE* pf = fopen("test.txt", "w+");
    for (n = 'a'; n <= 'd'; n++) {
        fputc(n, pf);
    }
    rewind(pf);
    while (!feof(pf)) {
        ch = fgetc(pf);
        printf("%d ", ch);
    }
    fclose(pf);
    pf = NULL;
    return 0;
}

代码运行结果:

 分析:基于fgetc函数在遇到文件结尾而读取结束的时候回返回EOF(-1),而-1不属于ASCII码值的范围,无法显示在屏幕上,因此为了更方便观察打印情况,这里以%d的形式打印fgetc的返回值(读取成功返回读取到的字符,失败或结尾返回EOF)。可以看到,输出结果除了预期的abcd四个字母的ASCII码值外,还多了一个-1(即EOF)。如下图所示,根据一开始说明的函数特性,当文件指针位于文件结尾(位置1)时,文件结束指示符还未被设置,当读取结尾最后一个字符d后,文件指针前进一位(位置2),此时若没有读取,文件指示符也是依旧没有被设置的,所以feof(pf)返回值还是0,循环条件!feof(pf)为真,依然可以进入循环。当在循环中再次执行读取操作时,发现读取超出文件结尾了,此时函数fgetc返回了EOF并打印,并且设置了文件结束指示符,循环结束。

结论: 当使用feof函数的返回作为循环条件判断文件是否到结尾,且将对文件的读取操作设置在此循环中时,可能会返回非预期的结果(EOF)。因此再次强调,在文件读取过程中,不要使用feof函数的返回值来直接判断文件是否结束。

6.2 feof的正确使用

那既然不能用feof函数的返回值来直接判断文件是否结束,那应该怎么判断文件是否结束呢?根据上述介绍的文件读取函数,可以将文件结束判断方式分为两类:

①判断文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)

例如:

>fgetc判断是否为EOF

>fgets判断是否为NULL

②二进制文件读取结束的判断,判断返回值是否小于实际要读的个数

例如:

>fread判断返回值是否小于实际要读的个数,只有当读取个数等于预期计数时才是读取成功

基于这两类情况,以下举两个feof函数正确使用的例子:

对于文本文件:

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int c; // 注意:int,非char,要求处理EOF
    FILE* fp = fopen("test.txt", "r");
    if (!fp) {
        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;
}

对于二进制文件:

#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
    double a[SIZE] = { 1.,2.,3.,4.,5. };
    FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式
    fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
    fclose(fp);
    double b[SIZE];
    fp = fopen("test.bin", "rb");
    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);
}

6.3 关于feof的实现

在cplusplus.com中对feof函数的解释是:函数用来检查文件结束指示符。那这个文件结束指示符究竟是什么?

经过查阅资料显示,在VS2013编译环境提供的stdio.h头文件(这里使用的VS2022编译环境,已经看不到这些声明了)中,对feof有如下宏定义:

//VS2013编译环境下stdio.h头文件中

//宏定义

#define feof(__F) ((__F)->_flag & _IOEOF)
#define _IOEOF 0x0010 /* EOF reached on read */

//声明

_CRTIMP int __cdecl__MINGW_NOTHROW    feof (FILE* stream);

//文件类型声明

struct _iobuf {
       char *_ptr;
       int   _cnt;
       char *_base;
       int   _flag;
       int   _file;
       int   _charbuf;
       int   _bufsiz;
       char *_tmpfname;
      };
typedef struct _iobuf FILE;

可以发现:feof的实现关键代码是:(pFile->_flag & 00010000),且主要与文件类型中的_flag成员有关。即当经过读取操作判定文件读取到文件末尾时,_flag变量的值会设置为第5个比特位为1的值,此时feof返回非0值,代表文件读取到文件尾结束。

基于此,这里猜测_flag或许就是VS2013编译环境下所谓的文件结束指示符(_flag实际上是否是文件结束指示符这里没有再进行探究)。

由于在VS2022编译环境提供的stdio.h头文件中只能看到对feof及FILE的声明如下:

//feof

_ACRTIMP   int __cdecl   feof( _In_   FILE*   _Stream);

//FILE

typedef struct _iobuf
{
    void* _Placeholder;
} FILE;

在该头文件中已经看不到一些具体的定义了,也没有办法确定该环境下的文件结束指示符的定义情况。这里作者根据VS2013编译环境提供的stdio.h头文件中对feof与FILE的定义情况,猜测当前环境下在文件指针pf指向的内存空间中有某个区域是用来存储所谓文件结束指示符的值的,当这个值发生变化,即所谓设置了文件结束指示符,feof就会根据其判断文件读取已到达末尾。为了验证猜测,这里通过以下代码进行测试:

#include <stdio.h>

int main()
{
    char ch;
    int n = 0;
    int ret = 0; //用于观察feof返回值
    FILE* pf = fopen("test.txt", "w+");
    for (n = 'a'; n <= 'd'; n++) {
        fputc(n, pf);
    }
    rewind(pf);
    while ((ch = fgetc(pf)) != EOF) {

        //*((int*)pf + 3) = 0x204d; //测试代码,第二次调试前添加
        ret = feof(pf);
        printf("%d ", ch);
    }
    ret = feof(pf);
    fclose(pf);
    pf = NULL;
    return 0;
}

运行代码,在调试窗口中观察变量和pf指向的内存空间的变化情况:

 如图所示:

这里观察到在pf指向的内存空间中有一块区域的值(后面简称区域值,特指这块空间存储的值),在读取过程中没有发生变化(保持0x2045-小端字节序),且此时feof的返回值也保持为0;而当文件读取返回-1时,此时文件到达了末尾,观察区域的值突然变为了0x204d,且此时feof的返回值变为了非0值1。因此猜测这块区域存储的内容会影响feof的返回值(即这块区域可能是所谓文件结束指示符的存储空间)

为了验证,这里在原代码的基础上添加*((int*)pf + 3) = 0x204d;表示在只读取了一个字符的时候就将上述观察区域中的值强行修改为0x204d,这时运行发现即使还未读取到文件末尾,feof的返回值也变成了非0值1。

那是不是执行任何形式读取操作的时候,达到文件末尾时,这个观察区域的值都是0x204d呢?这里作者又测试了二进制读取形式(fread)和格式化读取数据形式(fscanf),发现这个值始终是0x204d;为了更全面的观察,作者在保证文件存在且数据依旧为abcd的前提下,又测试了以只读模式打开和以读写模式打开两种情况下的区域值(这里没有展示出来,感兴趣的读者也可以自行测试一下),发现:区域值有了变化—以只读模式打开文件时,当文件读取到末尾时,区域值为0x2049;以读写模式打开文件时,当文件读取到末尾时,区域值为0x204d。

通过以上测试推出:文件读取到达末尾时的区域值与读取形式无关,与文件打开模式有关。而区域值影响着feof的返回值,通过文件指针pf找到这块区域并将其中存储的值修改为(只读:0x2049; 读写:0x204d),可使feof在文件未读取到末尾时就返回非0值。即符合猜测(这块区域可能是所谓文件结束指示符的存储空间)。

说明:本节关于feof实现的相关论述均为个人测试后所得,至于实际是否如此,未曾探究。

7 文件缓冲区

ANSIC标准采用“缓冲文件系统”来处理数据文件,所谓缓冲文件系统是指系统自动的在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先将数据送到内存中的输出缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向内存中读入数据,则从磁盘文件读取数据输入到内存中的输入缓冲区,充满缓冲区后再从缓冲区逐个将数据送到程序数据区(程序变量等)。缓冲区的大小是根据C编译系统决定的。

 以下是一段测试代码:

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

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

以上是我对文件操作相关知识的一些学习记录总结,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大米饭_Mirai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值