5.8 二进制I/O

5.8 二进制I/O

上面所述函数以一次一个字符或一次一行的方式进行操作。如果为二进制I/O,那么我们更愿意一次读或写整个结构。为了使用getcputc做到这一点,必须循环通过整个结构,一次读或写一个字节,麻烦且费时。若使用fputs,它在遇到null字节时就停止,而在结构中可能含有null字节,所以不能使用每次一行函数实现这种要求。相类似,如果输入数据中包含有null字节或新行符,则fgets也不能正确工作。因此,提供了下列两个函数以执行二进制I / O操作

#include <stdio.h>

size_t fread(void*restrict ptr, size_t size, size_t nobj,FILE*restrict fp);

size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE*restrict fp);

两个函数的返回值为读或写的对象数。

常见的用法有两种,如下:

(1)读或写一个二进制数组。例如将一个浮点数组的第2至第5个元素写至一个文件上:

float data[10];
if (fwrite(&data[2], sizeof(float), 4, fp) != 4)
err_sys("fwrite error");

其中,指定size为每个数组元素的长度,nobj为欲写的元素数。

(2) 读或写一个结构。例如,可以写作:

struct {

short count;

long total;

char name[NAMESIZE];

} item;

if (fwrite(&item, sizeof(item), 1, fp) != 1)

err_sys("fwrite error");

其中,指定size为结构的长度,nobj1(要写的对象数)。

将这两个例子结合起来就可读或写一个结构数组。为了做到这一点,size应当是该结构的sizeof,nobj应是该数组中的元素数。

freadfwrite返回读或写的对象数。对于读,如果出错或到达文件尾端,则此数字可以少于nobj。在这种情况,应调用ferrorfeof以判断究竟是那一种情况。对于写,如果返回值少于所要求的nobj,则出错。

使用二进制I / O的基本问题是,它只能用于读已写在同一系统上的数据。现在,很多异构系统通过网络相互连接起来。常常有这种情形,在一个系统上写的数据,在另一个系统上处理。在这种环境下,这两个函数可能就不能正常工作,其原因是:

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

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

在不同系统之间交换二进制数据的实际解决方法是使用较高层次的协议。

5.7 每次一行I/O

5.7 每次一行I/O

下面两个函数提供每次输入一行的功能。

 

#include <stdio.h>

char* fgets(char*restrict buf, int n, FILE*restrict fp);

char* gets( char* buf);

这两个函数都指定了缓存地址,读入的行将送入其中。gets从标准输入读,而fgets则从指定的流读。

对于fgets,必须指定缓存的长度n。此函数一直读到下一个新行符为止,但是不超过 n1个字符,读入的字符被送入缓存。该缓存以null字符结尾。如若该行,包括最后一个新行符的字符数超过n1,则只返回一个不完整的行,而且缓存总是以null字符结尾。对fgets的下一次调用会继续读该行。

gets是一个不推荐使用的函数。问题是调用者在使用gets时不能指定缓存的长度。这样就可能造成缓存越界(如若该行长于缓存长度),写到缓存之后的存储空间中,从而产生不可预料的后果。这种缺陷曾被利用,造成1988年的因特网蠕虫事件。getsfgets的另一个区别是,gets并不将换行符存入缓存中。

尽管ISO C要求实现提供gets,但不要使用它,还是使用fgets为好。

fputsputs提供每次输出一行的功能。

#include <stdio.h>

int fputs(const char*restrict str, FILE*restrict fp);

int puts(const char*str);

函数fputs将一个以null符终止的字符串写到指定的流,终止符null不写出。注意,这并不一定是每次输出一行,因为它并不要求在null符之前一定是新行符。通常,在null符之前是一个新行符,但并不要求总是如此。

puts将一个以null符终止的字符串写到标准输出,终止符不写出。但是,puts然后又将一个新换行符写到标准输出。

puts并不像它所对应的gets那样不安全。但是我们还是应避免使用它,以免需要记住它在最后又加上了一个新行符。如果总是使用fgetsfputs,那么就会熟知在每行终止处我们必须自己加一个新行符。

5.6 读和写流

5.6 读和写流

一旦打开了流,则可在三种不同类型的非格式化I/O中进行选择,对其进行读、写操作。

(1) 每次一个字符I / O。一次读或写一个字符,如果流是带缓存的,则标准I / O函数处理所有缓存。

(2) 每次一行I / O。使用fgetsfputs一次读或写一行。每行都以一个新行符终止。当调用fgets时,应说明能处理的最大行长。

(3) 直接I / Ofreadfwrite函数支持这种类型的I / O。每次I / O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中读或写一个结构

5.6.1 输入函数

以下三个函数可用于一次读一个字符:

#include<stdio.h>

int getc( FILE* fp);

int fgetc(FILE*fp);

int getchar( void); //equal to getc(stdin);

前两个函数的区别在于getc可被实现为宏,而fgetc则不能实现为宏。这意味着:

(1) getc的参数不应当是具有副作用的表达式。

(2) 因为fgetc一定是个函数,所以可以得到其地址。这就允许将fgetc的地址作为一个参数传送给另一个函数。

(3) 调用fgetc所需时间很可能长于调用getc,因为调用函数通常所需的时间长于调用宏。检验一下<stdio.h>头文件的大多数实现,从中可见getc是一个宏,其编码具有较高的工作效率。

