今天草草的把第四章结了,后面的内容分析的也不是很详细,就连书中的例子都没有怎么实验,还是等以后有机会吧。
从5.3节开始研究起吧,这一节主要谈了一个进程预定义的3个流,分别是标准输入、标准输出和标准错误,通过stdin、stdout、stderr引用。这里要和进程中的文件描述符STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO相区分。
/* Standard streams. */
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
/* C89/C99 say they're macros. Make them happy. */ //这一句最有意思,让他们乐吧
#define stdin stdin
#define stdout stdout
#define stderr stderr
5.4缓冲
标准I/O提供以下3种缓冲:
对于上面提到的第二点可通过几个简单的实验进行验证,是通过fputc向标准输出输入数据,具体程序如下:
#include
#include
int main(void)
{
char msg[] = "Hello world";
int i = 0;
while (msg[i])
{
fputc(msg[i], stdout); //将fputc函数改成printf效果相同
sleep(1); //写入后程序挂起1s
i++;
}
return 0;
}
运行结果:程序先不输出,最后统一输出msg,通过这个程序基本验证了行缓冲的特点,没有换行符或写满行缓冲区的情况下,不会执行I/O操作,同时也验证了标准I/O使用行缓冲模式。但这个实验又引出了一个问题那就是进行I/O操作的时机,在以上实验中我既没有输出换行符,同时也没有写满缓冲区,暂且现把这个疑问记下,学了后面的知识也许就能解答了。
标准I/O缓冲还具有以下惯例:
可通过以下函数更改缓冲类型。
#include
extern void setbuf (FILE *__restrict __stream, char *__restrict __buf) __THROW;
extern int setvbuf (FILE *__restrict __stream, char *__restrict __buf,
int __modes, size_t __n) __THROW;
setvbuf的功能比较明确,使用modes参数可以设定缓冲模式:
#include
#define _IOFBF 0 /* Fully buffered. */
#define _IOLBF 1 /* Line buffered. */
#define _IONBF 2 /* No buffering. */
强制冲洗一个流。
#include
extern int fflush (FILE *__stream);
若fp为NULL,则此函数将导致所有输出流被冲洗。
5.5打开流
extern FILE *fopen (const char *__restrict __filename,
const char *__restrict __modes) __wur;
extern FILE *freopen (const char *__restrict __filename,
const char *__restrict __modes,
FILE *__restrict __stream) __wur;
extern FILE *fdopen (int __fd, const char *__modes) __THROW __wur;
上述三个函数的区别如下:
ISO规定type参数可以有15种不同的值。
有关于type解释请见:http://www.cnblogs.com/emanlee/p/4418163.html
由于内核不区分文本文件和二进制文件。所以b作为type的一部分实际上并无作用。
对于fdopen函数,若描述符已被打开,则fdopen为写而打开并不截断该文件。另外,标准I/O追加写方式(a或a+)也不能用于创建该文件(因为如果一个描述符引用一个文件,则该文件一定已经存在)。
在使用w或a类型创建一个新文件时,POSIX.1要求使用如下权限来创建文件:
S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
可通过unmask值来更改权限。
按照系统默认,流打开时是全缓冲的。若流引用终端设备,则该设备是行缓冲的。
调用fclose可关闭一个打开的流。
#include
extern int fclose (FILE *__stream);
在该文件被关闭前,冲洗缓冲中的输出数据。缓冲区中的任何输入数据被丢弃。
当一个进程正常终止时(直接调用exit函数,或从main函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。这也解释了我们之前留下的疑问,缓冲区中数据冲洗的时机——进程正常终止时,根据我们之前学习到的知识,应该是在main函数开始之前注册了某个析构函数,这个析构函数的功能就是冲洗缓冲区。
5.6 读和写流
标准I/O提供三种方式进行非格式化读、写操作。
每次一个字符的I/O。由于标准I/O采用行缓冲模式,所以只有出现换行符或写满一行的情况下才会冲洗缓冲区,此时数据已经写入缓冲区中。输入函数如下:
#include
extern int fgetc (FILE *__stream);
extern int getc (FILE *__stream);
extern int getchar (void); 函数getchar()等同于getc(stdin)
getc的具体实现为:
#include
#define getc(_fp) _IO_getc (_fp)
使用int作为返回值的原因是:返回所有可能的字符值在加上一个已出错或已达到文件尾端的指示值。我也查了以下仅是ascii码表就已经包含128个符号,因此使用char作为返回数据远远不够。
EOF的定义为
#include
#ifndef EOF
# define EOF (-1)
#endif
对于到达文件尾端或出错都会返回EOF,所以为了进一步区分这两种情况,引入两个函数:
#include
extern int feof (FILE *__stream) __THROW __wur; //若以达到文件尾端,则返回非0,否则返回0
extern int ferror (FILE *__stream) __THROW __wur; //若输入出错,则返回非0,否则返回0
为每个流在FILE对象中维护了两个标志。
由于feof仅检查文件结束标志位是否被置位,文件结束标志是由当前时间之前的最后一次相关操作(包括读、seek等操作)设置的。因此在fgets函数前调用feof还无法判断文件流是否到达尾部。有关于这个问题的解决方法请见:http://book.51cto.com/art/201311/419432.htm
调用clearerr可以清除这两个标志。
#include
extern void clearerr (FILE *__stream) __THROW;
从流中读取数据后,可以调用ungetc将字符再压送回流中。
#include
extern int ungetc (int __c, FILE *__stream);
每次一个字符的I/O。输出函数:
#include
extern int fputc (int __c, FILE *__stream);
extern int putc (int __c, FILE *__stream);
#define putc(_ch, _fp) _IO_putc (_ch, _fp)
extern int putchar (int __c);
5.7 每次一行I/O
5.6节主要分析每次一个字符的I/O,5.7节则介绍每次一行的I/O。
先来看输入函数:
#include
extern char *fgets (char *__restrict __s, int __n, FILE *__restrict __stream) //从指定流读
__wur;
extern char *gets (char *__s) __wur __attribute_deprecated__; //从标准输入读
由于是标准I/O,所以一直读到下一个换行符为止,但不会超过n-1个字符。该缓冲区以null字节结尾。如若该行包括最后一个换行符的字符数超过n-1,则fgets只返回一个不完整的行,但是,缓冲区总是以null字节结尾。对fgets的下一次调用会继续读该行。
先来看看对fgets的下一次调用会继续读改行这一点,通过一个验证一下。源码如下:
#include
#include
int main()
{
FILE* fp;
char* buf1 = (char*)malloc(4*sizeof(char));
char* buf2 = (char*)malloc(7*sizeof(char));
fp = fopen("./temp","r");
fgets(buf1,4,fp);
printf("%s\n",buf1);
fgets(buf2,7,fp);
printf("%s\n",buf2);
free(buf1);
free(buf2);
return 0;
}
运行结果如下:
hel
lo wor
由于标准I/O使用行缓冲模式,根据实验的结果,我猜测fgets会一次性读取一定长度的数据填充到缓冲区中,每次调用fgets函数会从缓冲区中读取n-1个字符,直到数据被读取完。但如果数据中包含有换行符,则执行I/O操作,此处就是输出到屏幕。
由于缓冲区中的数据没有被全部读取,因此fgets会继续读该行。输出的第二行也验证了这一点。
或者不从源码的角度理解,仅从功能角度理解函数的特点,fgets一次就是读取一行,如果这其中包含有换行符,实际上就意味着数据不是一行而是两行。读取一行数据后,fgets将n-1个字符复制到用户缓冲区中,知道缓冲区中的数据被全部读取完。根据以上描述,我们自己都可以实现一个简单的fgets函数。函数的执行流程如下:
fgets函数的源码在此就不详细分析了,争取对fread的源码进行一个简单的分析。
输出函数,每次一行。
#include
extern int fputs (const char *__restrict __s, FILE *__restrict __stream);
extern int puts (const char *__s);
puts输出后还会将一个换行符写到标准输出。
5.9二进制I/O
二进制I/O的功能主要就是读入或写入任意类型、任意字节的数据,其中的两个参数与MPI接口中包含的参数相类似。
函数原型类型:
#include
extern size_t fread (void *__restrict __ptr, size_t __size,
size_t __n, FILE *__restrict __stream) __wur;
extern size_t fwrite (const void *__restrict __ptr, size_t __size,
size_t __n, FILE *__restrict __s);
返回读或写的对象数。对于写,如果返回值小于所要求的__n,则出错。可通过ferror检查。
5.12 实现细节
通过以下函数可以获得文件流指针对应的文件描述符。
#include
extern int fileno (FILE *__stream) __THROW __wur;
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
有关于临时文件与内存流的内容,现在就先不跟大家分享了,以后遇到的时候在详细研究吧。
关于书后习题第6题给大家分享一点我的见解
题目如下:打印的提示信息没有包含换行符,程序也没有调用fflush函数,请解释输出提示信息的原因是什么?
题目中给出了两点条件,逐条来分析。
“打印的提示信息没有包含换行符”,由于标准I/O使用行缓冲模式,所以没有换行符则不执行I/O操作。此处就是不输出提示信息。
“程序也没有调用fflush函数”,fflush函数的功能就是将流所有未写的数据都传送至内核。作为一种特殊情形,如若fp为NULL,则此函数将导致所有输出流被清洗。此处就是不输出提示信息。
以上两点的结果都是不输出提示信息,那么是什么原因导致提示信息的输出?
这是基于行缓冲的特点:从一个行缓冲的流得到输入数据,输出流就会被自动冲洗。此处就是每次调用fgets时标准输出设备将自动冲洗。
关于以上内容我们可以通过几个简单的实验进行验证。首先来看一个永远不会输出的源码:
#include
int main()
{
char output[] = "Hello world";
printf("%s",output);
while(1);
return 0;
}
针对第一点做一点调整:
#include
int main()
{
char output[] = "Hello world\n";
printf("%s",output);
while(1);
return 0;
}
#include
int main()
{
char output[] = "Hello world";
printf("%s\n",output);
while(1);
return 0;
}
好了看完第一点,针对第二点作出调整:
#include
int main()
{
char output[] = "Hello world";
printf("%s",output);
fflush(stdout);
while(1);
return 0;
}
#include
int main()
{
char output[] = "Hello world";
printf("%s",output);
fgetc(stdin);
while(1);
return 0;
}