第5章 标准IO库

版权声明:转载请声明 https://blog.csdn.net/qq_40732350/article/details/82318044

请移步到:

http://note.youdao.com/noteshare?id=5965d9787a48611a44cb421504e85ab0&sub=041921346D2C4E93B8C64A334C302F9E

5.2 流和FILE对象

当用标准IO库打开或创建一个文件时,我们已使一个流与一个文件相关联。对

于ASCI1字符集,一个字符用一个字节表示。对于国际字符集,一个字符可用多个字节表示。

标准IO文件流可用于单字节或多字节("宽")字符集。

流的定向决定了所读、写的字符是单字节还是多字节的。

当一个流最初被创建时,它并没有定向。

如若在未定向的流上使用一个多字节IO函数(见),则将该流的定向设置为宽定向的。

若在未定向的流上使用一个单字节IO函数,则将该流的定向设为字节定向的。

只有两个函数可改变流的定向。

freopen函数清除一个流的定向;fwide函数可用于设置流的定向。

int fwide(FILE *stream, int mode);

根据mode参数的不同值, fwide函数执行不同的工作。

如若mode参数值为负, fwide将试图使指定的流是字节定向的。

如若mode参数值为正, fwide将试图使指定的流是宽定向的。

如若mode参数值为0, fwide将不试图设置流的定向,但返回标识该流定向的值。

注意:

fwide并不改变已定向流的定向。还应注意的是, fwide无出错返回。试想,如若,流是无效的,那么将发生什么呢?我们唯一可依靠的是,在调用fwide前先清除errno,从 fwide返回时检查errno的值。在本书的其余部分,我们只涉及字节定向流。

当打开一个流时,标准IO函数fopen (参考5.5节)返回一个指向FILE对象的指针。该对象通常是一个结构,它包含了标准IO库为管理该流需要的所有信息,包括用于实际IO的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志等。

应用程序没有必要检验FILE对象。为了引用一个流,需将FILE指针作为参数传递给每个标准IO函数。在本书中,我们称指向FILE对象的指针(类型为FILE*)为文件指针。

5.3 标准输入,标准输出和标准错误

对一个进程预定义了3个流,并且这3个流可以自动地被进程使用,

它们是:标准输入、标准输出,和标准错误。

系统调用:STDIN_FILENO, STDOUT_FILENO和 STDERR_FILENO

标准I/O流:预定义文件指针stdin, stdout和stderr。

5.4 缓冲

标准I/O库提供缓冲的目的:

1是尽可能减少使用read和write调用的次数

2.它也对每个IO流自动地进行,缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。遗憾的是,标准IO库最令人迷惑的也是它的缓冲。

标准IO提供了以下3种类型的缓冲。

(1)全缓冲。

在这种情况下,在填满标准I/O缓冲区后才进行实际IO操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次IO操作时,相关标准IO函数通常调用malloc (见7.8节)获得需使用的缓冲区。

术语冲洗(flush)说明标准IO缓冲区的写操作。缓冲区可由标准IO例程自动地冲洗(例如,当填满一个缓冲区时),或者可以调用函数fflush冲洗一个流。值得注意的是,

在UNIX环境中, flush有两种意思。

1.在标准I/O库方面, flush (冲洗)意味着将缓冲区中的内容写到磁盘,上(该缓冲区可能只是部分填满的),

2.在终端驱动程序方面(例如,在第18章中所述的tcflush函数), flush (刷清)表示丢弃已存储在缓冲区中的数据。

(2)行缓冲。

在这种情况下,当在输入和输出中遇到换行符时,标准IO库执行IO操作。这允许我们一次输出一个字符(用标准IO函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。

对于行缓冲有两个限制。

第一,因为标准IO库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行IO操作。

第二,任何时候只要,通过标准IO库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求需要,数据)得到输入数据,那么就会冲洗所有行缓冲输出流。在(b)中带了一个在括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核读数据。很明显,从一个不带缓冲的流中输入(即(a)项)需要从内核获得数据。

(3)不带缓冲。

标准IO库不对字符进行缓冲存储。例如,若用标准IO函数fputs写15个字符到不带缓冲的流中,我们就期望这15个字符能立即输出,很可能使用3.8节的write函数将这些字符写到相关联的打开文件中。

