零基础学C语言——文件IO
这是一个C语言系列文章,如果是初学者的话,建议先行阅读之前的文章。笔者也会按照章节顺序发布。
在编程的世界中,我们主要打交道的对象是内存,但是很多实用的软件却也很难离开对文件的操作。例如,nginx这样著名的开源web server,它会读取文件中的配置。
因此,学习编程语言语法后,紧跟的也必然是文件IO——即文件输入(Input)输出(Output)。
在类UNIX操作系统环境下,通常都是把终端(初学者暂时理解为命令行界面吧)当作文件(/dev/tty…)来看待,因此标准库封装了同一套函数来处理终端的IO与文件的IO。
本文所讨论的函数,其声明都在stdio.h中,因此需要使用预处理指令include将其引入。
我们所要讨论的函数有:
- fopen
- fclose
- fputc
- fputs
- fgetc
- fgets
- fread
- fwrite
- fseek
- feof
- fprintf
- fscanf
- printf
- scanf
流式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 <stdio.h>
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 <stdio.h>
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。
fputs
fputc是用来写入单个字符的,fputs是用来写入字符数组的。
函数原型:
int fputs(const char *s, FILE *stream);
返回值:成功返回非负值,否则返回EOF。
fgetc
有了写入字符就有读取字符操作。
函数原型:
int fgetc(FILE *stream);
功能:从头stream指代的文件的当前位置读取一个字符。
返回值:如果成功,则返回字符,否则返回EOF。
示例:
#include <stdio.h>
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中的内容为:
hello
world
那么执行如下代码:
#include <stdio.h>
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 <stdio.h>
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 world
feof
当读取文件内容时,虽然读取函数会返回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 <stdio.h>
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 World
fscanf
与fprintf相反,fscanf用于格式输出,即给定一个格式字符数组(字符串),其格式刚好匹配文件内容的格式,那么将字符数组中的特殊格式符处的值,写入到其后相应的参数变量中。
函数原型:
int fscanf(FILE *stream, const char *format, ...);
返回值:成功则返回成功匹配格式符并赋值的个数,失败则返回EOF。
format支持的格式符与fprintf的一致。
这里要注意的是,format后的参数,都是指针,因为fscanf在其内部要对变量赋值,而函数参数是自动变量,在函数生命周期结束后就会销毁回收,因此修改的内容无法传递给调用方,所以需要传递指针。
示例:
#include <stdio.h>
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。
printf
printf相当于fprintf(stdout, format, …);
其函数原型为:
int printf(const char *format, ...);
scanf
scanf相当于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 <stdio.h>
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.dat
00000000: 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 <stdio.h>
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环境高级编程》以及一些操作系统相关知识,奠定一些实用基础。
喜欢的小伙伴可以关注码哥,也可以给码哥留言评论,如有建议或者意见也欢迎私信码哥,我会第一时间回复。
感谢阅读!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!