零基础学C语言——文件IO

码哥比特课程 2024-04-07 09:42:33
这是一个C语言系列文章,如果是初学者的话,建议先行阅读之前的文章。笔者也会按照章节顺序发布。 在编程的世界中,我们主要打交道的对象是内存,但是很多实用的软件却也很难离开对文件的操作。例如,nginx这样著名的开源web server,它会读取文件中的配置。 因此,学习编程语言语法后,紧跟的也必然是文件IO——即文件输入(Input)输出(Output)。 在类UNIX操作系统环境下,通常都是把终端(初学者暂时理解为命令行界面吧)当作文件(/dev/tty...)来看待,因此标准库封装了同一套函数来处理终端的IO与文件的IO。 本文所讨论的函数,其声明都在stdio.h中,因此需要使用预处理指令include将其引入。 我们所要讨论的函数有: fopenfclosefputcfputsfgetcfgetsfreadfwritefseekfeoffprintffscanfprintfscanf流式IO上面列举出来的函数是用来操作系统中的文件的,文件内容都是以字节流或者字符流的形式被处理的。 什么叫做流? 我们对文件的操作就好比从一个水管接水。水管就是我们的文件,而水就是文件的内容。那么接水有两种方式: 要多少接多少先按需求接水,但每次接的都先放在碗里,攒到一定量再倒出去处理此时,第二种就是我们所谓的流式处理。即按照要求将文件内容处理好后放入缓冲区,攒到一定量或者达到某种特殊条件时,才进行后续处理。 fopen既然我们将文件比作水管,那么要读取文件内容(水)必然需要先打开水管。 fopen就是用来打开文件的。我们看下函数原型: FILE *fopen(const char * path, const char * mode);path——文件的路径及文件名 mode——打开方式,打开方式有很多种组合,我们仅介绍如下几种: r——字符只读模式,打开文件只可读不可写,且读取的内容将作为字符类型(char)w——字符只写模式,打开文件只可写不可读,且写入的内容为字符类型(char),如果文件不存在则会新建一个文件,如果文件已存在则会清空文件(也称截断)a——字符写追加模式,打开文件只可写且写入位置是从当前文件内容的末尾处开始,写入的内容为字符类型(char),如果文件不存在则新建文件rb——二进制只读模式,打开文件只可读不可写,且读取的内容将作为无符号字符类型(unsigned char)wb——二进制只写模式,打开文件只可写不可读,且写入的内容为无符号字符类型(unsigned char),如果文件不存在则会新建一个文件,如果文件已存在则会清空文件ab——二进制写追加模式,打开文件只可写且写入位置是从当前文件内容的末尾处开始,写入的内容为无符号字符类型(unsigned char),如果文件不存在则新建文件如果在上述打开方式后再加个+,例如:r+、a+、rb+,那么文件则变为即可读写模式,其余特性依赖于+前的前缀。 函数的返回值为FILE指针类型,读者不必纠结这个类型的具体定义,每个标准库的实现也有可能不同,因此不必记下。有些文章会管这个返回值叫做文件句柄,也可以简称其为fp(file pointer)。 我们看个例子: #include int main(void){ FILE *fp = fopen("a.txt", "r"); if (fp == NULL) //为NULL时,表明打开失败,一般失败原因有:文件不存在,文件及其路径上的目录的权限不正确 return -1; return 0;}除却利用fopen打开的句柄,标准库都会预定义三个句柄:stdin, stdout, stderr,分别代表终端的:标准输入、标准输出、标准出错。我们常用的printf其实就是对stdout的输出操作。 fclose既然水管可以打开,自然也需要关闭。 函数原型如下: int fclose(FILE *stream);参数为fopen打开的文件句柄或者stdin、stdout、stderr。 返回值:如果成功关闭返回0,否则为EOF(是个宏,一般值为-1)。一般工程中,如果没有特殊需求,通常会忽略返回值。 fputc这个函数是针对字符流句柄的处理。 函数原型: int fputc(int c, FILE *stream);函数功能:将字符c,注意是字符(char)不是整数(虽然是整型),写入stream句柄指代的文件的当前位置。 返回值:成功返回0,否则返回EOF。 这里有个当前位置的概念,即文件的读取和写入都是依赖于一个位置指示器,这个指示器指示的就是当前操作到的位置。例如,fputc前,这个位置为0,表示文件起始位置。当fputc一个字符后,当前位置就变为了1。 示例: #include int main(void){ FILE *fp = fopen("a.txt", "w"); if (fp == NULL) return -1; int rc = fputc('c', fp); if (rc == EOF) return -1; fclose(fp); return 0;}打开a.txt可以看到刚刚写入的c。 fputsfputc是用来写入单个字符的,fputs是用来写入字符数组的。 函数原型: int fputs(const char *s, FILE *stream);返回值:成功返回非负值,否则返回EOF。 fgetc有了写入字符就有读取字符操作。 函数原型: int fgetc(FILE *stream);功能:从头stream指代的文件的当前位置读取一个字符。 返回值:如果成功,则返回字符,否则返回EOF。 示例: #include int main(void){ FILE *fp = fopen("a.txt", "r"); if (fp == NULL) return -1; int rc = fgetc(fp); if (rc == EOF) return -1; printf("%c\n", (char)rc); fclose(fp); return 0;}输出结果为:c fgets同理,可以读取一个字符,也可以读取一段字符数组。 函数原型: char *fgets(char *str, int size, FILE *stream);功能:从stream指代的文件的当前位置处读取最多为size-1个字符到str中,如果读取时遇到了换行符,则读取的内容只截止到换行符及其以前。 返回值:成功,则返回字符数组首地址,否则返回NULL。 示例: 加入a.txt中的内容为: helloworld那么执行如下代码: #include int main(void){ char s[64] = {0}, *ret; FILE *fp = fopen("a.txt", "r"); if (fp == NULL) return -1; ret = fgets(s, sizeof(s), fp); if (ret == NULL) return -1; printf("%s\n", ret); fclose(fp); return 0;}执行结果为: hello你没看错,hello自带了一个\n,printf中还有个\n,因此会是如此输出结果。 fseek前面提到过当前位置这个概念,那这个位置可否修改呢?当然可以,正是利用fseek进行修改的。 函数原型: int fseek(FILE *stream, long offset, int whence);功能:将stream指代的文件中的当前位置相对于whence指定的位置移动offset字节。offset可以为负数,即向前移动。 其中,whence的值有: SEEK_SET——文件开始处SEEK_CUR——当前位置处SEEK_END——文件末尾示例: #include int main(void){ char s[64] = {0}, *ret; FILE *fp = fopen("a.txt", "r+"); if (fp == NULL) return -1; fseek(fp, 5, SEEK_CUR); fputc(' ', fp); fclose(fp); return 0;}利用fseek定位到hello和world之间的换行符,然后用写入空格来覆盖换行符,此时a.txt的内容变为: hello worldfeof当读取文件内容时,虽然读取函数会返回EOF来表示读取结束或者读取出错,但有时我们不希望改变当前位置,同时获知当前位置是否达到文件末尾。这时就要使用feof函数。 函数原型: int feof(FILE *stream);返回值:如果未到结尾,则返回0,否则返回非0值。 fprintf接下来的这个是一种格式输入,即输入的字符数组中存在一种约定好的格式符,不同的格式符对应不同的数据类型,函数会利用后续参数的值来替换字符数组中对应的格式符,然后将替换好的内容写入到文件中。 函数原型: int fprintf(FILE *stream, const char *format, ...);最后的...不是省略的意思,而是一种特殊的参数,叫做可变参数,即后面的参数个数不确定,类型不确定。关于可变参的内容,本系列不打算讲解。 功能:将format字符数组中特殊的格式符利用后续参数替换后,写入stream指代的文件的当前位置。 返回值:如果出错,则返回负值,否则返回输出到文件中的字符数。 其中,format支持的常用格式符有: %d——对应int型数值%u——对应unsigned int型数值%ld——long型数值%lu——unsigned long型数值%lld——long long型数值%llu——unsigned long long数值%f——float和double数值%lf——long double数值%c——char型字符%s——char型字符数组(必须以\0结尾)示例: #include int main(void){ char s[64] = {0}; FILE *fp = fopen("a.txt", "w"); if (fp == NULL) return -1; fprintf(fp, "Hello %s", "World"); fclose(fp); return 0;}a.txt的内容变为: Hello Worldfscanf与fprintf相反,fscanf用于格式输出,即给定一个格式字符数组(字符串),其格式刚好匹配文件内容的格式,那么将字符数组中的特殊格式符处的值,写入到其后相应的参数变量中。 函数原型: int fscanf(FILE *stream, const char *format, ...);返回值:成功则返回成功匹配格式符并赋值的个数,失败则返回EOF。 format支持的格式符与fprintf的一致。 这里要注意的是,format后的参数,都是指针,因为fscanf在其内部要对变量赋值,而函数参数是自动变量,在函数生命周期结束后就会销毁回收,因此修改的内容无法传递给调用方,所以需要传递指针。 示例: #include int main(void){ char s[64] = {0}; FILE *fp = fopen("a.txt", "r"); if (fp == NULL) return -1; int n = fscanf(fp, "Hello %s", s); fclose(fp); printf("n:%d s:%s\n", n, s); return 0;}输出的结果为: n:1 s:World注意:本例中,World的长度远小于数组s的长度,因此如此使用没有问题。但是如果s只有2字节长度,那么调用fscanf就会导致缓冲区溢出。缓冲区溢出是一种bug,轻则程序崩溃,重则会被黑客利用夺取本机远程操作权限。对于任何一个C开发人员来说,都应慎重对待此类bug。 printfprintf相当于fprintf(stdout, format, ...); 其函数原型为: int printf(const char *format, ...);scanfscanf相当于fscanf(stdin, format, ...); 其函数原型为: int scanf(const char *restrict format, ...);fwrite下面要介绍的这两个函数都是针对二进制流的操作。 二进制流(字节流)与字符流一样,都需要有输入操作,即向文件中写入数据。 函数原型: size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);功能:将ptr所指向的数组(nitems个size字节数据)写入到stream指代的文件的当前位置中。 返回值:成功,返回写入的size大小的数据个数(理论上应该等于nitems),否则返回0或者一个小于nitems的数。 示例: #include int main(void){ int i = 65536; FILE *fp = fopen("a.dat", "wb"); if (fp == NULL) return -1; size_t n = fwrite(&i, sizeof(i), 1, fp); fclose(fp); return 0;}此时,a.dat的内容我们用xxd看下文件中的十六进制: $ xxd a.dat00000000: 0000 0100 可以看到,文件中的十六进制值为0000 0100刚好是十进制的65536。 fread二进制流(字节流)输出操作。 函数原型: size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);功能:从stream指向的文件的当前位置中,读取nitems个size字节大小(一共nitems*size字节)的数据到ptr指向的数组中。 返回值:成功,返回读取的size大小的数据个数(理论上应该等于nitems),否则返回0或者一个小于nitems的数。 示例: #include int main(void){ int i = 0; FILE *fp = fopen("a.dat", "rb"); if (fp == NULL) return -1; size_t n = fread(&i, sizeof(i), 1, fp); fclose(fp); printf("%d\n", i); return 0;}输出结果为:65536 到此,零基础学C语言系列文章完结。读者如果通读过这个系列的文章,并且学会其中的各个知识点,那么恭喜你,你已经站在了C语言大门内了,虽然依旧是贴着门站。 笔者推荐下一步学习《UNIX环境高级编程》以及一些操作系统相关知识,奠定一些实用基础。 喜欢的小伙伴可以关注码哥,也可以给码哥留言评论,如有建议或者意见也欢迎私信码哥,我会第一时间回复。 感谢阅读!
0 阅读:0

码哥比特课程

简介:感谢大家的关注