这三个函数unsigned char 类型转换为int的方式返回下一个字符。说明为不带符号的理由是,如果最高位为1也不会使返回值为负。要求整型返回值的理由是,这样就可以返回所有可能的字符值再加上一个已发生错误或已到达文件尾端的指示值。在<stdio.h>中的常数EOF被要求是一个负值,其值经常是-1。这就意味着不能将这三个函数的返回值存放在一个字符变量中,以后还要将这些函数的返回值与常数EOF相比较。

注意,不管是出错还是到达文件尾端,这三个函数都返回同样的值。为了区分这两种不同的情况,必须调用ferrorfeof

#include<stdio.h>

int ferror( FILE*fp);

int feof( FILE*fp);

void clearerr( FILE*fp);

前两个函数返回值:若条件为真,则返回非0值;否则返回0

在大多数实现的FILE对象中,为每个流保持了两个标志:

l  出错标志。

l  文件结束标志。

调用clearerr则清除这两个标志。

从一个流读之后,可以调用ungetc将字符再送回流中。

#include<stdio.h>

int ungetc( int c, FILE*fp);

成功则返回c,失败则返回EOF

送回到流中的字符以后又可从流中读出,但读出字符的顺序与送回的顺序相反。应当了解,虽然ANSI C允许支持任何数量的字符回送的实现,但是它要求任何一种实现都要支持一个字符的回送功能。

回送的字符,不一定必须是上一次读到的字符。EOF不能回送。但是当已经到达文件尾端时,仍可以回送一字符。下次读将返回该字符,再次读则返回EOF。之所以能这样做的原因是一次成功的ungetc调用会清除该流的文件结束指示。

当正在读一个输入流,并进行某种形式的分字或分记号操作时,会经常用到回送字符操作。有时需要先看一看下一个字符,以决定如何处理当前字符。然后就需要方便地将刚查看的字符送回,以便下一次调用getc时返回该字符。如果标准I / O库不提供回送能力,就需将该字符存放到一个我们自己的变量中,并设置一个标志以便判别在下一次需要一个字符时是调用getc,还是从我们自己的变量中取用。

5.6.2 输出函数

对应于上面所述的每个输入函数都有一个输出函数。

#include <stdio.h>

int putc(int c, FILE*fp);

int fputc(int c,FILE*fp);

int putchar(int c);

三个函数返回值:成功返回c;失败返回EOF.

与输入函数一样,putchar(c) 等同于putc(c, stdout)putc 可被实现为宏,而fputc 则不能实现为宏。

5.5 打开流

5.5 打开流

下列三个函数打开一个标准I/O流。

·         #include <stdio.h>

· FILE *fopen(const char *restrict pathname, const char *restrict type);

· FILE *fropen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);

· FILE *fdopen(int filedes, const char *type);

返回值:成功返回文件指针,出错返回NULL

1.     fopen打开一个指定的文件。

2.     freopen在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭流。若该流已经定向,则freopen清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准出错。

3.     fdopen获取一个现有的文件描述符,并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数返回的描述符。因为这些特殊类型的文件不能用标准I/O fopen函数打开,所以必须先调用设备专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与该描述符相关联。

fopenfreopenISO C的所属部分。而ISO C并不涉及文件描述符,所以仅有POSIX.1具有fdopen

type参数指定对该I/O流的读、写方式,ISO C规定type参数可以有15种不同的值,它们示于表5.2中。

type

Description

r rb

open for reading

wwb

truncate to 0 length or create for writing

aab

append; open for writing at end of file, or create for writing

r+r+brb+

open for reading and writing

w+w+bwb+

truncate to 0 length or create for reading and writing

a+a+bab+

open or create for reading and writing at end of file

5.2 打开标准I/O流的type参数

对于fdopentype参数的意义稍有区别。因为该描述符已被打开,所以fdopen为写而打开并不截短该文件。

当以读和写类型打开一个文件时(type+符号),具有下列限制:

·         如果中间没有fflushfseekfsetposrewind,则在输出的后面不能直接跟随输入。

·         如果中间没有fseekfsetposrewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。

Restriction

r

w

a

r+

w+

a+

file must already exist

   

   

previous contents of file discarded

 

   

 

stream can be read

   

stream can be written

 

stream can be written only at end

   

   

5.3 打开一个标准I/O流的6种不同的方式

注意,在指定wa类型创建一个新文件时,我们无法说明该文件的访问权限位。除非引用终端设备,否则按系统默认的情况,流被打开时是全缓冲的。若流引用终端设备,则该流是行缓冲的。一旦打开了流,那么在对该流执行任何操作之前,如果希望,则可使用上一节所述的setbufsetvbuf改变缓冲的类型。

调用fclose关闭一个打开的流。

·         #include <stdio.h>

· int fclose(FILE *fp);

返回值:成功返回0,出错返回EOF

在该文件被关闭之前,冲洗缓冲区中的输出数据。丢弃缓冲区中的任何输入数据。

当一个进程正常终止时(直接调用exit函数,或从main函数返回),则所有带未写缓冲数据的标准I/O流都会被冲洗,所有打开的标准I/O溜溜都会被关闭。