文章目录
C语言笔记16:文件操作
前置知识
文件作用
文件能够将数据持续化保存,按照功能分类可以分为程序文件和数据文件,Linux下路径分隔符为
/,windows下为\。
二进制文件和文本文件
文本文件和二进制文件的区别在于存储内容和解释方法不同,这点在程序中就能体现,比方说一个
"12345"字符串,占用空间6个字节(包括\0),而使用int a = 12345;却只占用4个字节,文本文件存储的内容一般是utf-8或者ASCII等等编码表的编码。
流
流是一个C标准库级别的抽象概念,和操作系统的文件描述符很相似却又不一样,流具有用户级空间缓冲,fd文件描述符只有内核级缓冲。可以简单理解成流是封装过的fd,而fd是流的底层实现。
文件指针
在<stdio.h>头文件中提供了FILE的定义:
cstruct _iobuf { char *_ptr; int _cnt; char *_base; int _flag; int _file; int _charbuf; int _bufsiz; char *_tmpfname; }; typedef struct _iobuf FILE;每调用一次fopen都会动态开辟一块空间,在堆上存放这个FILE结构体信息。通过这个FILE结构体可以间接地找到与它所关联的文件。
c#include<stdio.h> FILE* pf;//定义文件指针变量
文件打开和关闭
c
FILE* fopen(const char* filename,const char* mode);
c
int fclose(FILE* stream);
fclose返回值表示成功与否

mode
| mode | 含义 | 不存在是否新建 |
|---|---|---|
| "r" | 输入 | 否,打开失败 |
| "w" | 输出 | 是 |
| "a" | 向文件末尾追加内容 | 是 |
| "rb" | 二进制输入 | 否,打开失败 |
| "wb" | 二进制输出 | 是 |
| "ab" | 二进制追加 | 是 |
| "r+" | 读写方式打开 | 否 |
| "w+" | 读写方式打开 | 是 |
| "a+" | 文件末尾读写 | 是 |
| "rb+" | 二进制读写 | 否 |
| "wb+" | 二进制读写 | 是 |
| "ab+" | 二进制文件末尾读写 | 是 |
文件顺序读写
| 函数 | 功能 | 适用 |
|---|---|---|
| fgetc | 输入字符 | 所有输入流 |
| fputc | 输出字符 | 所有输出流 |
| fgets | 输入字符串 | 所有输入流 |
| fputs | 输出字符串 | 所有输出流 |
| fscanf | 格式化输入 | 所有输入流 |
| fprintf | 格式化输出 | 所有输出流 |
| fread | 二进制输入 | 文件输入流 |
| fwrite | 二进制输出 | 文件输出流 |
为什么二进制输入输出不适用于标准输入输出流呢?标准输入输出流是向终端打印文本信息,使用二进制输入输出可能会产生一些意想不到的后果,比如换行符的解释不同,以及一些控制字符的问题。
ctrl + C,ctrl + D。
scanf/sscanf/fscanf
printf/sprintf/fprintf之间的区别
scanf从键盘获取内容,sscanf从字符串中获取内容,fscanf从文件中获取内容
printf打印到屏幕,sprintf输出到字符串,fprintf输出到文件。
文件随机读写
fseek
c
int fseek(FILE* stream,long int offset,int origin);
从origin开始偏移offset位置
c
#include <stdio.h>
int main()
{
FILE* pfile;
pfile = fopen("example.txt","wb");
fputs("This is an apple.",pfile);
fseek(pfile,9,SEEK_SET);
fputs(" sam",pfile);
fclose(pfile);
return 0;
}
从开始位置偏移9是哪里呢?

光标指向字母n的位置了,从这里开始输出" sam",n被替换成了" ",最终结果是"This is a sample."

ftell
c
long int ftell(FILE* stream);
这个返回值和上面的long int offset是一个类型的,说明返回的是偏移量。
使用ftell计算文件大小
c
#include <stdio.h>
int main()
{
FILE* pf = fopen("example.txt","rb");
fseek(pf,0,SEEK_END);
long int size = ftell(pf);
fclose(pf);
printf("%ld\n",size);
return 0;
}
上面程序有个点需要注意,比方说
example.txt文件存放内容为:"This is a sample."。那SEEK_END指向的位置是哪里呢?是.吗?还是\0?我们刚刚使用的输出接口是fputs,这是一个专门用于字符串输出的接口,它和fprintf一样,属于**专门用于字符输出的,它们的共同点在于不会将
\0输出到文件中,因为\0只是C语言用来表示字符串结束的标志,不属于字符串的有效内容。**那所以SEEK_END指向的位置是.吗?也不是,SEEK_END实际上指向
.后面的EOF。为什么ftell返回SEEK_END相对于文件起始位置的偏移量就是文件的大小而不是文件大小-1?原因就在于SEEK_END并不是指向.而是.后面的EOF。
运行结果:

fputs和fprintf都不会向文件中打印
\0,那fgets的行为是什么呢?fgets读到换行符或者第n - 1个字符会停止,然后在末尾加上
\0,输入到缓冲区中。也就是会读取\nfscanf则取决于占位符的类型,它的行为参考scanf的行为。scanf读取内容其实是在缓冲区中读取,比方说输入abcd\n,缓冲区的内容就是abcd\n,如果以%s的形式输入,就会省略\n,\n会被留在缓冲区里面,如果以%c形式输入,则有可能读取到\n。
rewind(倒回)
c
void rewind(FILE* stream);
文件读取结束
文件读取结束判断返回值
- 判断fgetc返回值是否为EOF
- 判断fgets返回值是否为NULL
- 判断fscnaf匹配占位符数量是否正确
- fread判断返回值是否小于要读取的个数
feof
c
int feof(FILE* stream);
这是一个极容易误用的函数,原因在于它的实际作用是,当读取文件时已经将EOF读取到后 ,调用该函数才会返回一个非0值。
feof实际作用是,文件读取结束时,判断结束原因是否是读取到EOF。或者是ferror(pf)。
举个例子
c
#include <stdio.h>
int main()
{
FILE* pf = fopen("example.txt","rb");
char buffer[100];
while(!feof(pf))
{
fgets(buffer,sizeof(buffer),pf);
printf("%s",buffer);
}
fclose(pf);
return 0;
}
假设example.txt内容如下:

当fgets读完第三行内容的时候,此时并没有读到EOF,但是文件实际上已经读完了,再次读取时fgets返回NULL(成功返回传入的缓冲区指针)。但是这里没有判断fgets的返回值,而是判断feof返回值,以为文件还没读完,所以就会出错。
文件缓冲区

使用fflush是刷新到用户级缓冲区中。