- 1. IO概述
- 系统调用和库函数有什么区别?
- 2. 标准IO
- 2.1 缓冲区 / 缓存区
- 行缓冲区刷新方法
- 全缓冲区刷新方法
- 2.2 文件指针
- 2.3 标准IO常用函数
- fopen()
- fclose()
- perror()
- errno
- fgetc()
- fputc()
- feof() / ferror() 判断返回值EOF类型
- ungetc() 将字符放回输入流
- fgets()
- fputs()
- fprintf() / sprintf() / dprintf / snprintf
- fread() 与 fwrite()
- fseek() / rewind() / ftell()
- fgetpos() / fsetpos()
- 获取行缓冲、全缓冲大小
- setvbuf() 设置缓冲区的大小
IO 指的是输入输出函数。
站在计算机的角度,输入是读操作,输出是写操作。
例如:我们从终端输入信息给计算机,计算机是从终端读取信息,而我们让计算机输出信息时,计算机是要向终端写信息。
IO分为文件IO和标准IO
- 文件IO属于系统调用
- 标准IO属于库函数
系统调用:用户通过应用层函数操作Linux内核,Linux内核再控制硬件。
Linux内核中封装了大量的系统调用,用户通过应用层的系统调用函数调用内核中的系统调用。
因为不同的Linux内核中的系统调用是不同的,所以采用系统调用的应用程序移植性不高,而对底层硬件的操作一般都是用的系统调用
库函数:库函数本质还是系统调用,但比系统调用多了一个缓冲区,缓冲区用来减少系统调用次数,只有在缓冲区被刷新的时候,才会执行系统调用。
库函数是为了统一不同操作系统对文件操作的函数,只要支持C语言,库函数一般就可以使用,所以库函数的移植性更高。
| 库函数调用 | 系统调用 | |
|---|---|---|
| 移植性 | 可移植性好 | 依赖内核,不保证移植性 |
| 调用属性 | 调用函数库的代码 | 调用系统内核的服务 |
| 调用类型 | 普通函数的调用 | 操作系统的入口点 |
| 运行空间 | 用户空间执行 | 内核空间执行 |
| 运行时间所属 | 用户时间 | 系统时间 |
| 开销 | 属于过程调用,开销较小 | 在用户空间和内核空间上下文环境切换,开销较大 |
| 数量 | 库函数数量多 | Unix大约有90个系统调用,较少 |
| 典型调用函数 | printf scanf malloc | fork open write |
对硬件进行操作时,会先操作Linux内核
操作Linux内核的三种方式:shell、系统调用、库函数。
| 分类 | 解释 |
|---|---|
| 行缓冲 | 终端操作对应的缓冲区为行缓冲 |
| 全缓冲 | 文件操作对应的缓冲区为全缓冲 |
| 无缓冲 | 终端输出错误信息对应的缓冲区为无缓冲 |
缓冲区需要被刷新,如果不刷新缓冲区,数据就停留在缓冲区,无法执行系统调用。
向无缓冲写入数据会直接打印在终端
行缓冲区刷新方法- 输出n
- 缓冲区切换 (输出缓冲转换成输入缓冲)
- 使用fflush函数,刷新缓冲区
- 缓冲区满时,行缓冲大小为1024 Bytes
- 程序正常结束
- 使用fflush函数
- 缓冲区满
- 程序正常结束
文件指针又称为流指针,简称流。(stream pointer)
可以理解为向文件读取或者写入信息时,那些字符像流水一样有序地流出或者流入。
文件指针标识一个文件,用库函数对文件操作时,需要用到文件指针。
文件指针是结构体类型指针FILE *,typedef struct _IO_FILE FILE;
_IO_FILE结构体中,保存了打开的文件的各种信息
程序运行时,操作系统会自动为当前程序分配三个文件指针
| FILE * | 作用 |
|---|---|
| stdin | 标准输入流指针,用于从终端读取数据 |
| stdout | 标准输出流指针,用于对终端写数据 |
| stderr | 标准错误输出流指针,用于向终端输出错误信息 |
fopen不仅打开一个文件,还会创建缓冲区——如果是读写模式,打开两个;只读或者只写,则只打开一个,再创建一个包含文件和缓冲区数据的结构体。
以文本模式打开,返回的就是文本流
以二进制模式打开,返回的就是二进制流
#includeFILE *fopen(const char *pathname, const char *mode); 功能:打开或者创建一个文件 参数: pathname: 要打开或者创建的文件名,可以跟路径,如果不跟路径,默认就是当前路径 mode:操作权限 r 以只读的方式打开,如果文件不存在则报错,文件指针定位到文件的起始位置 r+ 以读写的方式打开,如果文件不存在则报错,文件指针定位到文件的起始位置 以 "r" 或者 "r+" 打开,文件不存在,返回错误码为 2 。 w 以只写的方式打开,如果文件存在则清空,文件不存在则创建,文件指针定位到文件的起始位置 w+ 以读写的方式打开,如果文件存在则清空,文件不存在则创建,文件指针定位到文件的起始位置 a 以只写的方式打开,如果文件存在则追加,文件不存在则创建,文件指针定位到文件的起末尾位置 a+ 以读写的方式打开,如果文件存在则追加,文件不存在则创建,文件指针定位到文件的起末尾位置 在 r / w / a 后面加上 b 就代表以二进制方式打开文件 w 后面加上 x (c11)如果文件已存在或以独占模式打开文件,则打开文件失败 返回值: 成功:文件指针 失败:NULL
使用a+操作时,无论读到文件哪个位置,执行写操作时,都会在文件结尾处追加。
fclose()#includeint fclose(FILE *stream); 功能: 关闭一个文件指针,如果关闭了文件指针,就无法再通过这个文件指针对文件进行操作了 参数: stream:文件指针,fopen的返回值 返回值: 成功:0 失败:EOF
EOF→end of file文件结束描述符,一般值为-1
perror()#includeerrnovoid perror(const char *s); 功能:输出一个函数调用失败之后的错误信息,输出会自动换行 参数: s:提示语句 返回值:无
#includefgetc()int errno; errno是一个全局变量,用于输出函数调用失败的错误码 可以调用strerror函数通过errno值打印错误信息 char * strerror(int errnum); printf("%sn", strerror(errno));
#includeint fgetc(FILE *stream); 功能:从文件中读取一个字符 参数: stream:文件指针 返回值: 成功:读取的字节(char 类型本质仍是int类型) 失败:EOF 文件内容读取完毕 EOF
这里函数执行失败和读到文件结束的返回值相同,是一个比较棘手的问题,因为如果直接返回了EOF,我们并不知道该文件是空的,还是函数执行失败。
所以,请看函数feof/ferror
文件中每一行的结尾都有一个结束标志换行符,fgetc可以读取到整个换行符,可以根据这个来判断文件中有多少行。
fputc()#includeint fputc(int c, FILE *stream); 功能:向指定的文件中写入一个字符 参数: c:要写入的字符 stream:文件指针 返回值: 成功:写入的字符 失败:EOF
注意
feof() / ferror() 判断返回值EOF类型如果先往文件中写数据,然后读数据,读的位置是最后写完数据的下一个位置,因为写和读的文件指针定位的位置是同一个
如果标准输入函数返回EOF,通常表明函数已经到达文件末尾,但读取错误时,也会返回EOF,feof()和feorror()就是用于区分这两种情况。
int feof(FILE *fp); 功能:判断上一次EOF是否为文件末尾 参数:要检测的文件指针 返回值: 当EOF是 文件末尾:非0 错误: 0
int ferror(FILE *fp); 功能:判断上一次EOF是否为读取错误 参数:要检测的文件指针 返回值: 当EOF是 错误: 非0 文件末尾: 0ungetc() 将字符放回输入流
#includeint ungetc(int c, FILE * fp); 功能: 将指定字符放回输入流,且是已有缓存的最前端,下次调用函数读取时,将会先读取到该字符 参数: c 要放回流的字符 fp 目标流 返回值: 成功 c 失败 EOF
Linux c语言,只有在程序阻塞等待输入,并在终端输入字符时,并按下回车,才会把输入字符放入输入流。
fgets()#includechar *fgets(char *s, int size, FILE *stream); 功能:从文件中读取一个字符串 参数: s:保存读取的内容 size:要读取的字节数 stream:文件指针 返回值: 成功:返回读取的内容 失败:NULL 如果读到文件末尾,也返回NULL
注意:如果能够读取到的字节数小于第二个参数size,会将回车符n也当做是字符保存在第一个参数s里面。
如果输入的数据的字节数大于第二个参数size,则只会保存size - 1个字节,最后一个位置补
注意需要保证第一个参数字节数足够大,至少是size + 1,否则会内存越界,可能会影响程序正常执行,好点话会报段错误。
fputs()#includeint fputs(const char *s, FILE *stream); 功能:向文件写入一个字符串 参数: s:要写入的字符串 stream:文件指针 返回值: 成功:1 失败:EOF
注意:这里输出字符串,结束标志是
fprintf() / sprintf() / dprintf / snprintf先解析fprintf,第一个f指file,中间print指打印,最后一个f指format,连起来就是将字符串格式化地打印到文件中。类似于printf
那么sprintf中s即为string字符串,意为字符串格式化打印到字符串中
#includeint printf(const char *format, ...); int fprintf(FILE *stream, const char *format, ...); int dprintf(int fd, const char *format, ...); int sprintf(char *str, const char *format, ...); int snprintf(char *str, size_t size, const char *format, ...);
第5 - 9行的函数fprintf、dprintf、sprintf,第一个参数都是要打印的目标区,都是将后面的格式化字符串打印到该目标区,目标区可以是文件指针,文件描述符或者字符串,不过函数不同而已。
snprintf多了一个第二个参数size,这里指定了要向字符串中打印多少个字节。
这5个函数返回值都是成功打印的字节数。
使用dprintf时,或者read write时,可能文件描述符0(STDIN_FILENO)、1(STDOUT_FILENO)、2(STDERR_FILENO)都指向同一个文件,所以我可以从标准输出中读取终端的数据,也能通过标准输入向终端写数据。
而通过流指针进行类似操作时,库函数应该已经进行了优化,是无法达到类似现象的
(此处,本人不才,无法过多解释,望有大佬指教)
为保证数据在存储前后保持一致,标准I/O的fread()和fwrite()函数用二进制形式处理数据。
意味着我们不光能向文件中写字符,还可以写入int、double等类型的数据。
如果以程序所用的表示方法把数据存储在文件中,则称以二进制形式存储数据
——————————————————
例如,double类型的值存在一个double大小的单元中
#includesize_t fread(void *ptr, size_t size, size_t nmemb, FILE * stream); 功能: 从文件中读取任意类型的内容 参数: ptr:保存数据变量的地址,如数组、结构体等 size:每一个块(对象)的长度 nememb:块数(对象数) stream:文件指针 返回值: 成功:实际读取的块数(对象数) 失败:0 读取到文件末尾也返回0
#includesize_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE * stream); 功能: 向文件写入任意类型的内容 参数: ptr:要写入的数据 size:每一个块(对象)的长度 nememb:块数(对象数) stream:文件指针 返回值: 成功:写入的块数(对象数),从第一个参数的首地址数据开始一直写到指定的字节数 失败:0
这里注意fread读取数据时,如果能够读取的数据大于指定的size*nememb,那么读取size*nememb大小的数据(也要小心越界问题),否则,能读多少读多少。
而fwrite写数据时,如果给指针ptr下的数据比size*nememb大,那么只写size*nememb大小数据,否则,会一直向后读内存向文件中写数据,直到写入数据够size*nememb。
fseek() / rewind() / ftell()#includeint fseek(FILE *stream, long offset, int whence); 功能: 定位文件指针的位置 参数: stream 文件指针 offset 要偏移的位置,可正可负 whence 相对位置 SEEK_SET 文件起始位置,为0 SEEK_CUR 文件当前位置 SEEK_END 文件末尾位置,最后一个字符的下一个位置 返回值: 成功:0 失败:‐1
void rewind(FILE *stream); 功能: 将文件指针定位到起始位置 参数: stream:文件指针 返回值:无
rewind(fp) <==> fseek(fp, 0, SEEK_SET);
long ftell(FILE *stream); 功能: 获取当前文件指针定位的位置 参数: stream:文件指针 返回值: 成功:当前文件指针定位的位置 失败:‐1fgetpos() / fsetpos()
fseek()与ftell()潜在问题是他们把文件大小现在在long类型范围下(大概20亿字节)
ANSI C新增两个函数,处理较大文件的定位。他们定义了一个新类型fpos_t(file position type,文件定位类型),它不是一个基本类型,不能定义为数组类型,其他没有限制。
不过,ANSI C定义了如何使用fpos_t类型
#includeint fgetpos(FILE * stream, fpos_t * pos); 功能: 获取文件中的当前位置距离文件开头的字节数 参数: stream 要获取位置的文件指针 pos 文件的位置信息放在该指针指向的地址 返回值: 成功 0 失败 非0
#include获取行缓冲、全缓冲大小int fsetpos(FILE * stream, const fpos_t * pos); 功能: 设置文件指针指向偏移值后指定的位置。 参数: stream 文件指针 pos 文件指针位置信息 返回值: 成功 0 失败 非0
#includeint main(int argc, char const *argv[]) { //stdout printf("hello worldn"); printf("行缓冲大小:%ld字节n", stdout->_IO_buf_end ‐ stdout‐>_IO_buf_base); FILE *fp = fopen("file.txt", "w"); fputc('w', fp); printf("全缓冲大小:%ld字节n", fp->_IO_buf_end ‐ fp‐>_IO_buf_base); return 0; }
获取缓冲区大小前,一定要先使用缓冲区,否则打印出来的值为0
setvbuf() 设置缓冲区的大小int setvbuf(FILE * fp, char * buf, int mode, size_t size); 功能: 创建一个供标准IO函数替换使用的缓冲区。 使用要求,需要在打开文件后且未对流进行其他操作前,掉用该函数。 参数: fp 待处理的文件流 buf 指向待使用的存储区,如果buf的值不为NULL,则必须创建缓冲区。 如果传NULL,函数会为自己创建一个缓冲区(我也不知道这个函数要缓冲区干嘛),文件缓冲没有变化。 mode 缓冲区的模式 _IOFBF 全缓冲(full buf) 0 _IOLBF 行缓冲(line buf) 1 _IonBF 无缓冲(no buf) 2 size 一般指传入数组大小(字节),文件的缓冲区也与此有关 返回值: 成功 0 失败 非0
当设置为全缓冲或者行缓冲时,读写数据会先放在指定的数组中,满足刷新条件后再读写文件,所以此处最好不要把size设置的比数组大,容易出大问题。
当设置无缓冲时,缓冲区大小为1,与传入数组和size大小无关。
还有注意,这里如果在没有刷新缓冲区前,把数组中的值修改了,则读写的数据也会改变
程序存储一个数据对象,建立的缓冲区会依照该数据对象来建立,缓冲区的大小是该数据对象到的整数倍
系统创建的缓冲区一般都为512或者512的倍数



