APUE学习笔记3——第五章——标准I/O库

时间:2022-05-07 09:55:43

APUE学习笔记3——第五章——标准I/O库


学号:16340043
中山大学
本博客为《UNIX环境高级编程》的学习笔记,希望能对大家有所帮助


1.前面的废话

今天第二篇咯(过了国庆肯定做不到一天一更…一周能写两篇就不错了…)


2.博客正文

5.1 引言

本章讲述标准I/O库(终于有点熟悉的内容啦)

5.2 流和FILE对象

对于第三章中的I/O函数,所有的操作都是围绕着文件描述符的,而对于标准I/O库,它们的操作是围绕流进行的。当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。

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

只有两个函数可以改变流的定向。freopen函数(稍后讨论)清楚一个流的定向;fwide函数可用于设置流的定向:

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

其中:

  • 根据mode参数的不同值,fwide函数执行不同的工作:
mode值 功能
使指定的流字节定向
使指定的流宽定向
0 不设置流的定向,但返回该流定向的值
  • fp:指向一个FILE对象的指针(文件指针)
  • 返回值:若流是宽定向的,返回正值;若流是字节定向的,返回负值;若流是未定向的,返回0

由于该函数没有出错返回,我们在使用它之前应把errno清除,使用后检查errno的值来判断流是否无效

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

有3个流标准输入、标准输出和标准错误可以自动地被进程使用,通过文件指针stdin、stdout和stderr加以引用

5.4 缓冲

缓冲的目的使尽可能减少read和write的次数。标准I/O提供了以下3种类型的缓冲:

  • 1)全缓冲。要填满缓冲区后才能进行实际I/O操作
  • 2)行缓冲。当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。行缓冲有两个限制:

    a.由于缓冲区的长度是固定的,所以只要填满了它,即使没有换行符,也会执行I/O操作
    b.通过I/O库要求从一个不带缓冲的流,或者是一个行缓冲的流得到输入数据,那么会冲洗1所有行缓冲输出流

  • 3)不带缓冲。不对字符进行缓冲存储。

很多系统默认使用下列类型的缓冲:

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

对任何一个给定的流,若我们不喜欢系统默认,可以用下列函数更改:

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

其中:

  • fp:指向文件的指针
  • 对于setbuf,若buf指向长度为BUFSIZE的缓冲区,设为全缓冲;若该流与终端设备相关,那么可能会被设置为行缓冲;若buf为NULL,则设为不带缓冲
  • 对于setvbuf,可通过mode具体设定所需缓冲类型:
mode值 类型
_IOFBF 全缓冲
_IOLBF 行缓冲
_IONBL 不带缓冲

mode的优先级高于buf参数,若mode为带缓冲的,而buf为NULL那么系统会自动分配一个缓冲区给流

一般而言,应由系统自动分配缓冲区,这样在关闭流的时候缓冲区被自动释放

任何时候,我们都可以强制冲洗一个流:

 #include <stdio.h>
int fflush(FILE *fp);
  • 返回值:若成功,返回0;若出错,返回EOF

特殊情况:若fp为NULL,则所有输出流被冲洗

5.5 打开流

下列3个函数用来打开一个标准I/O流:

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

三个函数区别如下:

  • 1)fopen打开路径名为pathname的指定文件
  • 2)freopen在一个指定流上打开一个指定文件。若流已经打开,则消除该流;若流已经定向,则清除该定向
  • 3)fdopen使一个标准I/O流与一个文件描述符相结合

type有15种不同的值:

type 说明
r或rb2 为读而打开
w或wb 把文件截至0长,或为写而创建
a或ab 追加:为在文件尾写而打开,或为写而创建
r+或r+b或rb+ 为读和写而打开
w+或w+b或wb+ 把文件截至0长,或为读和写而打开
a+或a+b或ab+ 在文件尾读和写而打开或创建

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

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

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

5.6 读和写流

一旦打开了流,可选择3种不同类型的I/O方式

  • 1)每次一个字符的I/O
  • 2)每次一行的I/O
  • 3)直接I/O

先说输入函数(这一块就有交叉啦)

