《UNIXLinux程序设计教程》一2.4 读和写流

简介: 本节书摘来自华章出版社《UNIXLinux程序设计教程》一 书中的第2章,第2.4节,作者:赵克佳 沈志宇,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.4 读和写流

一旦打开了一个流,就能对它进行读写,读写可以按无格式方式也可以按有格式方式进行。这一节介绍无格式I/O函数,下一节介绍有格式I/O函数。
有以下三种类型的无格式I/O函数可供选择:
1)字符I/O函数。这种函数每次读或写一个字符。
2)行I/O函数。这种函数每次读写一行,每一行以换行符结束。
3)块I/O函数。这种函数支持成块I/O,它们每次读写若干个对象,每个对象的大小是指定的。块I/O有时也称为二进制I/O、对象I/O或结构I/O。

2.4.1 字符I/O

如下三个字符输入函数每次读入一个字符:

#include <stdio.h>

int fgetc (FILE * stream);
int getc (FILE * stream);
int getchar (void);

fgetc()从流stream中按unsigned char类型读取下一字符,并将它强制为int类型返回,若遇到文件结束或者出现错误,则返回EOF。
getc()的功能与fgetc()相同,不同的是允许将getc()作为宏来实现,而fgetc()则必须为函数。getc()常常是被高度优化了的,因此是最常用的读单个字符的函数。
getchar()等价于getc(stdin)。
这三个函数之所以将流中的字符视为unsigned char,是为了保证在其高位被设置时函数的返回值不会为负值。要求返回值为int类型是为了能够返回所有可表示的字符,不仅是ASCII字符集中的字符,也包括宽字符集中的字符,还包括遇到文件结束和错误时的指示符EOF。这意味着我们不能将fgetc()的返回值存储在字符类型的变量中。
与三个字符输入函数对应有如下三个字符输出函数:

#include <stdio.h>

int fputc (int c, FILE *stream);
int putc (int c, FILE * stream);
int putchar (int c)

fputc()将字符c转换为unsigned char 类型,然后写至流stream并返回字符c。
putc()与fputc()相同,但它常常是用较快的宏来实现的。putc()是用于输出单个字符最合适的函数。类似于输入函数,putchar()等价于putc(stdin)。
例2-2 程序2-2给出的函数y_or_n_ques()在标准输入输出终端提出参数指定的问题,并读用户的回答。回答为'y'则返回真值,回答为'n'则返回假值。用getc()和putc()或者getchar()和putchar()替换其中的fgetc()和fputc(),它也照样工作。其中函数fputs()输出一行字符串,下一节将详细介绍它。tolower()将输入字符转换为小写字符以便随后对它进行检测。
screenshot

注意这个函数读单字符命令的处理,它在用fgetc()读取一个字符之后还继续调用fgetc()抛弃同一行的其他字符。因为默认情况下终端输入只有在键入换行符之后才有效,如果不抛弃这个换行符,它将遗留在输入流中使得下一次读命令字符时会读不到实际键入的正确命令。第9章低级终端I/O中我们将看到对这种问题更为精致的处理方法。

2.4.2 行I/O

有许多应用是按行来处理数据的,例如编译程序通常每次读入一行源程序来进行词法扫描。标准C库中有两个函数用于每次读入一行:

#include <stdio.h>

char * fgets (char *s,  int count,  FILE * stream )
char * gets (char *s)

