标准IO
前言
一、为什么需要标准IO
因为每个操作系统下面,对文件的管理和接口是不一样的。
linux:open/read/write/close…,struct file, struct inode
windows:winopen,winclose,…
同一个文件,在不同的操作系统下面,我们的操作文件的代码都不一样。
c语言标准委员会,就统一了文件操作的接口
–》标准IO库: 主要统一对文件操作的接口
“文件”: 普通的文本文件和二进制文件
文本文件: 无组织、无格式的文件,以字符的ASCII码等其他编码来解析的文件。
如:.txt .c .h .s .cpp .java …
二进制文件: 有特定格式的文件。
如:.jpg .bmp .gif .mp3 .mp4
在标准IO库,用结构体 FILE结构体来描述或表示一个文件,然后在这个结构体中创建了两个缓冲区(一段内存),一个读缓冲区,一个写缓冲区
FILE
{
char *in; //指向读的缓冲区
char *out //指向写的缓冲区
};
还提供了对"文件"操作的接口函数:
fopen/fclose/fread/fwrite/fseek/…
puts/gets/fputs/fgets/scanf/printf…
使用标准IO的实现过程:
Your Code —>标准IO库(如:fopen/fclose…) -->系统IO -->内核–>Hardware
FILE 有两个缓冲区(标准IO库中开辟的一段内存)
*in —> 读的缓冲区
*out --> 写的缓冲区
标准IO带缓存的IO,IO流,它的效率要比系统IO要高,why?
beacase
系统IO: read 1byte 从硬盘中读一个字节出来
标准IO:
read 1byte,它会从硬盘上读一块(如:512byte)出来,放到 标准IO缓冲区。(内存条)
标准IO:IO流,stream跟水流一样,流走了就没有了,读走了就没有了
缓冲: 同步的问题。
缓冲区中的数据,何时同步到外设上去呢?
缓存区开多大呢? 标准IO缓冲区有三种类型:行缓冲、全缓冲、无缓冲
行缓冲:缓冲区的数据达到一行,同步到外设上去。
假设你设置一行顶多80个字节。遇到\n(称换行符,一行结束的标志)就会把缓冲区中的数据同步到外设上去
printf —> 行缓存
main()
{
printf("hello");
while(1);
}
此时在屏幕上没有“hello”打印出来,因为没有hello后面没有‘\n’,hello一直在缓冲区中,并没有把缓冲区中的数据同步到外设上去。
全缓冲: 缓冲区中数据要填满整个缓冲区,才同步到外设上去。
无缓冲: 缓冲区中有一个字节,就同步到外设上去。
perror --> 无缓冲
标准IO库,会自动为每个进程,打开三个标准IO流(文件):
标准输入流:FILE *stdin
stdin 是定义在<stdio.h>中的一个全局变量,它指向标准输入设备(一般为终端或键盘)
scanf() <-----stdin
标准输出流: FILE *stdout
stdout是定义在<stdio.h>中的一个全局变量,它指向标准输出设备(控制台console,终端或屏幕)
printf() <----stdout
标准出错:FILE *stderr
stderr是定义在<stdio.h>中的一个全局变量,它指向标准出错设备(一般为终端)
perror() <---- stdout
二、标准IO的函数接口
(1)标准IO打开或关闭一个文件流
fopen
头文件
#include <stdio.h>
函数功能
用来打开一个普通文件(文本文件/二进制文件)
函数原型
FILE *fopen(const char *pathname, const char *mode);
函数参数
const char *pathname //要打开的那个文件的文件名
const char *mode //打开文件的方式,有如下集中:
“r”: 只读打开。文件不存在,则报错; 打开后,光标在开头。
"r+": 读写打开。文件不存在,则报错;打开后,光标在开头。
"w": 只写打开。文件不存在,则创建; 打开后,文件内容截短(文件内容被清掉)。
"w+": 读写打开。文件不存在,则创建; 打开后,文件内容截短(文件内容被清掉)
“a”: append 追加打开,文件不存在,则创建; 打开后,光标在末尾。文件内容不会被截短。
“a+”:读写打开。文件不存在,则创建。 原始读的位置在开头,原始写的位置在末尾
返回值
成功:返回打开文件指针。 FILE *
在标准IO中,FILE *代表一个打开的文件,后续标准IO库的函数都需要要到它。
失败:返回NULL,同时errno被设置
fclose:用来关闭指定的文件流
头文件
#include <stdio.h>
函数功能
用来关闭stream指定的文件流
函数原型
int fclose(FILE *stream);
函数参数
FILE *stream //要关闭的文件的FILE *
返回值
成功: 返回0
失败: 返回-1,同时errno被设置
(2)读写流:一旦读写成功啦,光标会自动往后移n个位置(n就是你读写成功的字节数)
a:每次读一个字符读写
fgetc/getc/getchar
fput/putc/putchar
b:每次一行读写
fgets/gets
fputs/puts
c:直接读写,你想要读多少个对象都可以
fread
rwrite
(2.1)每次一个字符读写
fgetc/getc/getchar
fput/putc/putchar
头文件
#include <stdio.h>
函数功能
用来从stream指定的文件流中,读取一个字符。
函数原型
int fgetc(FILE *stream);
函数参数
FILE *stream //指定要从哪个文件流中读取字符
返回值
成功: 返回读到的那个字符的ASCII码
失败:返回-1,同时errno被设置。
get和fget一样,也是用来从stream指定的文件流中,读取一个字符。getc和fget的区别在哪里?
fgetc是一个函数;
getc可能是用用来实现的
函数原型
int getc(FILE *stream);
getcha是用来从标准输入流stdin中获取一个字符,并把读取到的字符的ASCII码返回。
函数原型
int getchar(void); 《==》 fgetc(stdin);
头文件
#include <stdio.h>
函数功能
用来把c指定的字符,输出到stream指定的文件流中去,
函数原型
int fputc(int c, FILE *stream);
函数参数
int c //要输出的字符的ASCII码
FILE *stream //文件流,表示输出到哪个文件中去。
返回值
成功:返回实际写入到文件流中的字符的ASCII码(c值)
失败:返回-1,同时errno被设置。
putc与fputc功能与返回值 一样,只不过putc的实现可能是由宏来实现的
函数原型
int putc(int c, FILE *stream);
putchar用来把c指定的字符,输出到"标准输出流文件中"
函数原型
int putchar(int c); 《==》 fputc(c,stdout);
(2.2)每次一行读写
fgets/gets
fputs/puts
头文件
#include <stdio.h>
函数功能
用来从标准输入流(stdin)获取一行字符,存储到s指向的内存空间中去。
函数原型
char *gets(char *s);
函数参数
char *s //指向的空间,用来存储从输入缓冲区获取到的多个字符
返回值
成功:非空 NULL
失败:返回NULL,同时errno被设置
注意:
gets有一巨大的bug,你懂的!!!
gets没有考虑到s指向的空间的大小问题,存在越界的可能。 所以从今天开始,gets你们就不要用啦。
fgets修正了gets的这个bug
函数功能
从stream所指向的文件中读取size个字节大小的数据到s指向的空间中
函数原型
char *fgets(char *s, int size, FILE *stream);
函数参数
char *s //指向的空间用来保存从文件流中读取的数据
int size //表示你要最多获取size个字节,size一般为s指向的
空间的可用长度。
fgets输入结束有两种情况:
(1)遇到\n或文件结束
(2)已经读取到size-1个字节啦(后面留一个\0的位置)
FILE *stream //FILE *指针,表示从哪个文件流中读取数据
返回值
成功:返回s的首地址
失败:返回NULL,同时errno被设置
头文件
#include <stdio.h>
函数功能
fputs用来把s指向的字符串,输出到stream指定的文件流中去
函数原型
int fputs(const char *s, FILE *stream);
函数参数
const char *s //指向要输出的字符串的首地址
FILE *stream //表示要输出到哪个文件流中去
返回值
成功:返回一个非负数
失败:返回-1,同时errno被设置。
(2.3)直接读写 fread/ fwrite
fread
头文件
#include <stdio.h>
函数功能
从stream指定的文件流中,读取nmemb个对象且每个对象size字节,读取到ptr指向的内存空间中去。
函数原型
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
函数参数
void *ptr //指向的内存空间,用来保存从文件流读取到的"数组"
size_t size //每个元素占的字节大小
size_t nmemb //要读取的n个元素
FILE *stream //表示要从哪个文件流中读取数据
返回值
成功:返回实际读取到的元素个数 <= nmemb
失败:返回-1,同时errno被设置。
fwrite
头文件
#include <stdio.h>
函数功能
用来把ptr指向的nmemb个元素(每个元素占size字节)写入
到stream指向的文件流中去。
函数原型
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
函数参数
const void *ptr //要写入的数组的首地址
size_t size //每个元素所占的大小
size_t nmemb //元素的个数
FILE *stream //表示要写到哪个文件流中去
返回值
成功:返回实际写入到文件流中去的元素个数, <= nmemb
失败:返回-1,同时errno被设置。
(3)冲洗流 fflush,同步
头文件
#include <stdio.h>
函数功能
将缓冲区数据冲洗到对应的设备上或者丢弃(同步到硬件设备)
对输出流,fflush把写缓冲区的内容写/更新到文件中去;
对输入流,fflush把读缓冲区的内容直接discards(丢弃);
strea为NULL,fflush把该进程所有打开的输出流文件同步
函数原型
int fflush(FILE *stream);
函数参数
FILE *stream //要同步的文件流
返回值
成功:返回0
失败:返回-1,同时errno被设置。
练习:
请分析如下程序的输出结果?
main()
{
printf("hello"); //把hello写到文件 stdout的 输出缓冲区
//中去了,但并没有写入到外设文件中去
sleep(10);
fflush(NULL); //fflsh(stdout); //if stream 输出流,把输出缓冲区的内容写到
//外设文件中去
//if stream 输入流,把输入缓冲区的内容丢弃,这样的话
//你下次读,就可以重新从外设文件中读啦
while(1);
}
(4)定位流
上面讲到的,fread/fwrite …只是说要从哪个文件,读/写多少个字节的数据并没有指定从文件流的哪个位置开始读写,标志IO库会为每个开打的文件流保存一个“文件偏移量”
“文件偏移量”:offset,“光标”。直接确定下一个读/写的起始位置。
一般来说,每次读或写之前,先要定位流
头文件
#include <stdio.h>
函数功能
定位一个文件的偏移量
函数原型
int fseek(FILE *stream, long offset, int whence);
函数参数
FILE *stream //文件流指针,表示你要定位哪个文件
long offse //偏移量,可正可负;要结合第三个参数
int whence //从哪里开始定位 定位的方式,有如下三种:
SEEK_SET:基于文件开头定位
新光标位置 = 文件开头 + offset(>=0)
SEEK_CUR:current基于当前光标位置定位
新光标位置=当前位置 + offset(可正可负)
SEEK_END:基于文件末尾定位
新光标位置=文件末尾 + offset(可正可负)
返回值
成功:返回0
失败:返回-1,同时errno被设置。
ftell返回当前光标位置离文件开头有多少字节
函数原型
long ftell(FILE *stream);
rewind把文件光标,定位在文件开头
函数原型
long ftell(FILE *stream);
rewind(stream) => fseek(stream, 0, SEEK_SET);
(5)文件出错/结束标记
头文件
#include <stdio.h>
函数功能
判断stream指定的文件流是否结束
函数原型
int feof(FILE *stream);
函数参数
FILE *stream //填要判断的文件流
返回值
如果返回真(非0) 文件到达末尾啦
如果返回假(0) 该文件还没有到达末尾。
注意:
标准IO库,在读到文件末尾时,会往缓冲区中填入一个EOF(二进制 11111111)
(6)格式化IO
(6.1)格式化输入
scanf/sscanf/fscanf
“格式化输入”?:按照我指定的格式来输入。
头文件
#include <stdio.h>
函数功能
按照我指定的格式来输入
函数原型
int scanf(const char *format, …);
函数参数
const char *format,…
scanf可以带很多个参数,scanf的参数可以分为两类:
第一个参数为第一类,格式化字符串,format string:
"格式化字符串"就是告诉用户怎么输入的,意思是你必须按照它指定的格式去输入。
在“格式化字符串”中有三类字符:
a.空白符(space tab)
指示用户 你可以输入任意数量的空白符(包含0个)scanf把\n当作是输入结束
b.非转义字符(普通字符,除空白符和%以外的字符)精准匹配,你得原样输入
c.转义字符(以%开头的字符),有特殊含义:
%d -> [0-9]+
%c -> 匹配一个字符(可以输入的字符)
%f -> 浮点数
%s -> 字符串(不带空白符,scanf把空白符当作是一个分割符号)
其他参数为第二类,地址列表:
格式化字符串中一个转义字符会对应一个地址,把一个转移字符的输入存储到指定的地址中去,如果转移字符的个数 多余 地址个数,程序行为将是 undefined(未定义的)
返回值
成功匹配的变量个数!!!
scanf获取输入时,如何结束?scanf从stdin的读缓冲去中获取输入
a.该输入的都输入完了
sacnf(“abcd%d %cabcd”,&a,&c);
用户输入:
abcd1234Aabcd -> 该输入的都输入啦,scanf结束
b.失败啦
scanf(“abcd%d %cabcd”,&a,&c);
用户输入:
ABCD-》scanf停止匹配啦
例子:
int r;
int a;
char c;
r = scanf(“abcd%d %c”,&a,&c);
假设用户入:
ABCD123 A
a=?
c=?
r=?
不匹配所以a与c不是123与A ,r=0;
r = scanf("%d %c",&a,&c);
加入用户输入:
123B
a=123
c=B
r=2
sscanf它的功能与返回值 ,和scanf一样的,只不过sscanf的输入来源不是stdin,而str指向的字符串。
sscanf的参数有三类:
1.str是输入来源字符串
2.format是格式化字符串
3.其他参数为地址列表
函数原型
int sscanf(const char *str, const char *format, …);
例子:
(1)char *str = “123456sdadwa”
int r;
int a;
char c;
r = sscanf(str, “%d %c”, &a,&c);
a=?
c=?
r=?
(2)
char *s = “123”;
int a;
sscanf(s,"%d",&a);
fscanf它的功能和返回值,与scanf一样,只不过,fscanf它的输入
来源不是stdin,而stream指定的文件流,所以fscanf的参数,分为三类:
1.第一个参数FILE *,指定输入来源
2.第二个参数format,格式化字符串,与scanf是一样
3.其他参数为 地址列表
函数原型
int fscanf(FILE *stream, const char *format, …);
scanf(format,…) <==> fscanf(stdin,format,…)
(6.2)格式化输出
printf/sprinft/snprintf/fprintf
“格式化输出”?:按照我指定的格式去输出。
头文件
#include <stdio.h>
函数功能
按照指定的格式去输出
函数原型
int printf(const char *format, …);
函数参数
const char *format, …
printf可以带多个参数 ,这么多参数,可以分为两类:
第一个参数为第一类 格式化字符串,就是告诉你怎么输出的
格式化输出字符串有两类:
a.转义字符:以%开头的字符
%d -> 按十进制有符号整数输出
%u
%ld
%lu
%f
%c ->输出字符的形状
%s ->把后面的地址,字符串去输出 直到遇到\0
…
b.非转义字符
你得原样输出
其他参数为第二类,要输出得变量或对象列表
要输出得变量或对象个数 应该与转义字符的个数一致
返回值
实际打印的字符个数
例子:
int a = 123;
char c = ‘A’;
int r;
r = printf(“a = %d c = %c”,a,c);
r =?
fprintf它的功能和printf一样的,只不过fprintf
输出不是输出到stdout,而是输出到stream指定的
文件中。所以fprintf它的参数,可以分为三部分:
第一个参数为 FILE *,指定要输出到哪个文件中去
第二个参数 format,格式化字符串
其他参数,为要输出的变量或对象列表
函数原型
int fprintf(FILE *stream, const char *format, …);
返回值 :
为实际输出到文件中的字符个数
printf(format,…) <==> fprintf(stdout,format,…);
sprintf它的功能和printf一样,只不过,sprintf
输出不是输出到stdout,而是输出到str指定的内存中去。
所以sprintf参数,可以分为三部分:
第一个参数为char *,内存地址,指定输出字符串的输出位置
第二个参数为format,格式化字符串
其他参数,为实际输出的变量或对象列表
函数原型
int sprintf(char *str, const char *format, …);
返回值 :
为实际输出到内存中去的字符个数。
例子:
char filename[512];
char *file =“xxx.mp3”;
char *path ="/home/china";
sprintf(filename,"%s/%s",path,file);
注意:但是,sprintf有一个bug
str只是指定了一个内存的起始地址,并没有限定它的内存范围,if输出字符串的长度 > str指向的内存的范围,有越界的风险。
=》内存的非法访问
so,为了解决sprintf的这个bug,才有了 下面的snprintf ,snprintf中size指定str指向的那段内存空间的最大长度,也就是说,格式化的输出顶多输出size-1个字符到str 指向的空间中去。
函数原型
int snprintf(char *str, size_t size, const char *format, …);
例子:
char s[8];
char *str = “1234567890”;
int r;
r = snprintf(s, 8, “%s”,str);
r =
printf("%s",s);
注意:
snprintf它的返回值 是应该输出的字符串长度,而不是实际输出到内存中的长度。
总结
1.标准IO库是什么东西? 为什么需要它?它的实现原理是什么?
2. 标准IO与系统IO之间的关系?标准IO中用什么东西来表示一个文件?
3. 为什么把标准IO称为流?为什么说标准IO的效率要系统IO高?
4.标准IO只能操作 “普通文件”!!!!