标准错误流stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。

ISO C要求下列缓冲特征。·当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的。·标准错误决不会是全缓冲的。但是,这并没有告诉我们如果标准输入和标准输出指向交互式设备时,它们是不带缓冲的还是行缓冲的;以及标准错误是不带缓冲的还是行缓冲的。

很多系统默认:

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

void setbuf(FILE *stream, char *buf);

void setbuffer(FILE *stream, char *buf, size_t size);

void setlinebuf(FILE *stream);

int setvbuf(FILE *stream, char *buf, int mode, size_t size);

5.5 打开流

FILE *fopen(const char *path, const char *mode);

FILE *freopen(const char *path, const char *mode, FILE *stream);

FILE *fdopen(int fd, const char *mode);

参数mode 的取值:

5.6 读和写流

1.输入函数

把字符再压回去:挺有意思

2.输出函数

5.7 每次一行I/O

注意:

这里的向文件输入一行后,再输入一行,新输入的这一行不是添加到开始那一行的下一行,而是紧接着开始那一行的末尾

5.8 标准I/O效率

结果:

这3个标准I/O版本的每一个,其用户CPU时间都大于最佳read版本

因为:

在每次读一个字符的标准I/O版本中有一个要执行1亿次的循环,

在每次读一行的版本中有一个要执行3144 984次的循环。

在read版本中,其循环只需执行25 224次(对于缓冲区长度 , 为4096字节)

因为系统CPU时间几乎相同

所以用户CPU时间的差别以及等待I/O结束所消耗时间的差别造成了时钟时间的差别。

系统CPU时间几乎相同,原因是因为所有这些程序对内核提出的读、写请求数基本同。

注意

使用标准I/O例程的一个优点是无需考虑缓冲及最佳I/O长度的选择。在使用fqets时需要考虑最大行长,但是与选择最佳I/O长度比较,这要方便得多。

总结:

  1. 标准IO库与直接调用read和write函数相比并不慢很多。
  2. 对于大多数比较复杂的应用程序,最主要的用户CPU时间是由应用本身的各种处理消耗的,而不是由标准IO例程消耗的。

5.9 二进制I/O

缺点:

  1. 5.6节和5.7节中的函数以一次一个字符或一次一行的方式进行操作。
  2. 如果进行二进制IO操作,那么我们更愿意一次读或写一个完整的结构。如果使用getc或putc读、写一个结构,那么必须循环通过整个结构,每次循环处理一个字节,一次读或写一个字节,这会非常麻烦而且费时。
  3. 如果使用fputs和fgets,那么因为fputs在遇到null字节时就停止,而在结构中可能含有null字节,所以不能使用它实现读结构的要求。
  4. 如果输入数据中包含有null字节或换行符,则fgets也不能正确工作。

解决:

因此,提供了下列两个函数以执行二进制I/O操作。

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

返回值:

  1. fread和fwrite返回读或写的对象数。
  2. 对于读,如果出错或到达文件尾端,则此数字可以少于nmemb。
  3. 在这种情况,应调用ferror或feof以判断究竟是那一种情况。

使用二进制I/O的基本问题:

1.它只能用于读在同一系统上已写的数据。

2.多年之前,这并无!问题(那时,所有UNIX系统都运行于PDP-11上),而现在,很多异构系统通过网络相互连接起来,而且,这种情况已经非常普遍。常常有这种情形,在一个系统上写的数据,要在另一个系统,上进行处理。在这种环境下,这两个函数可能就不能正常工作,

原因是:

(1)在一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同(由于不同的对齐要求),确实,某些编译程序有一个选项,选择它的不同值,或者使结构中的各成员紧密包装 (这可以节省存储空间,而运行性能则可能有所下降);或者准确对齐(以便在运行时易于存取结构中的各成员),这意味着即使在同一个系统上,一个结构的二进制存放方式也可能因编译程序选项的不同而不同。

(2)用来存储多字节整数和浮点值的二进制格式在不同的系统结构间也可能不同。

5.10 定位流

有3种方法定位标准IO流。

