标准I/O的由来
标准I/O指的是ANSI C 中定义的用于I/O操作的一系列函数。
只要操作系统安装了C库,标准I/O函数就可以调用。换句话说,如果程序中使用的是标准I/O函数,那么源代码不需要任何修改就可以在其他操作系统下编译运行,具有更好的可移植性。
除此之外,使用标准I/O可以减少系统调用的次数,提高系统效率。标准I/O函数在执行时也会用到系统调用。在执行系统调用时,Linux必须从用户态切换到内核态,处理相应的请求,然后再返回到用户态。如果频繁的执行系统调用会增加系统的开销。为避免这种情况,标准I/O在使用时为用户控件创建缓冲区,读写时先操作缓冲区,在合适的时机再通过系统调用访问实际的文件,从而减少了使用系统调用的次数。
流的含义
标准I/O的核心对象就是流。当用标准I/O打开一个文件时,就会创建一个FILE结构体描述该文件(或者理解为创建一个FILE结构体和实际打开的文件关联起来)。我们把这个FILE结构体形象的称为流,我们在stdio.h里可以看到这个FILE结构体。
typedef struct {
short level; /* fill/empty level of buffer */
unsigned flags; /* File status flags */
char fd; /* File descriptor */
unsigned char hold; /* Ungetc char if no buffer */
short bsize; /* Buffer size */
unsigned char *buffer; /* Data transfer buffer */
unsigned char *curp; /* Current active pointer */
unsigned istemp; /* Temporary file indicator */
short token; /* Used for validity checking */
} FILE; /* This is the FILE object */
这个结构体:1)对 fd 进行了封装;2)对缓存进行了封装 unsigned char *buffer; 这而指向了buffer 的地址,实际这块buffer是cache,我们要将其与用户控件的buffer分开。
标准I/O函数都是基于流的各种操作,标准I/O中的流的缓冲类型有下面三种:
1)、全缓冲。
在这种情况下,实际的I/O操作只有在缓冲区被填满了之后才会进行。对驻留在磁盘上的文件的操作一般是有标准I/O库提供全缓冲。缓冲区一般是在第一次对流进行I/O操作时,由标准I/O函数调用malloc函数分配得到的。
术语flush描述了标准I/O缓冲的写操作。缓冲区可以由标准I/O函数自动flush(例如缓冲区满的时候);或者我们对流调用fflush函数。
2)、行缓冲
在这种情况下,只有在输入/输出中遇到换行符的时候,才会执行实际的I/O操作。这允许我们一次写一个字符,但是只有在写完一行之后才做I/O操作。一般的,涉及到终端的流--例如标注输入(stdin)和标准输出(stdout)--是行缓冲的。
3)、无缓冲
标准I/O库不缓存字符。需要注意的是,标准库不缓存并不意味着操作系统或者设备驱动不缓存。
标准I/O函数时库函数,是对系统调用的封装,所以我们的标准I/O函数其实都是基于文件I/O函数的,是对文件I/O函数的封装,下面具体介绍·标准I/O最常用的函数:
一、流的打开与关闭
使用标准I/O打开文件的函数有fopen() 、fdopen() 、freopen()。他们可以以不同的模式打开文件,都返回一个指向FILE的指针,该指针指向对应的I/O流。此后,对文件的读写都是通过这个FILE指针来进行。
fopen函数描述如下:
所需头文件 | #include |
函数原型 | FILE *fopen(const char *path, const char *mode); |
函数参数 | path: 包含要打开的文件路径及文件名 mode:文件打开方式 |
函数返回值 |
成功:指向FILE的指针 失败:NULL |
mode用于指定打开文件的方式。
关闭流的函数为fclose(),该函数将流的缓冲区内的数据全部写入文件中,并释放相关资源。
fclose()函数描述如下:
所需头文件 | #include |
函数原型 | int fclose(FILE *stram); |
函数参数 | stream:已打开的流指针 |
函数返回值 |
成功:0 失败:EOF |
二、流的读写
1、按字符(字节)输入/输出
字符输入/输出函数一次仅读写一个字符。
字符输入函数原型如下:
所需头文件 | #include |
函数原型 | int getc(FILE *stream); int fgetc(FILE *stream); int getchar (void); |
函数参数 | stream:要输入的文件流 |
函数返回值 |
成功:读取的字符 失败:EOF |
函数getchar等价于get(stdin)。前两个函数的区别在于getc可被实现为宏,而fgetc则不能实现为宏。这意味着:
1)getc 的参数不应当是具有副作用的表达式。
2)因为fgetc一定是一个函数,所以可以得到其地址。这就允许将fgetc的地址作为一个参数传给另一个参数;
3)调用fgetc所需时间很可能长于调用getc,因为调用函数通常所需的时间长于调用宏。
这三个函数在返回下一个字符时,会将其unsigned char 类型转换为int类型。说明为什么不带符号的理由是,如果是最高位为1也不会使返回值为负。要求整数返回值的理由是,这样就可以返回所有可能的字符值再加上一个已出错或已达到文件尾端的指示值。在
注意,不管是出错还是到达文件尾端,这三个函数都返回同样的值。为了区分这两种不同的情况,必须调用ferror或feof。
#include
int ferror (FILE *fp);
int feof (FILE *fp);
两个函数返回值;若条件为真则返回非0值(真),否则返回0(假);
在大多数实现中,为每个流在FILE对象中维持了两个标志:
出错标志。
文件结束标志。
字符输出-函数原型如下:
所需头文件 | #include |
函数原型 | int putc (int c ,FILE *stream); int fputc (int c, FILE *stream); int putchar(int c); |
函数返回值 | 成功:输出的字符c 失败:EOF |
putc()和fputc()向指定的流输出一个字符(节),putchar()向stdout输出一个字符(节)。
2、按行输入、输出
行输入/输出函数一次操作一行。
行输入函数原型如下:
所需头文件 | #include |
函数原型 | char *gets(char *s); char *fgets(char *s,int size,FILE *stream); |
函数参数 | s:存放输入字符串的缓冲区首地址; size:输入的字符串长度 stream:对应的流 |
函数返回值 |
成功:s 失败或到达文件末尾:NULL |
这两个函数都指定了缓冲区的地址,读入的行将送入其中。gets从标准输入读,而fgets则从指定的流读。
gets函数容易造成缓冲区溢出,不推荐使用;
fgets从指定的流中读取一个字符串,当遇到 \n 或读取了 size - 1个字符串后返回。注意,fgets不能保证每次都能读出一行。 如若该行(包括最后一个换行符)的字符数超过size -1 ,则fgets只返回一个不完整的行,但是,缓冲区总是以null字符结尾。对fgets的下一次调用会继续执行。
行输出函数原型如下:
所需头文件 | #include |
函数原型 | int puts(const char *s); int fgets(const char *s,FILE *stream); |
函数参数 | s:存放输入字符串的缓冲区首地址; stream:对应的流 |
函数返回值 |
成功:非负值 失败或到达文件末尾:NULL |
函数fputs将一个以null符终止的字符串写到指定的流,尾端的终止符null不写出。注意,这并不一定是每次输出一行,因为它并不要求在null符之前一定是换行符。通常,在null符之前是一个换行符,但并不要求总是如此。
下面举个例子:模拟文件的复制过程:
#include
#include
#include
#include
#define maxsize 5
int main(int argc, char *argv[])
{
FILE *fp1 ,*fp2;
char buffer[maxsize];
char *p,*q;
if(argc < 3)
{
printf("Usage:%s \n",argv[0]);
return -1;
}
if((fp1 = fopen(argv[1],"r")) == NULL)
{
perror("fopen argv[1] fails");
return -1;
}
if((fp2 = fopen(argv[2],"w+")) == NULL)
{
perror("fopen argv[2] fails");
return -1;
}
while((p = fgets(buffer,maxsize,fp1)) != NULL)
{
fputs(buffer,fp2);
}
if(p == NULL)
{
if(ferror(fp1))
perror("fgets failed");
if(feof(fp1))
printf("cp over!\n");
}
fclose(fp1);
fclose(fp2);
return 0;
}
执行结果如下:
fs@ubuntu:~/qiang/stdio/cp$ ls -l
total 16
-rwxrwxr-x 1 fs fs 7503 Jan 5 15:49 cp
-rw-rw-r-- 1 fs fs 736 Jan 5 15:50 cp.c
-rw-rw-r-- 1 fs fs 437 Jan 5 15:15 time.c
fs@ubuntu:~/qiang/stdio/cp$ ./cp time.c 1.c
cp over!
fs@ubuntu:~/qiang/stdio/cp$ ls -l
total 20
-rw-rw-r-- 1 fs fs 437 Jan 5 21:09 1.c
-rwxrwxr-x 1 fs fs 7503 Jan 5 15:49 cp
-rw-rw-r-- 1 fs fs 736 Jan 5 15:50 cp.c
-rw-rw-r-- 1 fs fs 437 Jan 5 15:15 time.c
fs@ubuntu:~/qiang/stdio/cp$
我们可以看到,这里将time.c拷贝给1.c ,1.c和time.c大小一样,都是437个字节;
3、以指定大小为单位读写文件
三、流的定位
四、格式化输入输出
这里举个相关应用例子:循环记录系统时间
实验内容:程序每秒一次读取依次系统时间并写入文件
#include
#include
#include
#define N 64
int main(int argc, char *argv[])
{
int n;
char buf[N];
FILE *fp;
time_t t;
if(argc < 2)
{
printf("Usage : %s \n",argv[0]);
return -1;
}
if((fp = fopen(argv[1],"a+")) == NULL)
{
perror("open fails");
return -1;
}
while(1)
{
time(&t);
fprintf(fp,"%s",ctime(&t));
fflush(fp);
sleep(1);
}
fclose(fp);
return 0;
}
执行结果如下:
fs@ubuntu:~/qiang/stdio/timepri$ ls -l
total 12
-rwxrwxr-x 1 fs fs 7468 Jan 5 16:06 time
-rw-rw-r-- 1 fs fs 451 Jan 5 17:40 time.c
fs@ubuntu:~/qiang/stdio/timepri$ ./time 1.txt
^C
fs@ubuntu:~/qiang/stdio/timepri$ ls -l
total 16
-rw-rw-r-- 1 fs fs 175 Jan 5 21:14 1.txt
-rwxrwxr-x 1 fs fs 7468 Jan 5 16:06 time
-rw-rw-r-- 1 fs fs 451 Jan 5 17:40 time.c
fs@ubuntu:~/qiang/stdio/timepri$ cat 1.txt
Tue Jan 5 21:14:11 2016
Tue Jan 5 21:14:12 2016
Tue Jan 5 21:14:13 2016
Tue Jan 5 21:14:14 2016
Tue Jan 5 21:14:15 2016
Tue Jan 5 21:14:16 2016
Tue Jan 5 21:14:17 2016
fs@ubuntu:~/qiang/stdio/timepri$