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