在编写代码时,我有一个习惯是“保证一一对应”。
写下代码fopen()
之后,还没有写对文件进行增删查改等操作的代码,先立刻写上fclose()
,避免忘记关闭FILE* fd
的情况。
不关闭fd
,在fopen()
次数较少的情况下,系统不会出现错误,但是当打开的fd
多了,会造成段错误。这类问题需要反复调用fopen()
才会出现错误,如果测试次数不多的话容易忽略该问题,因此建议fopen()
与fclose()
配套使用,避免此类问题。
在fopen()
打开文件后,还需要需要判断文件指针是否为NULL,这一步是必要的,否则接下来的fclose(NULL)
会造成段错误。
文件的随机访问依赖于两个函数:fseek()
与ftell()
。fseek()
函数将文件看作数组,可以移动至任意字节处。
文件的打开方式限制了fseek()
的功能。假设有文件test.bin
,文件的原始内容如下,文件大小为5字节:
设计如下程序:
#include
#include
#include
int main()
{
// 打开文件
// 文件原始内容为0x11 0x22 0x33 0x44 0x55,共5字节
FILE* fd = fopen("/home/qnh/Desktop/film/test.bin", "ab");
if(NULL == fd)
{
printf("FILE[%s] \n LINE[%d] \n FUN[%s]: Open file failed ! \n", __FILE__, __LINE__, __FUNCTION__);
return 0;
}
// 读取文件内容并显示
fseek(fd, 3, SEEK_SET);
int offset = ftell(fd);
printf("offset_after_fseek: %d \n", offset);
int ch = getc(fd);
printf("ch: %02x \n", ch);
offset = ftell(fd);
printf("offset_after_getc: %d \n", offset);
// puts()方法向文件中写入数据
putc(0xAA, fd);
offset = ftell(fd);
printf("offset_after_puts: %d \n", offset);
// fwrite()方法向文件中写入数据
fseek(fd, 3, SEEK_SET);
unsigned char data[2] = {0xBB, 0xCC};
fwrite(data, 1, 2, fd);
offset = ftell(fd);
printf("offset_after_fwrite: %d \n", offset);
fclose(fd);
}
举例来说,以"ab"模式打开文件,并使用fseek()
移动文件指针。先进行读操作,此时使用ftell()
输出文件的当前位置,可以发现,文件指针是移动至offset的的,但是使用getc()
函数,无法读取到文件在该偏移量的内容,读取到的内容是0xFF,这是合理的,因为"ab"模式是写模式,并没有读取文件内容的权限,如果使用"ab+"模式打开文件,就可以读取文件内容了。
再测试一下写操作,可以发现,写入的0xAA以及0xBB、0xCC被追加到了文件的末尾,而不是offset处,并且此时ftell()
显示文件指针移动到了文件的末尾。说明"a模式"下写入数据的情况,与fseek()
函数移动的文件指针无关,数据只能追加到文件的末尾。见参考链接【1】。
见参考链接【2】
EOF 是 End Of File 的缩写。在C语言中,它是在标准库中定义的一个宏,在数值上为int类型的-1(0xFFFFFFFF)。
在《C primer plus》的13.2.4 文件结尾
章节,有如下描述:
如果getc()函数在读取一个字符时发现是文件结尾,它将返回一个特殊值EOF。所以C程序只有在读到超过文件末尾时才会发现文件的结尾。
这句话书里写的比较混乱,我第一次读产生了歧义。
首先,书中的文件结尾和文件末尾是两个不同的概念,我的理解是,文件末尾是文件的最后一个字节,而文件结尾是一个“哨兵”字符,指向文件末尾的后一个字节,也就是SEEK_END
。
其次,不要认为EOF是附加在文件结尾处的字符,实际上文件的结尾并不存在一个EOF字符,EOF表示一种状态。getc()
函数读到超过文件末尾时,会发现文件已经结束了,此时getc()
返回EOF。
书的13.5.1 fseek()和ftell()的工作原理
一章,表13.3将SEEK_END和文件末尾混为一谈,与13.2.4 文件结尾
的超过文件末尾时才会发现文件的结尾
产生了矛盾。
鉴于书中这几个章节的混乱,我建议抛弃文件末尾的概念,只留“文件结尾 = SEEK_END”这个概念,概念解释图如下:
当getc()
读完整个文件,此时满足(ch == EOF)
时,文件指针指向了SEEK_END
处,此时想要读取文件的最后一个字节,需要从SEEK_END
处回退一个字节。
fseek(fd, -1, SEEK_END);
当文件指针指向SEEK_END
时,ftell()
表示文件开始处到SEEK_END
的字节数,也可以认为是从文件开始处移动到SEEK_END
所需要的次数,也可以认为是文件的大小。
在工程中经常需要逐字节获取文件内容。
如果使用while()循环+判断EOF的方法,getchar
、getc
、fgets
的返回值需要使用int类型变量接收,如果使用char类型变量接收,可能会导致错误。
int main()
{
FILE* fd = fopen("/home/qnh/Desktop/film/test.bin", "rb+");
if(NULL == fd)
{
printf("FILE[%s] \n LINE[%d] \n FUN[%s]: Open file failed ! \n", __FILE__, __LINE__, __FUNCTION__);
return 0;
}
int ch = getc(fd);
while(EOF != ch)
{
printf("%02x ", ch);
ch = getc(fd);
}
fclose(fd);
}
使用char类型变量可能产生错误的原因在于,假设getc()
读取到的字节为0xFF,则getc()
返回值为0x000000FF,赋值给char类型的ch后,ch的值为0xFF,那么(EOF == ch)条件将会成立,而此时文件尚未读到结尾,while()循环会提前退出。见参考链接【3】。
或者先获取文件的大小,再使用for()
循环读取文件,这种方法就不需要使用EOF了。
获取文件大小后,循环的次数就确定了,可以使用for()
循环获取文件内容,此时可以使用char类型变量接收。
需要注意文件指针的位置,我常用的做法是在使用ftell()
前,先将文件指针移动至SEEK_END
处,使用ftell()
后,再将文件指针移动至SEEK_SET
处,从头开始遍历文件。
int main()
{
FILE* fd = fopen("/home/qnh/Desktop/film/test.bin", "rb+");
if(NULL == fd)
{
printf("FILE[%s] \n LINE[%d] \n FUN[%s]: Open file failed ! \n", __FILE__, __LINE__, __FUNCTION__);
return 0;
}
fseek(fd, 0, SEEK_END);
int size = ftell(fd); // 获取文件大小
fseek(fd, 0, SEEK_SET);
for(int i = 0; i < size; i++)
{
printf("%02x ", getc(fd));
}
fclose(fd);
}
【1】https://blog.csdn.net/veghlreywg/article/details/103348856
【2】https://blog.csdn.net/chenaibo/article/details/6062773
【3】https://blog.csdn.net/fengyuruhui/article/details/1682495