(1) ftel1和fseek函数。但是它们都假定文件的位置可以存放在一个长整型中。

(2) ftello和fseeko函数。这两个函数,使文件偏移量可以不必一定使用长整型。它们使用off_t数据类型代替了长整型。可以用off_t进行自定义

如:#define _FILE_OFFSET_BITS 64 //成为64位的类型

(3) fgetpos和fsetpos函数。它们使用一个抽象数据类型 fpos_t记录文件的位置。这种数据类型可以根据需要定义为一个足够大的数,用以记录文件位置。

注意:

需要移植到非UNIX系统上运行的应用程序应当使用fgetpos和fsetpos.

long ftell(FILE *stream);

int fseek(FILE *stream, long offset, int whence);

void rewind(FILE *stream);

off_t ftello(FILE *stream);

int fseeko(FILE *stream, off_t offset, int whence);

int fgetpos(FILE *stream, fpos_t *pos);

int fsetpos(FILE *stream, fpos_t *pos);

对于文本文件,它们的文件当前位置可能不以简单的字节偏移量来度量。这主要也是在非, UNIX系统中,它们可能以不同的格式存放文本文件。

为了定位一个文本文件, whence一定要是SEEK_SET,

而且offset只能有两种值:

  1. 0(后退到文件的起始位置)
  2. 对该文件的ftell所返回的值。

使用rewind函数也可将一个流设置到文件的起始位置。

5.11 格式化I/O

格式化输出:

变体:接收一个数组参数,而不是一系列可变数量的参数。

格式化输入:

变体:

5.12 实现细节

5.13 临时文件

为了提供更好的性能,往往会采用建立临时文件的方式加速数据的存取。

  绝大多数的软件在安装过程中,会将自身的安装文件解压缩到一个临时目录,然后再进行安装。当安装结束后,很多软件的临时文件并不会自动删除,而是成为硬盘里一堆无用的垃圾。

而在软件的运行过程中,通常也会产生一些临时交换文件,比如一些程序工作时产生的形如*.old、*.bak这样的备份文件等。一些软件自带了冗余的字体库和帮助文件都是占用磁盘空间的罪魁祸首。

  2.软件卸载产生的残留文件

  软件在卸载的过程中,很多时候并不能完全删除,更多的时候会留下一些残存的.dll文件或在注册表中留下自己的身影。这是因为大部分软件都使用了动态链接库,这些动态链接库文件在很多时候会互相占用,有的时候则是由于用户非正常的删除程序(例如直接删除文件夹)而导致程序已经删除了,相关的注册信息却还保留在注册表中。

  3.上网产生的临时文件

  我们在上网的同时,Cookies为了加快我们下次登录网站的访问速度,就记录下了用户的身份识别等信息。Internet临时文件夹也会记录下我们访问过哪些站点,这不但对我们的个人隐私是一种挑战,更重要的是,如果不及时清理这些记录,它同样会占用越来越多的磁盘空间,从而降低浏览速度。

/usr/include/x86_64-linux-gnu/bits/stdio_lim.h

char *tmpnam(char *s);

tmpnam函数产生一个与现有文件名不同的一个有效路径名字符串。每次调用它时,都产生一个不同的路径名,最多调用次数是TMP-MAX, TMP-MAX定义在<stdio.h>中。

若ptr是NULL

则所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回。

后续调用tmpnam时,会重写该静态区(这意味着,如果我们调用此函数多次,而且想保存路径名,则我们应当保存该路径名的副本,而不是指针的副本),

如若ptr不是NULL

则认为它,应该是指向长度至少是L_tmpnam个字符的数组(常量L_tmpnam定义在头文件<stdio.h>中),所产生的路径名存放在该数组中, ptr也作为函数值返回。

注意:

如果向文件写内容后,则这种文件不会自动删除,第二种方式会自动删除

FILE *tmpfile(void);

tmpfile创建一个临时二进制文件(类型wb+),在关闭该文件或程序结束时将自动删除这种文件。注意, UNIX对二进制文件不进行特殊区分。

解决临时文件不自动删除的问题:

