APUE读书笔记-第五章 标准I/O库

时间:2022-12-26 10:38:24

今天草草的把第四章结了,后面的内容分析的也不是很详细,就连书中的例子都没有怎么实验,还是等以后有机会吧。

从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种缓冲:

  1. 全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。
  2. 行缓冲。在这种情况下,当在输入和输出遇到换行符时,标准I/O库执行I/O操作。行缓冲允许一次输出一个字符(用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。对于行缓冲又存在两个限制。第一,因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只有填满缓冲区,那么即使还没有写一个换行符,也进行I/O操作。第二,任何时候只要通过标准I/O库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求需要数据)得到输入数据,那么就会冲洗所有行缓冲输入流。此处冲洗是指将缓冲区中的数据写入内核中。在(b)中带了一个在括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核读数据。很明显,从一个不带缓冲的流中输入(即(a)项)需要从内核获得数据。
  3. 不带缓冲。标准I/O库不对字符进行缓冲存储。

对于上面提到的第二点可通过几个简单的实验进行验证,是通过fputc向标准输出输入数据,具体程序如下:

#include <stdio.h>
#include <unistd.h>

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缓冲还具有以下惯例:

  1. 标准错误是不带缓冲的。
  2. 若是指向终端设备的流,则是行缓冲的;否则是全缓冲的。

可通过以下函数更改缓冲类型。

#include <stdio.h>
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 <stdio.h>
#define _IOFBF 0/* Fully buffered. */
#define _IOLBF 1/* Line buffered. */
#define _IONBF 2/* No buffering. */

强制冲洗一个流。

#include <stdio.h>
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;

上述三个函数的区别如下:

  1. fopen函数打开路径名为pathname的一个指定的文件。
  2. freopen函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用freopen清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准错误。
  3. fdopen函数取一个已有的文件描述符(可通过open函数获得此文件描述符),并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数返回的描述符。因为这些特殊类型的文件不能用标准I/O函数fopen打开,所以我们必须先调用设备专用函数以获得一个文件描述符,然后用fopen使一个标准I/O流与该描述符相结合。

ISO规定type参数可以有15种不同的值。

  1. r或rb:为读而打开。但文件必须已经存在。(O_RDONLY)
  2. w或wb:把文件截断至0长,或为写而创建。文件之前的内容会被删除。(O_WRONLY|O_CREAT|O_TRUNC)
  3. a或ab:追加;为在文件尾写而打开,或为写而打开。(O_WRONLY|O_CREAT|O_APPEND)
  4. r+或r+b或rb+:为读和写而打开。流只可在尾端处写。(O_RDWR)
  5. w+或w+b或wb+:把文件截断至0长,或为读和写而打开。(O_RDWR|O_CREAT|O_TRUNC)
  6. a+或a+b或ab+:为在文件尾读和写而打开或创建。(O_RDWR|O_CREAT|O_APPEND)

有关于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 <stdio.h>
extern int fclose (FILE *__stream);
在该文件被关闭前,冲洗缓冲中的输出数据。缓冲区中的任何输入数据被丢弃。

