Chapter 5 Standard I/O library

1. Introduction

在本章我们介绍标准IO的内容,它们是由ISO C库定义的,并且已经在很多操作系统中被实现,不仅仅是UNIX系统。这里也就是说前面学的那些IO函数的可移动性并不好。标准IO库进行IO操作的时候都是带缓冲操作的,在前一章我们已经学过,代缓冲的IO具有更好的性能。

2. Streams and FILE Objects

第三章中IO操作通常是和文件描述符搭配使用,不过毕竟文件描述符是UNIX系统中的东西,在Windows与之相对的是句柄(handler),显然程序就会缺少一些可移植性。在新的标准中,操作的对象是流。ASCII中单个字符可以用一个字节来比表示,但是有些字符集一个字符是多个字节的。

标准IO可以处理单个字节或者多个字节的字符集,stream’s orientation决定了一次read或者write是多个还是单个字节的字符。如果没有指定的话,IO操作就是面向单个自己的。freopen函数用于清楚stream’s orientation,而fwide用于设置stream’s orientation。

#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);

fwide的作用取决于参数mode的值:

  1. 如果是负数,那么让stream fp是按字节读取的
  2. 如果是正的,那么让stream fp是按照多字节读取的(wide oriented)。

当我们打开一个stream的时候,使用的是标准IO的fopen函数,返回的是一个FILE指针,它包含了标准IO库所需要的信息:底层的文件描述符,以及缓冲区的指针,缓冲区大小等等内容。

3. Standard Input, Standard Output, and Standard Error

标准IO流将STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO这三个文件描述符,替换为了三个标准流stdin, stdout, and stderr。

4. Buffering

标准IO库提供的buffering的目的是将read和write最小化,提高性能。Unfortunately, the single aspect of the standard I/O library that generates the most confusion is its buffering,Three types of buffering are provided:

  1. Fully buffered。这种情况下,只有buffered满的情况下才会进行实际的IO操作,没有特别理解这里的说的是什么,按照作者的意思就是当缓冲区满了的时候进行实际的IO操作将数据写入到外部设备。调用flush或者tcflush。
  2. Line buffered。只有遇到newline character的时候才会进行IO操作。这就使得我们可以每次都输出单个字符,者带来的开销并不大,只有遇到换行符的时候才会执行IO操作。比如说在terminal当中,printf实际上就是这样的,首先输入到缓冲区,然后遇到\n就将数据刷出。当然了,程序结束也会刷出,不然怎么把东西显示到屏幕上? c++当中遇到endl的时候,也会将缓冲区刷新。
  3. Unbuffered。不缓冲任何数据,那么就调用前一章学到的那些read和write,直接发起IO操作。

上面说的Buffering的类型,ISO C标准定义了:

  1. Standard input and standard output are fully buffered,并且不是用于 interactive device。
  2. Standard error is never fully buffered

不过大部分的实现满足如下的类型:

  1. Standard error is always unbuffered
  2. All other streams are line buffered if they refer to a terminal device; otherwise, they are fully buffered

对于terminal device是line buffered,而其他的都是fully buffered。如果我们想修改默认的buf行为,那么可以通过如下两个函数进行修改:

#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf );
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);

这两个函数必须在流的打开之后,但是正式执行IO操作之前使用。如果需要启用buffering,参数buf指定为BUFSIZ,这是一个定义在<stdio.h>中。通常情况下,stream是fully buffered的,如果需要禁止buffering,那么将参数buf设置为NULL即可。通过函数setvbuf可以设置buffering的类型,通过参数mode.如下:

5. Opening a Stream

fopen, freopen, and fdopen 这三个函数都是打开一个 IO stream。签名如下:

#include <stdio.h>
FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type,FILE *restrict fp);
FILE *fdopen(int fd, const char *type);

fopen的作用是在路径pathname上创建IOstream,而freopen的作用是在一个流上再打开一个文件。如果先前的stream已经有orientation,那么会被取消。这个函数通常用于在standard input, standard output, or standard error上打开文件。fdopen的参数是一个文件描述符,那么就可以作用于任何open,dup,dup2等哪些返回文件描述符的函数。这个函数通常用于pipe和network,因为这些类型只能用过文件描述符访问。