tmpfile函数经常使用的标准UNIX技术是先调用tmpnam产生一个唯一的路径名,然后,·用该路径名创建一个文件,并立即unlink它。请回忆4.15节,对一个文件解除链接并不删除其.内容,关闭该文件时才删除其内容。而关闭文件可以是显式的,也可以在程序终止时自动进行。

注意:

系统提示用tmpnam时危险的,最好用mkstemp

mkstemp函数创建了一个文件,该文件有一个唯一的名字。名字是通过template字符串进行选择的。

这点有意思:

这个字符串是后6位设置为xxxxxx的路径名。函数将这些占位符替换成不同的字符来构建一个唯一的路径名。如果成功的,话,这两个函数将修改template字符串反映临时文件的名字。

mkstemp函数以唯一的名字创建一个普通文件并且打开该文件,该函数返回的文件描述符以读写方式打开。

mkstemp实例:

区别:

mkstemp创建的临时文件并不会自动删除。如果希望从文件系统命名空间中删除该文件,必须自己对它解除链接。

使用tmpnam和tempnam至少有一个缺点:在返回唯一的路径名和用该名字创建文件之间存在一个时间窗口,在这个时间窗口中,另一进程可以用相同的名字创建文件。

因此应该使用, tmpfile和mkstemp函数,因为它们不存在这个问题。

5.14 内存流

注意,这些取值对应于基于文件的标准IO流的type参数取值,但其中有些微小差别。

第一:

无论何时以追加写方式打开内存流时,当前文件位置设为缓冲区中的第一个null字节。如果缓冲区中不存在null字节,则当前位置就设为缓冲区结尾的后一个字节。当流并不是以追加写方式打开时,当前位置设为缓冲区的开始位置。因为追加写模式通过第一个null字节确定数据的尾端,内存流并不适合存储二进制数据(二进制数据在数据尾端之前就可能包含多个null字节)。

第二:

如果buf参数是一个null指针,打开流进行读或者写都没有任何意义。因为在这种情况下缓冲区是通过Ememopen进行分配的,没有办法找到缓冲区的地址,只写方式打开流意味着无法读取已写入的数据,同样,以读方式打开流意味着只能读取那些我们无法写入的缓冲区中的,数据。

第三:

任何时候需要增加流缓冲区中数据量以及调用fclose, fflush, fseek, fseeko以及fsetpos时都会在当前位置写入一个null字节。

想了解务必参考我的博客:

https://blog.csdn.net/qq_40732350/article/details/82317759

5.15 标准I/O的替代软件

标准IO库并不完善。Kor和Vo[I99II列出了它的很多不足之处,其中,某些属于基本设计,但是大多数则与各种不同的实现有关。

标准IO库的一个不足之处是效率不高,这与它需要复制的数据量有关。

当使用每次一行函数fqets和fputs时,通常需要复制两次数据:

第一次是在内核和标准IO缓冲区之间(当调用read和write时)

第二次是在标准I/O缓冲区和用户程序中的行缓冲区之间。

快速I/O库避免了这一点

其方法是使读一行的函数返回指向该行的指针,而不是将该行复制到另一个缓冲区中。

由于做了这种更改, grep(I)实用程序的速度提升了3倍。

标准IO库的另一种替代版: SFIO

这一软件包在速度上与fFIO相近,通常快于标准IO库。

SFIO软件包也提供了一些其他标准I/O库所没有的新特征;

推广了IO流,使其不仅可以代表文件,也可代表存储区:可以编写处理模块,并以栈方式将其压入I/O流,这样就可以改变一个流的操作;

较好的异常处理等。

另一个替代软件包该新软件包称为ASI (Alloc Stream Interface),

它使用了映射文件-mmap函数

其编程接口类似于UNIX系统存储分配函数(malloc、 realloc和free),与sfio软件包相同, ASI使用指针力图减少数据复制量。

对于嵌入式系统设计

合理内存要求的关注超过对可移植性、速度以及功能性等方面的关注。

许多标准IO库实现在C函数库中可用,这种C函数库是为内存较小的系统,

这种类型函数库的两种实现

uClibc C库(参阅http://www.uclibc.org)和Newlib C库(http://www source. redhat. com/newlib).

猜你喜欢

转载自blog.csdn.net/qq_40732350/article/details/82318044