fgets()从stream指定的流中连续读字符直至读到换行符或者读够count–1个字符(包括换行符)为止,读入的这一行字符(包括最后的换行符)存储在参数s指定的字符串中,并且在其末尾添加一个空字符(0)作为结束。参数count指明字符串s的大小。
如果要读入的这一行(包括结尾的换行符)长度大于count–1,则只有部分字符被读入,而字符串s总是以空字符结尾,下一次调用fgets()将返回此行剩余的部分。
gets()函数从标准输入流stdin中读入完整的一行至参数s指定的字符串中。它删除换行符并在字符串s的末尾添加一个空字符作为结束。
注意gets()与fgets()的不同。fgets()不能保证一定读入完整的一行,因此为了判别是否已经读入一行,它需要保留换行符,而gets()则无此需要,故它删除换行符。另外,由于gets()不要求提供字符串s的空间大小,这导致gets()成了危险的函数:它没有为字符串s的溢出提供保护!当要读入的行长度超过字符串s所能容纳的大小时,超出的部分将越过s提供的空间而覆盖其他的数据或程序。因此最好不要使用gets()。
如果调用这两个函数时文件已处在文件尾,则字符串s不发生改变且返回值都是EOF。这两个函数遇到错误时也返回EOF,正常情况下返回指向字符串s的指针。
例2-3 程序2-3说明了fgets()和gets()的不同。该程序首先提问使用fgets()还是gets()读输入行,并根据用户的选择使用不同的函数。由于我们故意指定缓冲区的大小只有8字节,因此,当输入行的长度大于8时,使用fgets()每次只读8个字符,并且为了读入一完整的行需要循环读直至读到换行符为止。而用gets()读一行则不需要循环,但却可能导致读入的数据溢出缓冲区。

screenshot

为了展示gets()导致数组buf溢出的情况,我们利用结构类型将两个数组合在一起。当输入字符超过buf的大小(8个字符)时,fgets()至多读8个字符,而gets()则会导致数据溢出到成员others中。注意,这种结果与编译器的存储分配方法有关。我们这里假定编译器按结构成员书写的顺序分配存储。如果编译器按逆序分配,则应将others书写在buf之前。作为练习,建议你用不同的选择和长度不同的输入运行这个程序来查看运行结果。注意,这个程序要与程序2-2一起连接。
gets()可以读入完整的一行,但存在溢出的危险;fgets()虽然保险,但当输入数据中含有空字符(NUL,即'0')时却会遇到麻烦。因为fgets()不能保证每次都能完整地读入一行,并且它自动地在读入的字符串末尾添加空字符,这使得在分辨输入行中原本就有的空字符时需要特别的动作:当在字符串s中读到一个空字符时,必须判别其前面是否有换行符以及所在位置来确定它是原本就有的数据,还是作为字符串结束的空字符。
Linux中的GNU C库为此专门提供了另外一个每次读一行的函数getline(),此外还扩充了一个更通用的类似函数getdelim(),该函数读入一被界定的记录,这种记录定义为直至下一特定分隔符为止的所有内容。

#include <stdio.h>

ssize_t getline (char **lineptr, size_t *n, FILE *stream)
ssize_t getdelim (char **lineptr, size_t *n, int delimiter, FILE * stream)

getline()从流stream中读入一行(包括换行符和一个终止空字符),并存储于lineptr所指缓冲区中,缓冲区的大小由参数n给出。
在调用getline()之前一般先要调用malloc()(5.5.2节)分配大小为 n个字节的缓冲区,并存放其地址于lineptr。如果这个缓冲区的大小足够容纳输入行,getline()将此行置于缓冲区中。否则,getline()会自动扩大此缓冲区,然后将新缓冲区的地址回填至lineptr,新增加后的缓冲区大小存回至n。特别地,当lineptr为空指针,n为0时,getline()会自动分配初始缓冲区。
getline()调用成功的返回值是读入的字符数(包括换行符,但不包括终止空字符),这使我们能够区分行中的空字符和作为终止符的空字符:终止空字符所在的位置一定等于getline()的返回值。
函数getdelim()类似于getline(),不同的只是作为行终止的分隔符不一定是换行符,而可以通过参数delimiter指定,getdelim()一直读至遇到该字符或文件结束为止。读入的正文包括该分隔符和一个终止的空字符。
实际上,getline()是用getdelim()来实现的:

ssize_t getline (char **lineptr, size_t *n, FILE *stream)
{
   return getdelim (lineptr, n, '\n', stream);
}