当一个进程正常终止时(直接调用exit函数,或从main函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。这也解释了我们之前留下的疑问,缓冲区中数据冲洗的时机——进程正常终止时,根据我们之前学习到的知识,应该是在main函数开始之前注册了某个析构函数,这个析构函数的功能就是冲洗缓冲区。


5.6 读和写流

标准I/O提供三种方式进行非格式化读、写操作。

  1. 每次一个字符的I/O。
  2. 每次一行的I/O。每行都以一个换行符终止。
  3. 直接I/O。

每次一个字符的I/O。由于标准I/O采用行缓冲模式,所以只有出现换行符或写满一行的情况下才会冲洗缓冲区,此时数据已经写入缓冲区中。输入函数如下:

#include <stdio.h>
extern int fgetc (FILE *__stream);
extern int getc (FILE *__stream);
extern int getchar (void); 函数getchar()等同于getc(stdin)
getc的具体实现为:

#include <stdio.h>
#define getc(_fp) _IO_getc (_fp)
使用int作为返回值的原因是:返回所有可能的字符值在加上一个已出错或已达到文件尾端的指示值。我也查了以下仅是ascii码表就已经包含128个符号,因此使用char作为返回数据远远不够。

EOF的定义为

#include <stdio.h>
#ifndef EOF
# define EOF (-1)
#endif
对于到达文件尾端或出错都会返回EOF,所以为了进一步区分这两种情况,引入两个函数:

#include <stdio.h>
extern int feof (FILE *__stream) __THROW __wur; //若以达到文件尾端,则返回非0,否则返回0
extern int ferror (FILE *__stream) __THROW __wur; //若输入出错,则返回非0,否则返回0

为每个流在FILE对象中维护了两个标志。

  1. 出错标志。
  2. 文件结束标志。

由于feof仅检查文件结束标志位是否被置位,文件结束标志是由当前时间之前的最后一次相关操作(包括读、seek等操作)设置的。因此在fgets函数前调用feof还无法判断文件流是否到达尾部。有关于这个问题的解决方法请见:http://book.51cto.com/art/201311/419432.htm

调用clearerr可以清除这两个标志。

#include <stdio.h>
extern void clearerr (FILE *__stream) __THROW;

从流中读取数据后,可以调用ungetc将字符再压送回流中。

#include <stdio.h>
extern int ungetc (int __c, FILE *__stream);
每次一个字符的I/O。输出函数:

#include <stdio.h>
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 <stdio.h>
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 <stdio.h>
#include <stdlib.h>

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;
}

temp文件中的内容还是我们熟悉的“hello world”

运行结果如下:

hel
lo wor

根据我们之前学到的知识,fgets在没有读到换行符的情况下,会读取n-1个字符,buf1的长度为4,所以会读取三个字符,也就是“hel”,输出第一行也印证了这一知识点。

由于标准I/O使用行缓冲模式,根据实验的结果,我猜测fgets会一次性读取一定长度的数据填充到缓冲区中,每次调用fgets函数会从缓冲区中读取n-1个字符,直到数据被读取完。但如果数据中包含有换行符,则执行I/O操作,此处就是输出到屏幕。

由于缓冲区中的数据没有被全部读取,因此fgets会继续读该行。输出的第二行也验证了这一点。

或者不从源码的角度理解,仅从功能角度理解函数的特点,fgets一次就是读取一行,如果这其中包含有换行符,实际上就意味着数据不是一行而是两行。读取一行数据后,fgets将n-1个字符复制到用户缓冲区中,知道缓冲区中的数据被全部读取完。根据以上描述,我们自己都可以实现一个简单的fgets函数。函数的执行流程如下:

  1. 判断当前位置指针是否为0,若为0则直接跳转到4执行,否则顺序执行。
  2. 读取数据,若遇到"\n"则跳出循环,否则一直读取直到缓冲区满。
  3. 首先设置当前位置指针指向缓冲区的头部。
  4. 计算复制的字符数,若缓冲区中包含有足够的数据则复制用户指定字节数的数据,否则复制缓冲区中实际含有的数据。
  5. 将用户缓冲区的最后一位置为"\0"。
  6. 更新当前位置指针。

fgets函数的源码在此就不详细分析了,争取对fread的源码进行一个简单的分析。

输出函数,每次一行。

#include <stdio.h>
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 <stdio.h>
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 <stdio.h>
extern int fileno (FILE *__stream) __THROW __wur;

有关于FILE结构的定义如下,位于libio.h中

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 <stdio.h>

int main()
{
char output[] = "Hello world";
printf("%s",output);
while(1);
return 0;
}

调用printf后,程序进入死循环,通过之前的知识可以了解到一个进程正常终止时,所有带未写缓冲数据的标准I/O流都被冲洗。由于此处进程一直未能正常终止,所以信息一直未能输出。

针对第一点做一点调整:

#include <stdio.h>

int main()
{
char output[] = "Hello world\n";
printf("%s",output);
while(1);
return 0;
}

此时程序可以正常输出,或者进行相同的调整:

#include <stdio.h>

int main()
{
char output[] = "Hello world";
printf("%s\n",output);
while(1);
return 0;
}

好了看完第一点,针对第二点作出调整:

#include <stdio.h>

int main()
{
char output[] = "Hello world";
printf("%s",output);
fflush(stdout);
while(1);
return 0;
}

最后一种调整方法:

#include <stdio.h>

int main()
{
char output[] = "Hello world";
printf("%s",output);
fgetc(stdin);
while(1);
return 0;
}

在标准输出后进行标准输入。