C语言有关文件的操作

打开文件与关闭文件

在编写代码时,我有一个习惯是“保证一一对应”。
写下代码fopen()之后,还没有写对文件进行增删查改等操作的代码,先立刻写上fclose(),避免忘记关闭FILE* fd的情况。

不关闭fd,在fopen()次数较少的情况下,系统不会出现错误,但是当打开的fd多了,会造成段错误。这类问题需要反复调用fopen()才会出现错误,如果测试次数不多的话容易忽略该问题,因此建议fopen()fclose()配套使用,避免此类问题。

fopen()打开文件后,还需要需要判断文件指针是否为NULL,这一步是必要的,否则接下来的fclose(NULL)会造成段错误。

函数return前关闭文件

之所以将这一点单独拿出来说,是因为真的很容易犯错。见如下代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int open_several_files()
{
    FILE* fd_1 = fopen("/home/qnh/Desktop/film/1.bin", "ab");
    if(NULL == fd_1)
    {
        printf("FILE[%s] \n LINE[%d] \n FUN[%s]: Open file failed ! \n", __FILE__, __LINE__, __FUNCTION__);
        return -1;
    }
    FILE* fd_2 = fopen("/home/qnh/Desktop/film/2.bin", "ab");
    if(NULL == fd_2)
    {
        //此处容易漏掉关闭fd_1
        fclose(fd_1);
        return -1;
    }

    fclose(fd_1);
    fclose(fd_2);
}

代码中的fclose(fd_1);很容易被漏掉,建议写完代码后使用cppcheck等工具对代码进行审查,同时保持写下return时的警惕之心。

文件的打开模式与随机访问

文件的随机访问依赖于两个函数:fseek()ftell()fseek()函数将文件看作数组,可以移动至任意字节处。

文件的打开方式限制了fseek()的功能。假设有文件test.bin,文件的原始内容如下,文件大小为5字节:
文件内容
设计如下程序:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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】
写入内容的文件

文件结束符EOF

参考链接【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的方法,getchargetcfgets的返回值需要使用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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值