ISO C标准定义了15个值用来表示type,如下:

因为在UNIX系统中,文件系统并不区分是文本文件还是二进制文件,所以b选项没什么用。当一个文件的写入模式为追加的时候,即使多个进程同时写入,也可以保证数据的正确性,也就是保证了原子性,这里可以参考前一章提到内容。

一个打开的流调用close函数来关闭,一个带有缓冲区的输出流在close之前将数据刷出。函数签名如下:

#include <stdio.h>
int fclose(FILE *fp);

当一个进程正常的退出,无论是因为exit还是因为main的结束,标准IO流都会将未被成功写入的数据刷出并且将IO流关闭。

6. Reading and Writing a Stream

当我们打开一个stream的时候,可以使用如下三种形式的IO:

  1. Character-at-a-time I/O. 一次读取或者写入一个字符,标准IO库会负责进行缓存,如果有的话。
  2. Line-at-a-time I/O. 一次读取或者写入一行数据,可以通过fputs(写)和fgets(读),每一行都是以换行符为结尾的,并且fgets需要指定行的最大值。
  3. Direct I/O. 由fread和fwrite函数来实现直接IO,整个的适用场景会在后面介绍。

Input Functions

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

#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);

getchargetc(stdin)是相等的,而getcfgetc的区别是,that getc can be implemented as a macro, whereas fgetc cannot be implemented as a macro。

这三个函数返回的是一个被转为int的unsigned char。返回值类型为int 的原因是能够保存所有不同长度的字符集,并且也能够表示是否到达了文末或者是发生了一场,EOF是一个常量,通常的值为-1。这些函数在发生异常或者到文末的时候返回的都是相同的值,那么为了区分这两者,那么需要使用两个不同的函数:

#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
void clearerr(FILE *fp);

任何一个FILE都包括两个flag:error,EOF。这两个标志都会被clearerr所清楚。

Output Functions

output函数和前面的input相类似,函数签名如下:

#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);

putchar(c)putc(c, stdout)。and putc can be implemented as a macro, whereas fputc cannot be implemented as a macro

7. Line-at-a-Time I/O

按行读取的IO由fgetsgets这两个函数,签名如下:

#include <stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf )

参数buf指定了读取的缓冲区,fgets用于从一个流当中读取,而gets则是从standard input中读取。如果书要读取 数据超过了fgets中的参数n,那么该行中的部分数据被读取。gets是不推荐使用的,因为它没有指定缓冲区大小,所以存在缓冲区溢出的危险。另外一个就是gets不会讲newline character放到缓冲区,然而fgets则会。虽然在每一个UNIX实现中都提供了gets,还是推荐使用fgets

往外输出调用的是fputsputs,也尽量避免使用puts

#include <stdio.h>
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);

8. Standard I/O Efficiency

接下来探讨的是标准IO的性能如何,并且和前一章的不带缓冲区的IO进行对比。实验结果如下图,引用书上的:

这里的user CPU time表示的是用于执行用户程序指令所化的时间,system CPU time表示的是执行内核所花的时间,clock time则是整个程序运行的时间。

标准IO的user CPU 时间相较于第一行的带缓冲区的read而言高了很多,这里是因为它们在用户态循环的时间更多。比如说fgetc是一次读取一个字符,那么自然需要更久,fgets带有缓冲区,那么循环的时间相对就会少一些。

总体的system CPU时间差不多,因为它们所需要的系统调用次数差不多。不过使用标准IO不需要自己去确认缓冲区的大小。前面说过getc可以用宏来实现,而fgetc则是函数调用,那么应该来说宏的代码会比较多,但是这里却一样,作者提到这是GNU C编译器做了一些扩展工作。

fgets比getc快了很多,这是因为fgets底层使用了memccpy,这是一个使用汇编来实现的函数,会快一些,如果fgets底层的实现是K&R的getc,那么性能也会差不多。

最后一行是read并且缓冲区为一个字节的情况,是为了和getc进行对比。read长很多因为它每执行一次都会进行系统调用,所以有多少个字符就会执行多少次系统调用,而fgetc后面的系统调用次数会少很多,它一次会多读一些数据过来。不过这里的实验描述都是取决于系统的具体实现的。

9. Binary I/O

