fwrite和fread的破皮探讨

好的,第一篇博文

灵感来源于《C缺陷与指针》里的一小段玩意,突然有兴趣就一直扣下去了

向下兼容也叫向后兼容(Backwards Compatibility)

百度后的大概意思就是:发行了一个程序的新版本后,即便用旧版本程序创建的文档/系统仍然能被新版本程序使用,就好像用Powerpoint1997做的ppt仍然能够被Powerpoint2003编辑一样。

在询问大佬后,得知在C语言中旧的C标准、C库中对于每个FILE对象不能同时进行读写操作,否则会引起文件的偏移量混乱。为了保持兼容旧标准,我们写的代码也有必要保证FILE对象不能同时进行读写操作。下面是书上的一段代码和解释(这两段代码的fseek使用似乎都有问题)

FILE *fp;
 2 struct record rec;
 3 ...
 4 while(fread((char *)&rec , sizeof(rec) , 1 , fp) == 1)
 5 {
 6     /*对rec执行某些操作*/
 7     if(/*rec必须被重新写入*/8         {
 9         fseek(fp , -(long)sizeof(rec) , 1);
10         fwrite( (char *)&rec , sizeof(rec) , 1 ,fp);
11      //fseek(fp , 0L .1);
12         }
13 }
书内解释:

这段代码看上去毫无问题:&rec在传入fread和fwrite函数时被小心翼翼地转换为字符指针类型,sizeof(long)被转换为长整型(fseek函数要求第二个参数是long类型,因为int类型的整数可能无法包含一个文件的大小;sizeof返回一个unsigned值,因此首先必须将其转换为有符号类型才有可能将其反号)。但这段代码仍然可能运行失败,而且出错的方式非常难于察觉。

问题出在:如果一个记录需要被重新写入文件,也就是说,fwrite函数得到执行,对这个文件执行的下一个操作将是循环开始的fread函数。因为在fwrite函数调用与fread函数调用之间缺少了一个fseek的函数调用,所以无法进行上述操作,解决方法是把注释中代码加上去。第二个fseek代码看上去什么也没做,但它改变了文件的状态。

以上是书内解释

那么问题来了,为什么就是不能同时R/W,交叉R/W就会引起文件偏移量混乱呢?

这涉及到fwrite和fread的内部运作原理,以及fseek的作用。

 

今天先探讨fwrite和fread的内部运作原理。下面是一个CSDN上的问题及回答,很详细。

因为他讲得很好我不认为我能讲得更好,我进行了部分的摘抄,如果有版权问题请通知我一下我马上处理,这是原址

#include <stdio.h>

int main()
{
    FILE *testFile = NULL;
    int ret = 0;
    char string[200] = {'\0'};

    testFile=fopen("test1.txt", "rb+"); //test1.txt的内容是abcdefghijklmnopqrstuvwxyz
    if(testFile == NULL)
    {
        return 0;
    }
    ret = fread(string, 1, 10, testFile);
    //ret = fseek(testFile, 0, SEEK_CUR);
    ret = fwrite("hello world", 1, 11, testFile);
    fclose(testFile); //test1.txt的内容还是abcdefghijklmnopqrstuvwxyz
    //如果fread和fwrite之间调用了fseek,文件内容变成abcdefghijhello worldvwxyz

    testFile=fopen("test2.txt", "rb+"); //test2.txt的内容也是abcdefghijklmnopqrstuvwxyz
    if(testFile == NULL)
    {
        return 0;
    }
    ret = fwrite("hello world", 1, 11, testFile);
    //ret = fseek(testFile, 0, SEEK_CUR);
    ret = fread(string, 1, 10, testFile);
    fclose(testFile); //test2.txt的内容变成hello worldヘヘヘヘヘヘヘヘヘヘvwxyz
    //如果fread和fwrite之间调用了fseek,文件内容变成hello worldlmnopqrstuvwxyz

    return 0;
}

回答大概如下:

程序和文件的交互都是通过缓存在完成的。fread和fwrite也是通过操作缓存(如果设置了缓存的话)来完成的

那fwrite、fread是如何实现这种交互的呢。这三者都是通过对FILE结构进行操作的,先看看FILE

struct _iobuf {
        char *_ptr;//文件缓存的当前位置
        int   _cnt;//缓存里可以读取的字节数
        char *_base;//文件缓存的起始位置
        int   _flag;
        int   _file;
        int   _charbuf;
        int   _bufsiz;//缓存大小
        char *_tmpfname;
        };
typedef struct _iobuf FILE;
  • FILE:这个结构其实包含的是文件信息(_flag,_file_,_tmpfname)和缓存信息(_ptr,_cnt,_base,_charbuf,_bufsiz)。

以此为基础,我们往下看

fread的简化过程如下,具体实现过程的代码在这里

当打开一个文件,并进行读取时,会先从磁盘读取一部分数据到缓存里,这时候_base指向缓存的起始地址,_ptr会根据读取的数量进行移动,_cnt则根据读取的数量进行减少。

fwrite的简化过程如下,具体实现过程的代码在这里

当打开一个文件,并进行写操作时,会先分配一块缓存,这时候_base指向缓存的起始地址,_ptr会根据写入缓存的数量进行移动。

有问题的代码中,取后半段代码即fwrite后fread为讨论对象

在调用一次fwrite和一次fread后,_ptr指针的位置距离_base为21(11+10)。那么,最后的close在flush的时候会把[_base,_ptr]之间的内容输入到文件中对应的位置。也就是说fwrite写入的hello world和fread调用导致_ptr移动的部分数据也被写入到文件了,这样就导致覆盖了文件里的部分数据。整个fwrite后fread的过程分为以下五步

  1. 打开文件
  2. 建立缓存,这个缓存里的数据是未初始化的4096字节,
  3. fwrite写入hello world在0-10字节里
  4. fread使得_ptr跳过11-20字节
  5. flush使得0-20字节写入文件,其中0-10是hello world,11-20未初始化

因为读写用的是同一块缓存,所以读之后再写要重置缓存,写之后再读也要重置缓存。否则两种操作会导致缓存里的数据错乱。

那么下一个问题,fseek是如何重置缓存的呢?

在《C与指针》P317页中提到fseek允许你从写入模式切换到读取模式,也就是

fseek(fp , 0L .1);

这一句并非没用的原因,他改变了文件的读写状态。具体为何他能够改变文件读写状态。上网翻了下

首先在CSDN论坛上也翻到了类似的问题,原址在这里

里面有回答是摘自MSDN98中fopen说明的,有这么一段

When the "r+", "w+", or "a+" access type is specified, both reading and writing are allowed (the file is said to be open for “update”). However, when you switch between reading and writing, there must be an intervening fflush, fsetpos, fseek, or rewind operation. The current position can be specified for the fsetpos or fseek operation, if desired.

翻译过来就是说:

当fopn以“r+”(读写方式打开,文件必须存在)、“w+”(打开可读/写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件)或“a+"(以附加方式打开可读/写的文件。若文件不存在,则会建立该文件,如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留(原来的 EOF 符不保留))的方式执行的时候。读和写都是可以进行的(打开是为了”更新“的)。但是当在读和写之间转换的时候,这两者时间必须有个fflush、fsetpos、fseek或rewind操作。如果有必要的话,使用的位置(我猜应该是文件指针偏移量)可以用fsetpos和fseek指定

 

至于对源码的研究,目前的水平还看不懂,只能说记得用fseek记得转换文件读写状态避免文件指针出错

posted @ 2017-08-30 01:24  老和尚念经  阅读(461)  评论(0编辑  收藏  举报