C语言有关文件的操作

打开文件与关闭文件

在编写代码时,我有一个习惯是"保证一一对应"。

写下代码fopen()之后,还没有写对文件进行增删查改等操作的代码,先立刻写上fclose(),避免忘记关闭FILE* fd的情况。

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

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

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

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

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

设计如下程序:

c 复制代码
#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处回退一个字节。

c 复制代码
fseek(fd, -1, SEEK_END);

当文件指针指向SEEK_END时,ftell()表示文件开始处到SEEK_END的字节数,也可以认为是从文件开始处移动到SEEK_END所需要的次数,也可以认为是文件的大小。

获取文件内容

在工程中经常需要逐字节获取文件内容。

如果使用while()循环+判断EOF的方法,getchargetcfgets的返回值需要使用int类型变量接收,如果使用char类型变量接收,可能会导致错误。

c 复制代码
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处,从头开始遍历文件。

c 复制代码
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

相关推荐
双手插兜-装高手7 分钟前
Linux - 线程基础
linux·c语言·笔记
froginwe118 分钟前
Go 语言数组
开发语言
瑞雨溪9 分钟前
python中的OS模块的基本使用
开发语言·python
醒过来摸鱼13 分钟前
【Golang】协程
开发语言·后端·golang
时光追逐者14 分钟前
.NET 9 中 LINQ 新增功能实操
开发语言·开源·c#·.net·.netcore·linq·微软技术
Leuanghing15 分钟前
使用Python生成F分布表并导出为Excel文件
开发语言·python·excel·f分布
IT枫斗者17 分钟前
如何解决Java EasyExcel 导出报内存溢出
java·服务器·开发语言·网络·分布式·物联网
XiaoCCCcCCccCcccC36 分钟前
Linux环境下的基础开发工具 -- 包管理器,vim,gcc/g++,make/makefile,git,gdb/cgdb
linux·c语言·gdb
sun_weitao1 小时前
括号匹配算法
开发语言·python
sky_smile_Allen1 小时前
[C#] 关于数组的详细解释以及使用注意点
开发语言·算法·c#