fgetc是一次读取一个字符,fgets是一次读取一行字符。如果我们想操作二进制数据,通常我们想写入或者读取整个结构,以四个字节为单位读取和写入int。可以使用getc和putc,逐个循环,但是效率低下。因此,可以使用fwrite和fread这两个函数,签名如下:

#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);

比如说将float数组中的数据写入,代码如下:

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

fread和fwirte的返回值分别是所读取的对象数(而不是字节数)和写入的对象数。

10. Positioning a Stream

标准IO流有三种方法来调整offset:

  1. ftell和fseek是从system V7中来的,这两个函数假设offset都能以long来表示
  2. ftello和fseeko是在 Single UNIX specification中引入的,类型为off_t,主要是解决long不能够表达的情况
  3. fgetpos和fsetpos是在ISO C标准中引入的,类型是fpos_t。

如果程序要被适用于non UNIX系统,那么应该使用fgetpos和fsetpos。函数签名如下:

#include <stdio.h>
long ftell(FILE *fp);
int fseek(FILE *fp, long offset, int whence);
void rewind(FILE *fp);

#include <stdio.h>
off_t ftello(FILE *fp);
int fseeko(FILE *fp, off_t offset, int whence);

#include <stdio.h>
int fgetpos(FILE *restrict fp, fpos_t *restrict pos);
int fsetpos(FILE *fp, const fpos_t *pos);

ftell是返回此时的offset,fseek则是用于设置offset,whence参数和lseek中的SEEK_SET,SEEK_CUR,SEEK_END意思相同。ftello和fseeko并没有什么大的区别,只是将偏移量设置为了类型off_t。

11. Formatted I/O

Formatted Output

输出的格式化由如下五个函数:

#include <stdio.h>
int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fp, const char *restrict format, ...);
int dprintf(int fd, const char *restrict format, ...);
int sprintf(char *restrict buf, const char *restrict format, ...);
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);

printf的作用是将格式化后的字符写到标准输出,fprintf则是写向指定的流fp,dprintf格式化后写入到指定的文件描述符中,sprintf则是将格式化后的字符放到参数buf中。但是sprintf可能存在缓冲区溢出的问题,因为buf放不下被格式化后的字符串。所以引入了snprintf,它的参数n就避免了缓冲区溢出的问题,超过缓冲区大小的部分字符会被直接丢弃。

原文还讲了很多和格式化有关的东西,比较琐碎,先略。以上提到的函数有类似的版本,只是将可变参数变成了va_list,签名如下:

#include <stdarg.h>
#include <stdio.h>
int vprintf(const char *restrict format, va_list arg);
int vfprintf(FILE *restrict fp, const char *restrict format, va_list arg);
int vdprintf(int fd, const char *restrict format, va_list arg);
int vsprintf(char *restrict buf, const char *restrict format, va_list arg);
int vsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list arg);

Formatted Input

以下三个函数用于讲输入数据进行格式化:

#include <stdio.h>
int scanf(const char *restrict format, ...);
int fscanf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(const char *restrict buf, const char *restrict format, ...);

scanf用于从标准输入中按照format所指定的形式进行格式化数据。而fscanf这是用于从文件中按照format的格式进行格式化。sscanf则是从缓冲区当中按照format进行格式化。这几个函数也有vscanf,vfscanf等版本,与之的区别就是可变参数...变成了va_list

12. Implementation Details

前面说过,在UNIX下的标准IO是由文件描述符,read,write等IO routines等实现的。每一个标准IO流都有一个与之相对的文件描述符,我们可以通过调用函数fileno()来获得。

#include <stdio.h>
int fileno(FILE *fp);

但是这个函数并不是IOS C标准的。

13. Temporary Files

ISO C标准定义了两个函数用于帮助创建临时文件,签名如下:

#include <stdio.h>
char *tmpnam(char *ptr);
FILE *tmpfile(void);

tmpnam会在/tmp目录下生成一个可用的文件名,并且将创建的文件名写入到参数ptr中,但是并没有实际创建文件。tmpfile则是会创建一个临时文件,并且会在程序结束的时候自动删除。Linux 推荐使用tmpfile,而不是tmpnam。在XSI标准中引入了两个新的函数mkdtemp和mkstemp,具体用法就不再赘述了。

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