这两个函数虽然是GNU 的扩充,但它们能从流中可靠地读一行,特别是getdelim()对正文匹配处理特别方便。因此,这里专门介绍了它们。
每次输出一行可由fputs()或puts()函数来完成。

#include <stdio.h>

int fputs (const char *s, FILE * stream)
int puts (const char *s)

fputs()输出以空字符结尾的字符串s至流stream,但结尾的空字符不写入,也不添加换行符,它只输出字符串中的字符。如果发生错误,该函数返回EOF,否则返回一非负值。
puts()输出以空字符结尾的字符串s至标准输出流stdout并添加一个换行符,但字符串结尾的空字符不写出。注意它与fputs()的不同:fputs()不添加换行符,而puts()则添加换行符。例如,下述三次fputs()的连续调用:

fputs ("Are ", stdout);
fputs ("you ", stdout);
fputs ("hungry?\n", stdout);

输出的正文是“Are you hungry?”后随一个换行符。这个换行符是字符串自身所带的。反之,如果用puts()替代上述调用,则会输出四行:每行一个单词并且最后有一空行。
puts()是用于打印简短消息最方便的函数。

2.4.3 读回退

程序在读输入的过程中,有时候会只想查看一下输入流中的下一字符而并不想将它从输入流中读走,这称为对输入流的超前窥视,因为程序只是对下次要读入的字符提前看一眼。在流I/O的情况下,只能通过首先从流中读出字符,然后再将该字符退回至输入流来实现超前窥视。
回退字符至流的函数是ungetc(),它是getc()的逆操作。

#include <stdio.h>
int ungetc (int c, FILE *stream)

ungetc()将字符c退回至输入流stream。于是下一次从stream的输入将首先读到字符c。若调用成功,该函数返回回退的字符,否则返回EOF。
如果要回退的字符c是EOF,ungetc()不做任何动作并返回EOF,这使得我们在使用getc()的返回值调用ungetc()时无须对getc()的错误进行检测。
大多数系统都只支持回退一个字符,Linux也如此。这意味着连续两次调用ungetc()之间必须有一次读入。如果在未调用getc()的情况下再次调用ungetc()将使前一个回退的字符丢失。
回退的字符不必是从流中最后一次读出的字符,可以是任意字符。事实上并不需要在用ungetc()做回退之前从流中真正读出任何字符!不过以这种方式编程是较奇怪的,通常ungetc()只用来回退刚从同一个流读出的字符。
回退字符并不是将字符送回文件本身,而是送回流的内部缓冲中。因此,如果调用了一个文件定位函数(如fseek()或rewind()),则将丢弃任何未重新被读入的回退字符。
回退一个字符至一个正处在文件尾的流将清除该流的文件尾指示器,因为它使该回退字符重新变为有效输入字符。当读入该字符之后再次读才会遇到文件尾。
例2-4 程序2-4说明了用getc()和ungetc()跳过空白字符的用法。当getc()到达一个非空白字符时,程序用ungetc()回退此字符,从而在下一次读时能再次读到它。
screenshot

2.4.4 块I/O

块I/O也称为二进制I/O,它以固定大小的块为单位而不是以字符或行为单位来读写数据。要读写的数据既可以是字符正文,也可以是二进制数据。函数fread()和fwrite()用于进行这种成块的输入输出。

#include <stdio.h>
size_t fread (void *data, size_t size, size_t count, FILE *stream);
size_t fwrite (const void *data, size_t size, size_t count, FILE * stream);

fread()从流stream中读count个数据项,并存放至data所指的数组中,每个数据项的长度为size字节,所读的总字节数为 count×size。
fwrite()从data所指的区域中写出count个数据项至流stream,每个数据项的长度为size字节,所写出的总字节数为 count×size。
fread()和fwrite()均返回实际读写的数据项数(注意,不是字节数)。若调用成功,返回值等于count;若遇到文件尾或错误,返回值小于count或为EOF;当出现错误时,设置errno指明错误原因;如果count或size为0,则不做任何动作并返回0。
这两个函数常在如下情形中使用:
1)读写一个二进制数组。例如,为了输出一个浮点数组的第二至第五个元素,可以这样调用fwrite():

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

