那些年,坑死自己的事之fread/fwrite
今天继续看牛人做过的东西,这个小程序并不大,加上相当多的注释行,才5000多行。这个小程序是在linux下实现的,之前自己也一直用vi来看并加以更加详细的注释,但是效率实在太低。于是将其转移到windows下决定改造到VS2012下运行。
这是一段纯C的代码,新建的工程是C++的,而代码中使用了强制类型转换将一个结构体类型转换成了另一个结构体。于是编译的时候报错不能通过。最后,我新建了空工程,将其以已存在的文件的形式导入,解决了这个问题。修改了一些问题之后,终于不报错,可以运行了。可是真正悲催的事情开始发生了。
首先,运行之后报错,确定了是文件读写的错误之后,给原来的代码中打开文件的地方加上异常处理。可是还是错误。单步进去发现
while(fread(&record,sizeof(RECORD_TYPE),1,fp_data)==1)
怎么都不能进入循环,fread的返回值永远都是0。因为sizeof(RECORD_TYPE)的值是128,于是把上面的代码改成了
while(fread(&record,1,128,fp_data)==128)
再来单步一看,好奇怪,这次返回的值变成了33。百思不得其解。之后我把这句代码拿出来,不放在while循环中,如下:
fseek(fp_data,0L,SEEK_END);//偏移都文件尾部 pos=ftell(fp_data);//读取尾部所在位置 fseek(fp_data,0L,SEEK_SET);//偏移到文件头部 err = fread(&recode,1,128,fp_data);//读取一条记录 pos = ftell(fp_data);//读取位置
之后再调试,发现前一个ftell得到的值是81792,而后一个ftell的返回值却是4096。更让人费解了。文件并没有到达文件末尾,文件足够大,才读取了4096/81792,可是,明明只读取了128位字节,128*8应该是1024才对,怎么第2个ftell返回了4096?这种情况下,有些慌了。虽然看了MSDN上的关于fread放回值得说明,可是却没有去实践。最后度娘告诉我这个网址http://www.360doc.com/content/11/0128/16/2150347_89591799.shtml。
/* 文本方式读取二进制数据, 可能在文件结束之前将某段数据判定为文件末尾EOF, 所以结束读取( 举个例子, 比如遇到 0x00 0x00 0xff 0xff, 则文本方式方式的文件流, 认为已经到文件末尾, 不能读取) */
瞬间明白了错误的原因。对上一段代码修改一下再试:
fseek(fp_data,0L,SEEK_END);//偏移都文件尾部 pos=ftell(fp_data);//读取尾部所在位置 fseek(fp_data,0L,SEEK_SET);//偏移到文件头部 err = fread(&recode,1,128,fp_data);//读取一条记录 pos = ftell(fp_data);//读取位置 err = feof(fp_data);//是否到达文件尾,非0为经过了文件尾,0为否;feof具体用法见msdn
得到了feof返回值16,非0,表达经过了文件尾。因而断定fread读取数据的时候遇到了误以为的EOF标志。修正方法为,将fopen中的mode参数改成了'rb’,即由文本方式读取改为二进制流的方式读取。如下:
//if (err=(fopen_s(&fp_data,"data","r")) != 0) // printf("open file data failed\n"); //打开数据文件 if (err=(fopen_s(&fp_data,"data","rb")) != 0) printf("open file data failed\n"); //打开数据文件
这个问题总算解决了。满以为就此解决了问题。偏偏陷入了令人更加头疼的境地。
再次点击运行,又是运行时错误。跟进去发现了出现了一个树的指针为空,却赋值了。再看明明调用了给这个节点申请了空间啊!难道是malloc失败?立马给malloc的地方加上判断。遗憾的是,依然一点进展都没有。再看作者的写代码的思路,给这个节点申请空间之前,先判断a的值是不是等于b,如果不等于,则打印一条消息新建树节点失败,但不终止程序,当然也不申请空间。噢,原来这样,那看看什么时候会出现这种情况。找到这段代码的上面一段代码,密密麻麻的一片,几个if-else写了一两百行。不过还是可以大致明白的。
头脑一震,发现问题了,作者代码大致如下:
if (a < b) { //.......一大段代码,好几十行 } else { if (a != b) { print(""); return FALSE; } //给节点申请空间 //......一大段代码 }
所以我满心欢喜的认为即使作者这样的牛人也会犯迷糊,觉得理应将此处的a != b改成 a >= b。不管它能不能改,先改了再说。可是改了之后,运行一下,这次还是报错了,不过呢,不是这个地方了。单步调试,这个a的值怎么打大得太奇怪了吧。先看代码:
//此处a,b,c,d等都不是程序中的原样,只是为了说明而做了简化 found=FALSE; if (a < b){ i = 0; while (!found && i<a){ if (c >=d[i]){//其中b就是数组d的大小 i++; } else { found = TRUE; } } //其他代码 }
再一看b的值是29,可是a的值却是5029。不发生越界才奇怪了。从此处也就明白了上一段代码中为何只判断a < b 及 a != b 而不处理 a > b 了,因为一旦 a > b 就出错了。
回头一想怎么会这样呢?
到底是哪儿错了???
想到了一点,会不会还是读取文件的时候的错误?会不会是因为以文本文件流方式读取了二进制文件,影响了a的取值?果断找到所有用到fopen的地方,把mode参数加上一个 'b' 。
再次点击运行,果然,问题就这么解决了。
总结
这么一场闹剧总算可以收场了,都是因为fread这个函数惹得祸。
/* 文本方式不能完全读取, 而二进制方式能的原因- 文本方式读取文件, 最主要的用处是一次读取一整句( 以换行符'\n', 即二进制的换行标志"\r\n"结束 ), 方便用于特殊用处ReadString、fscanf(...,"%s",...)之类, 每次读取的内容长度是不定的; 而二进制读取方式Read、fread等, 都是读取固定长度 所以文本方式读取对EOF的判定, 是一个文件尾结束标志, 如果是文本文件, 则这个文件尾肯定不会出现在文件内容中( 因为是不可打印字符构成的结束标志, 人可读的文本文件不会包括它 ), 这样以结束标志为文件尾则是可以的; 二进制文件内容可以是任意字节, 如果把它当文本文件来读, 以文件尾为结束, 当然可能出现把文件内容判定为文件尾的情况; 二进制读取方式由于每次读取固定字节, 所以只需要用总文件长度( 这个数值是系统管理的数值, 不是计算得出来的 )减去每次读取的长度( 或根据Seek的位置计算长度 ), 就可以知道是否到文件尾, 不需要定义结束标志; 所以用二进制方式打开任何文件都是合理的 */
至此也明白了作者是通过读取到的数据量是否等于fread 中count参数要求的数据量来结束循环而不是通过feof来判断。
最后,一定要明白,使用fread/fwrite的时候千万记得以二进制形式读取。