输入输出专题:
输入输出也就是IO操作,无论是系统编程中还是嵌入式开发中都是最基本的实现形式,反映在用户面前。
对于UNIX(Linux)系统而言,IO操作是所有系统操作的基础。有了IO操作才能让系统中进程间通信、线程间通信、文件传输成为可能。所以如何优化IO操作是优化系统的关键。
以下给出函数的常见用法,具体实现需要查询man手册!!
一、格式化输入输出函数:scanf,printf:
1、用法:
用man 3 printf可查询其用法: printf(fomat:"%[修饰符][格式字符]",输出表项)
scanf(const char * format ,地址表)
2、格式字符和修饰符:
格式字符和修饰符在把控时间日期的输入输出、特定格式的数字处理上非常重要。
格式字符:
-
%d
或%i
- 用于输出一个十进制的整数。 -
%u
- 用于输出一个无符号整数。 -
%x
或%X
- 用于以十六进制形式输出整数,%x
输出小写字母,%X
输出大写字母。 -
%o
- 用于以八进制形式输出整数。 -
%f
- 用于输出一个浮点数,以小数点表示。 -
%e
- 用于以科学计数法形式输出浮点数。 -
%g
- 用于输出浮点数,根据数值的大小自动选择%f
或%e
格式。 -
%a
或%A
- 用于以十六进制或八进制浮点数形式输出,%a
输出小写字母,%A
输出大写字母。 -
%c
- 用于输出一个字符。 -
%s
- 用于输出一个字符串。 -
%%
- 输出一个百分号字符。 -
%n
- 用于输出时不打印任何字符,但会将到目前为止已经打印的字符数量存储在相应的参数中。
修饰字符:
-
%-
表示左对齐输出。 -
%+
表示正数前加正号。 -
% 0
表示数字前面填充0而不是空格。 -
%*
表示宽度由后面的参数指定。 -
%.*
表示精度由后面的参数指定。 -
%ld
中的l
表示长整型(long int
)。 -
%lld
中的ll
表示长长整型(long long int
)。 -
%f
中的f
可以替换为F
、e
、E
、g
或G
来改变浮点数的输出格式。
⚠️:
%lld可以用于防止数据溢出,如在计算机中表示5g的大小:5* 1024* 1024* 1024
如果使用整型修饰,signed int的范围是-2g-1~2g,数据溢出,需要规定数据类型为long,并且加上单位(LL)5LL*1024LL......(或者LL)
3、scanf的优化:
1、脏数据
scanf在读取数据时格式需要严格把控,因为输入的数值与规定类型不匹配等等情况时会将错误的数据留在输入缓冲区中,形成脏数据。当下一次读取时,输入缓冲区中存在当前输入的值和脏数据。
解决方法:1、每次输入时校验返回值,scanf返回成功读取的输入数量
2、也可以将缓冲区中的脏代码清除:
①在scanf()后使用fflush函数刷新输入缓冲区:fflush(stdin)
②用setbuf(stdin,NULL)刷新缓冲区
③用while循环检测输入缓冲区,直到缓冲区没有数据。(脏数据被getchar获取)
while(c=getchar()!=EOF&&c!='\n')
scanf();
此外,可以用该循环机制实现输入的校验,即输入错误时重复scanf输入。
2、scanf输入的格式
scanf在同时输入多个值时可以在后值前加入抑制符*放在%c(或者其他)前面,或者分作多个scanf然后在下一个scanf前加getchar()
他们的作用都是吃掉一个字符
二、缓冲机制:
buff(缓冲区)是用户态读写数据的暂存区,缓冲区主要用于解决速率不匹配问题和减少I/O操作,而缓存主要用于提高数据访问速度。
1、缓冲区类型:
行缓冲:换行、满了的时候刷新,强制刷新(标准输出)
全缓冲:满了的时候刷新,强制刷新(只要不是终端设备,默认执行,一般为系统的文件IO操作),此时需要用fflush(fp)进行刷新
无缓冲:如stderr,直接输出内容
setvbuf():更改文件的缓冲模式
实例:
例如:
printf(“·······”);
while(1){
printf(“·······”);
}
此时没有换行符;缓冲区停留在while中不会打印任何东西
在第一个printf后加上fflush(stdout);最后一个加上fflush(null);清空所有缓冲区,即可正常输出,即强制刷新缓冲区。
当然在printf中加上换行符也可以做到刷新缓冲区。
2、缓冲区的运用:
缓冲区是标准IO的一大特点,相对应系统IO则通常不用缓冲区,而是直接或间接操作内存和对硬件交互。
当用户需要程序运行速度变快,需要从吞吐量和响应速度两方面来分析:标准IO有缓冲器可以减少磁盘IO的次数,等到缓冲区满了才进行输出,每次吞吐量很大。而系统IO的磁盘IO占用很大,成本很高,但是每次输入输出响应速度很快。
在一般情况下,用户需要吞吐量大的IO,因为响应速度的优势体现在一些特定的精密的计算中,而不是面向大众。
缓冲区的溢出:
在输入输出函数的应用中,很多函数比如gets、sprintf函数,不能检查缓冲区溢出的情况,所以一般用 fgets、getline、snprintf这样的有缓冲区保护机制的函数。
三、标准IO
1、什么是标准IO
标准IO:stdio
标准IO函数在多种操作系统和平台上可用,在换了不同系统kernrl时,系统IO不适用 ,所以利用stdio。标准io移植性强,优先考虑使用stdio。
2、文件流
FILE类型在标准IO中贯穿始终:FILE 是标准IO库中定义的一个结构体,它用于表示一个流的状态。
流则是一个抽象的概念,是用于输入输出的数据源。
3、常用函数
1、fopen
参数:
路径名,模式:r,r+,w,w+ --> 读 ,读写,截断或创建写,读和截断或创建写。
函数返回值是一个FILE* 也就意味着返回结构体的首地址
fopen是打开一个FILE流,这个流在创建时申请内存所以储存在heap中。
相应的fclose其实对应一个销毁空间的操作。
当打开多个文件时,fclose一般先关闭依赖的文件,再关闭被依赖的文件。
2、fread和fwrite函数:
参数:buffer区(即读写指针,一般用一块较大的数组空间),读的大小(即数组大小BUFSIZE),每次读的大小,文件流
返回值是读写的个数
该函数因为需要指定读取的字符个数、缓冲区大小,所以用于读取成块的数据。最好将参数设置为每次只读一个字节,避免读到的数据小于该读的大小。
3、printf函数族:
fprintf:
可以做到输出重定向,不像printf只能输出到stdout上:比如在读取文件时,可以将错误内容重定向到stderr中,进而直接输出到终端上。
在后面检错章节会有实例。
sprintf:
可以将指定格式的数据放入到字符串里。例如,你不能将一堆整型12345当做一个字符串来输出,而反之一串字符串却是可以利用atoi这样的函数来当整数输出。
但是sprintf不检查缓冲区的大小,snprintf规定字符串的大小,但是只能读取size-1个大小的字符:
4、scanf族函数:类似printf
5、fseek、ftell、rewind:
这是一个调整文件指针位置的函数族,文件指针就是指明输入和输出位置的参数,文件打开后文件指针只有关掉之后才会重置:
读取和写入的函数在取值的时候都能做到精准取值(取到哪里,取多少)都可以解决。
fseek:变更指定文件指针位置
ftell:返回当前文件指针位置
rewind:重置指定文件指针位置
参数:
指定的文件流、指针偏移量,指针位置。
指针位置有三个:文件开头、当前位置、文件末尾
返回值:
失败返回-1,成功ftell返回偏移量,fseek返回0
off_t:
由参数可知,当调整文件指针位置时,最多可以调节long类型的大小,即32位。而很多服务器上的日志(log)很容易能超过2g,所以重新定义了一个数值off_t和其对应函数:
其中off_t数据类型比较特殊,编译的时候需要在终端加入:
gcc x.c -o xx -D_FILE_OFFSET_BITS=64
或者在makefile中加上:
CFLAGS+= -D_FILE_OFFSET_BITS=64
来声明off_t数据类型
举例:
fputc10次之后,用fgetc读取该10个字符,此时读取的文件指针是在写入的10个字符后。所以在两个操作中间应该加上:
fseek(fp,-10,seek_cur) 即向前偏移10个字符,重新回到输出开头
拓展:剪切文档:
对一个流(源文件)进行读操作,指针定位到第10行,对另外一个流(目标文件)进行读写操作,指针定位到剪切位置,然后进行fwrite,将从源文件fread的值fwrite到目标文件中去,实现剪切
6、fflush:前文已介绍过的刷新缓冲区机制
7、getline:
前面说过的一系列输入输出函数,虽然很多函数都加入了校验缓冲区溢出的机制,但是始终无法准确的表达自己的输出范围和具体大小,所以他们在使用中都存在一定缺陷
getline函数是gets的优化,但是其数据类型比较复杂(某些编译环境下需要在声明前加上#define _GUN_SOURCE ):
参数:lineptr即为buffer,传入buf的地址,*n为每行的大小,传入大小的地址。
返回值:返回读到的字符个数
理论上lineptr是malloc再realloc后的一块空间。所以需要释放。
4、报错检测:
errno
(错误号)是一个在 <errno.h>
头文件中定义的全局变量,用于存储最近一次系统调用或库函数调用失败时的错误代码。这个变量通常在调用失败后由系统或库函数自动设置,以提供错误诊断信息。
1、linux系统中有相应的错误库
2、perror函数:IO函数中一般会设置errno可以用perror自动将errno转换为erromassage,可以自动关联全局变量errno
if(fp==NULL)
perror("fopen()");
3、stderr流报错:
fprintf(stderr,"xxxx()error");
用fprintf函数指定错误流输出,也可以用strerror函数直接引用errno进行报错:
fprintf(sterr,"fopen():%s\n",strerroe(errno));
4、函数返回值检测:
例如fread、fwrite函数的返回值,返回读到的个数,所以要验证n=fread是否> 0。;
再例如fgets返回的是缓冲区指针,缓冲区指针不为空就是读取到数据,所以fgets!=NULL;
还有读取字符的函数,可以检测其返回值是否为EOF、/n、/r等标志一行或者文件结束的标志符来进行校验
总结:报错检测还是要基于函数的基本用法,所以一切皆以man手册为准!
四、系统IO
1、什么是系统sysIO
user对系统kernel进行访问的时候利用sysio。
系统IO是操作系统提供给应用程序的接口,用于执行各种IO操作。在Linux系统中,这些操作通常通过系统调用实现,这些系统调用由内核提供支持,并在用户空间和内核空间之间进行数据交换。
2、文件描述符fd
文件描述符:fd:file description,整型数int。
是指向文件FILE这个结构体的首地址的指针所存放在的数组中的下标,而文件描述符优先使用当前范围内最小的。所以fd存放在每一个FILE 结构体中(当然结构体中还包含文件指针*pos)
所谓的指针数组就是maxfopen——流(stream)
一个进程打开默认打开了三个流:stdin、stdout、stderr,所以打开文件的最大数量限制:1024-3=1021个。可以在终端中使用ulimit -a查看所有所有文件相关属性的大小限制:
其中可以利用第二列的选项对其进行设置:
ulimit -n 2048 //对能打开的文件最大数进行修改(一般不建议修改)
fd有几种特性:1、指向一个结构体地址2、可以多个fd指向相同的结构体,在这个过程中,结构体还是只有一个,但是结构体中有count来记录有几个fd在指向
2.1、重定向
对标准IO进行重定向,例如printf不将其输出到终端上,那么需要对标准IO进行重定向,而标准IO位于文件描述符的1位置,所以更改文件描述符数组【1】即可。
在更改文件描述符的时候可以先用close(fd)来关闭标准输出,再用dup(fd)函数进行拷贝副本,此时可能出现fd就是你要复制的那个位置,也就是说close了fd,也就不能再dupfd了,此时应该用dup2(old,new)来进行复制:
终端命令:
在终端上可以用这样的命令将程序的标准输出重定向到指定文件中
./<程序名> > /tmp/out(或其他文件的路径)
3、常用函数:
1、open
因为是系统调用所以需要包含较多的头文件,open用flags指定的方式打开指定路径的文件。
注:两个open由变长参数实现,如同printf:(同函数不同参数的形式可以由重载或者可变长参数实现,可以输入多个参数进行编译,如果出现报错则为由重载实现,不报错则是可变长参数)
参数:
不更改的路径名,打开方式和打开模式
打开方式: O_RDONLY,O_WRONLY, or O_RDWR是常见的打开方式,他们的实质是位图。但是我们常常会加入一些特定功能来对文件进行操作:
fopen: open:
r O_RDONLY
r+ O_RDWR
w O_WRONLY(文件不存在就要创建:|O_CREAT 存在的话就截
断|O_TRUNC
w+ O_RDWR|O_CREAT|O_TRUNC
这是fopen功能和open功能的对比,open的flags需要进行按位或的操作才能替代fopen的功能。
当然除了以上还有其他功能,不过在功能使用的时候,如果出现O_CREAT or O_TMPFILE选项 则需要设置mode
mode其实就是文件权限,他的用法涉及文件系统,此处不做介绍,可以查询man手册进行设置。
返回值:返回打开的文件描述符,错误则返回-1 。
2、close、read、write等其他函数:
其他函数的用法与标准IO大同小异,主要在于FILE结构体指针换为fd。以下是对应的函数及其参数:
3.1、系统调用的函数的校验:
read和write在系统中可能会因为程序被中断、非阻塞IO操作等情况导致文件没有写入预期的数量的字符,这时候也需要校验返回值(返回字节数):
中断输入输出后需要将buf的读取或输出位置进行更改,改为上一次的读取位置。最常用的方法就是在循环读取的过程中,加入变量来记录读取的位置,每次读取完进行累加:
size_t len=xxx,ret,pos=0;
while(len>0){
ret=write(fd,buf+pos,len);
if(ret<0)
perror(xxx);
pos+=ret;
len-=ret;
}