此处指定参数size为数组元素的字节大小,参数count为元素个数。
2)读写一个结构。例如,

struct {
  short  count;
  long   total;
  char   name[NAMESIZE];
}item;
if (fwrite (&item, sizeof(item), 1, fp) != 1)
    perror ("fwrite error");

此处指定参数size为结构的字节大小,参数count为1。
这两种情形的更一般例子是读写一个结构数组。为此,参数size应当是该结构的字节大小,参数count应当是数组的元素个数。
从这两个例子看出,块I/O是按数据原始形态,即二进制格式进行读写的。按二进制格式读写数据的效率常常比使用其他形式的I/O要好,特别是对于浮点数据,二进制格式避免了格式转换处理(2.8节)时精度的丢失。但是它也有自己的问题,就是不能用许多标准的文件处理实用程序(如正文编辑程序)对二进制文件进行检查或修改。特别是,在某个系统中写出的二进制文件一般只能在同一个系统中才能读出,也就是说,二进制文件不能在不同的语言实现或不同类型的计算机系统之间进行移植。其原因主要是:
1)不同的编译器和不同的系统中,由于存储边界对齐要求不同,使得对结构成员在结构内的偏移处理有所不同。有一些编译器有选项开关,允许结构按紧缩方式分配空间(以节省存储)或精确地按边界对齐方式分配空间(以优化运行时各个成员的访问)。这意味着即使在同一个系统,结构的存储分配也是不同的,这取决于具体的编译选项。
2)不同的计算机体系结构存储多字节数据的方式不同。例如,有的采用big-endian,有的采用little-endian(12.3.5节),而有的系统则可以在这两者之间进行选择。
例2-5 程序2-5使用fread()和fwrite()连接一个文件至另一个文件末尾。它的第一个参数指明要复制的文件,第二个参数指明被连接的文件,如果这个文件不存在,则创建它。
程序2-5 fread()和fwrite()连接两个文件之例

#include "ch02.h"
int main (int argc, char *argv[])
{
   int n;
   FILE *from, *to;
   char buf[BUFSIZ] ;
   if (argc != 3) {    /*检查参数 */
      fprintf(stderr, "Usage : %s from-file to-file\n", *argv) ;
      exit (1);
   }
   if ((from = fopen(argv[1],"r")) == NULL)  /* 为读而打开文件from */
      err_exit (argv[1] ) ; 
   /* 以添加方式打开文件to,若此文件不存在,fopen 将创建它*/
   if ((to=fopen(argv[2], "a")) == NULL)
      err_exit(argv[2] ) ;
   /* 现在每次可以从文件from读入并写至to。注意我们写出的字符个数是实际读入
      的字符个数而不总是BUFSIZ字节*/
   while ((n = fread(buf, sizeof(char),BUFSIZ,from)) > 0)
      fwrite (buf, sizeof(char),n,to) ;
   /*关闭文件*/
   fclose (from) ;
   fclose (to) ;
   exit (0) ;
}
相关文章
|
监控 网络协议 Unix
go程序报错Unix syslog delivery error
记录一下问题出错原因
2742 0
《UNIXLinux程序设计教程》一2.7 流缓冲
本节书摘来自华章出版社《UNIXLinux程序设计教程》一 书中的第2章,第2.7节,作者:赵克佳 沈志宇,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1066 0
《UNIXLinux程序设计教程》一3.3 设置描述字的文件位置
本节书摘来自华章出版社《UNIXLinux程序设计教程》一 书中的第3章,第3.3节,作者:赵克佳 沈志宇,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1065 0