一次读一字符:

#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
  • 返回值:若成功,返回下一个字符;若出错或到达文件尾,返回EOF
  • getchar()相当于getc(stdin)
  • 前两个函数的区别是,前者可被实现为宏,而后者不行

为了区分上述返回值的-1是出错还是到达文件尾,必须调用ferror或feof

#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
//两个函数在条件为真时返回非0;否则返回0
void clearerr(FILE *fp);

使用clearerr可以清除出错标志和文件尾标志

从流中读取数据后,可用ungetc将字符再压进流中

#include <stdio.h>
int ungetc(int c, FILE *fp);
  • 返回值:若成功,返回c;若出错,返回EOF

回送的字符可以再读出(不知道这个函数有什么用…),但是读出顺序与压回顺序相反
不能回送EOF

最后是输出函数

#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c,FILE *fp);
int putchar(int c);
  • 返回值:若成功,返回c;若出错,返回EOF

与输入函数之间的差别类似

5.7 每次一行的I/O

下面函数提供每次输入一行的功能:

#include <stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);
  • 返回值:若成功,返回buf;若出错或达到文件尾,返回NULL
  • gets3从标准输入读,fgets从指定的流读
  • n:缓冲的长度(fgets读取n-1个字符,以null字节结尾)

下面函数提供每次输出一行的功能:

#include <stdio.h>
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);
  • 返回值:若成功,返回非负值;若出错,返回EOF
  • puts会在输出完后添加一个换行符

5.8 标准I/O的效率

以上几个函数在循环多次时,对内核提出的读写请求数基本相同,所以无需考虑缓冲及最佳I/O长度的选择。

使用每次一行I/O版本的效率约为每次一个版本的两倍。

5.9 二进制I/O

二进制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);
  • 返回值:读或写的对象数
  • ptr:指向数据的指针
  • size:要读或写的元素的长度
  • nobj:要读或写的元素的个数

二进制I/O只能用来读同一系统上已写的数据
在不同的系统间交换二进制数据的实际方法是使用互认的规范格式

5.10 定位流

有三种方法可以定位标准I/O流

1)ftell和fseek函数

#include <stdio.h>
long ftell(FILE *fp);
//返回值:若成功,返回当前文件位置指示;若出错,返回-1L
int fseek(FILE *fp, long offset, int whence);
//返回值:若成功,返回0;若出错,返回-1
void rewind(FILE *fp);
  • ftell用于二进制文档时,返回当前位置距起始位置的字节数
  • whence的值与3.6中fseek函数的相同
  • offset:偏移量。作用于文本文档时,文件当前位置不能简单地用偏移量来度量,此时offset只能有两种值:0或ftell返回的值
  • rewind函数用于将一个流设置到文件的起始位置

2)除了偏移量的类型是off_t而非long以外,ftello函数与ftell相同,fseeko函数与fseek相同

3)fgetpos和fsetpos函数

#include <stdio.h>
int fgetpos(FILE *fp, fpos_t *restrict pos);
int fsetpos(FILE *fp, const fpos_t *pos);
  • 返回值:若成功,返回0;若出错,返回非0
  • fpos_t:记录文件位置的数据类型

5.11 格式化I/O

格式化输出是由5个printf函数来处理的:

#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写至指定的流,dprintf写至指定的文件描述符,sprintf将格式化字符送入数组buf中,并加上一个null字节4

  • snprintf中的n指定了最多读取的字符数,再多的被丢弃

至于怎么格式化输出(%d那一套东西),我就不记下来了,学过C的都知道

对,还有5中printf族的变体,在前面5个函数的前面加上一个v5,区别是可变参数(…)列表被替换成arg

格式化输入是由3个scanf函数来执行的:

#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, ...);
  • 返回值:赋值的输入项数;若输入出错或在任一转换前已达到文件尾端,返回EOF

scanf族也有前面加v的3个scanf,区别与printf的的类似

5.12 实现细节6

可以对一个流调用fileno函数以取得其描述符

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

-返回值当然是文件描述符啦

5.13 临时文件

ISO C 标准I/O库提供了两个函数以帮助创建临时文件:

#include <stdio.h>
char *tmpnam(char *ptr);
//返回值:指向唯一路径名的指针
FILE *tmpfile(void);
//返回值:若成功,返回文件指针;若失败,返回NULL
  • tmpnam函数产生一个与现有文件名不同的有效路径名字符串,最多调用次数为TMP_MAX7
  • 若ptr为NULL,所产生的路径名放在一个静态区中,指向该静态区的指针作为函数值返回,后续调用tmpnam时,会重写该静态区,所以我们应该保存路径名的副本而不是指针的副本
  • tmpfile创建一个二进制文件(UNIX中不区分)

函数mkdtemp和mkstemp可处理临时文件:

#include <stdlib.h>
char *mkdtemp(char *template);
//返回值:若成功,返回指向目录名的指针;若出错,返回NULL
int mkstemp(char *template);
//返回值:若成功,返回文件描述符;若出错,返回-1

其实这两个函数更像是创建随机文件名的非临时文件……(我的理解)只是访问权限略有不同。

5.14 内存流

内存流虽仍使用FILE进行访问,但其实没有底层文件

有三个函数可用于内存流的创建,第一个是fmemopen函数:

include <stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
  • 返回值:若成功,返回流指针;若出错,返回NULL

fmemopen函数允许调用者提供缓冲区用于内存流

  • buf:缓冲区开始位置
  • size:缓冲区大小的字节数
  • type:如何使用流:
type 说明
r或rb8 为读而打开
w或wb 为写而打开
a或ab 追加:为在第一个null字节处写而打开
r+或r+b或rb+ 为读和写而打开
w+或w+b或wb+ 把文件截至0长,为读和写而打开
a+或a+b或ab+ 追加:为在第一个null字节处读和写而打开
  • 这个type与5.5中有微小的差别
    1)无论何时以追加模式打开内存流,当前文件位置设为缓冲区中第一个null字节,若缓冲区中不存在null字节,那么当前位置就是缓冲区结尾的后一个字节
    2)若buf参数是null指针,打开流进行读写没有任何意义
    3)任何时候需要增加流缓冲区中的数据量,以及调用fclose、fflush、fseek、fseeko以及fsetpos时都会在当前位置写入一个null字节

用于创建内存流的其他两个函数是open_memstream和open_wmemstream:

#include <stdio.h>
FILE *open_memstream(char **bufp, size_t *sizep);

#include <wchar.h>
FILE *open_wmemstream(wchar_t **bufp, size_t *sizep);
  • 返回值:若成功,返回流指针;若出错,返回NULL
  • 从名字可以看出,带w的是面向宽字节的

这两个函数与前两个的区别是:

  • 1)创建的流只能写打开
  • 2)不能指定自己的缓冲区,但能通过两个参数访问缓冲区地址和大小
  • 3)关闭流后要自行释放缓冲区
  • 4)对流添加字节会增加缓冲区大小

因为避免了缓冲区溢出,内存流非常适用于创建字符串。

5.15 标准I/O的替换软件

标准I/O库并不完善
一个不足之处是效率不高,因为要复制的次数较多(内核->标准I/O缓冲区->行缓冲区)

替代品有:快速I/O库、sfio、ASI等

5.16 小结

大多数UNIX应用程序都使用标准I/O库
应该看到,标准I/O库使用了缓冲技术,而它正是产生很多问题、引起很多混淆的部分


3.习题



  1. 术语冲洗(flush)说明标准I/O缓冲区的写操作(就是把缓冲区的数据强行输出…吧)
  2. 使用b使得I/O系统区分文本文件和二进制文件,因为UNIX系统对这两种文件不做区分,所以并无作用
  3. 不推荐使用gets,因为无法指定缓冲的长度n,所以可能造成缓冲区溢出
  4. 该字节不在返回值中
  5. printf好像就是由vprintf写的哦
  6. 本节是一个很长很长的实例,没法打…
  7. 该常量定义在头文件<stdio.h>中
  8. 使用b使得I/O系统区分文本文件和二进制文件,因为UNIX系统对这两种文件不做区分,所以并无作用