第5章 标准I/O库
标准I/O库,它是C语言标准库的一部分,提供了更高级别的I/O函数,用于UNIX环境中进行输入和输出操作。
5.0 概念
标准I/O流:
- 标准I/O库引入了三个标准I/O流:标准输入、标准输出、标准错误。这些流是FILE类型的对象,分别对应于键盘输入、屏幕输出和错误输出。
标准I/O函数:
- 标准I/O库提供了一组函数,如
printf
、scanf
、fprintf
、fscanf
,用于格式化输入和输出。这些函数使用标准I/O流进行操作。 printf
用于将格式化的文本输出到标准输出,而scanf
用于从标准输入中读取格式化的数据。
I/O缓冲:
- 标准I/O库使用内部缓冲区来提高I/O性能。输出数据首先被写入缓冲区,然后再由库函数写入文件。输入数据也首先被读入缓冲区,然后由库函数从缓冲区读取。
- 使用
fflush
函数可以强制刷新输出缓冲,将数据写入文件。
格式化输入和输出:
- 标准I/O库允许你以特定的格式将数据输出到文件或从文件中读取数据。例如,你可以指定数据的格式、字段宽度、小数点位数等。
文件操作:
- 除了标准I/O流,标准I/O库也可以与普通文件一起使用。你可以使用
fopen
函数打开文件,然后使用fprintf
和fscanf
来执行文件操作。
错误处理:
- 标准IO库的函数可以返回错误,你应该在使用它们时进行适当的错误检查和处理。
5.1 流和FILE对象
1、标准I/O库与文件I/O区别:
-
标准I/O库处理很多细节,如缓冲区分片、以优化的块长度执行IO等。
-
文件I/O函数都是针对文件描述符的。当打开一个文件时,即返回一个文件描述符,然后该文件描述符就用于后续的I/O操作。
-
标准I/O库是围绕流进行的,当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相结合。
-
当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针。该对象通常是一个结构,它包含了(用于实际I/O的文件描述符、指向流缓存的指针、缓存的长度、当前在缓存中的字符数、出错标志等等)
-
在本书中,我们称指向FILE对象的指针(类型为FILE*)为文件指针。
2、对于ASCII字符集,一个字符用一个字节表示;对于国际字符集,一个字符可以用多个字节表示。
-
标准IO文件流可用于单字节或者多字节符集。流的定向决定了所处理的字符是单字节还是多字节的。
-
当一个流最初被创建时,它并没有定向。
- 若在未定向的流上使用一个多字节IO函数,则将该流的定向设置为宽定向的(即处理多字节)
- 若在未定向的流上使用一个单字节IO函数,则将该流的定向设置为字节定向的(即处理单字节)
-
只有两个函数可以改变流的定向
- freopen函数清除一个流的定向
- fwide函数设置流的定向
3、fwide函数:设置流的定向
#include <stdio.h>
#include <wchar.h>
int fwide(FILE* fp,int mode);
- 参数:
- fp:FILE文件对象的指针
- mode:流的定向模式
- 如果mode是负数,则函数试图使指定的流为字节定向(并不保证修改成功,因为fwide并不改变已定向流的定向)
- 如果
mode
是正数,则函数试图使指定的流为宽定向的(并不保证修改成功,因为fwide
并不改变已定向流的定向) - 如果
mode
为0,则函数不试图设置流的定向,而直接返回该流定向的值
- 返回值:
- 若流是宽定向的,返回正值
- 若流是字节定向的,返回负值
- 若流是未定向的,返回0
这里并没有函数失败的情况
注意:
fwide
并不改变已定向流的定向。- 如果
fp
是无效流,由于fwide
从返回值无法得知函数执行成功还是失败。那么我们必须采用这个方法:首先在调用fwide
之前清除errno
。然后在fwide
之后检查errno
的值。通过errno
来检测fwide
执行成功还是失败。
4、FILE
指针:当使用fopen
函数打开一个流时,它返回一个执行FILE
对象的指针。该对象通常是一个结构,包含了标准IO库为管理该流所需要的所有信息,包括:
- 用于实际IO的文件描述符
- 指向用于该流缓冲区的指针
- 该流缓冲区的长度
- 当前在缓冲区中的字符数
- 出错标志
应用程序没必要检验FILE
对象,只需要将FILE
指针作为参数传递给每个标准IO函数。
5、操作系统对每个进程与定义了3个流,并且这3个流可以自动地被进程使用,他们都是定义在<stdio.h>
中:
- 标准输入:预定义的文件指针为
stdin
,它内部的文件描述符就是STDIN_FILENO
- 标准输出:预定义的文件指针为
stdout
,它内部的文件描述符就是STDOUT_FILENO
- 标准错误:预定义的文件指针为
stderr
,它内部的文件描述符就是STDERR_FILENO
5.2 缓存
标准I/O提供缓存的目的是尽可能减少使用read和write调用的数量。
标准IO库对每个IO流自动地进行缓冲管理,从而避免了程序员需要手动管理这一点带来的麻烦。
1、标准I/O提供了三种类型的缓存:
-
全缓存:
填满标准I/O缓冲区后才进行实际I/O操作。若一个流第一次执行I/O操作,通常调用malloc获得需使用的缓存。
术语刷新(flush)说明标准I/O缓存的写操作。缓存可有标准I/O自动刷新(当填满一个缓存时),或者调用函数fflush刷新一个流。
在UNIX环境中,刷新有两种意思:
①标准I/O库方面:刷新意味着将缓存中的内容写到磁盘上。(该缓存可以只是局部填写的)。
②在终端驱动程序方面:刷新表示丢弃已存在缓存中的数据。
-
行缓存:
输入和输出遇到换行符,标准I/O库执行I/O操作。这允许一次输出一个字符(用标准I/O fputc函数)但只有在写了一行之后才进行实际I/O操作。
当流涉及一个终端时(例如标准输入和标准输出),典型地使用行缓存。
对行缓存有两个限制:
①因为标准I/O库用来收集每一行地缓存的长度时固定的,所以只要填满了缓存,那么即使还没有写一个新行符,也进行I/O操作。
②当你从一个没有缓存或行缓存的输入流中获取数据时,输出流的缓存会被刷新。对于行缓存的输入流,内核可以根据需要从内核获取数据,而不必立刻刷新缓存,以提高性能。
-
不带缓存:
标准I/O库不对字符进行缓存。用标准I/O函数写若干字符到不带缓存的流中,则相当于用write系统调用函数将这些字符写至相关联的打开文件上。标准出错流stderr通常时不带缓存的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个新行字符。
2、缓存特征:
- 当且仅当标准输入和标准输出并不涉及交互作用设备时,它们才是全缓存的。
- 标准出错决不会是全缓存的。
- SVR4和4.3+BSD的系统默认使用下列类型的缓存:
- ①标准出错是不带缓存的。
- ②如若是涉及终端设备的其他流,则它们是行缓存的;否则是全缓存的。
若有需要,可调用下列两个函数中的一个更改缓存类型:
#include <stdio.h>
void setbuf(FILE* fp, char* buf);
int setvbuf(FILE* fp, char* buf, int mode, size_t size);
//这些函数一定要在流已被打开后调用(这是十分明显的,因为每个函数都要求一个有效的文件指针作为它们的第一个参数。)
//而且也应在对该流执行任何一个其他操作之前调用。
-
参数:
- fp:被打开的文件对象的指针
- buf:一个缓冲区的指针。缓冲区长度必须为
BUFSIZ
常量(该常量定义在<stdio.h>
中)。- 如果
buf
为NULL
,则是关闭缓冲 - 如果
buf
非NULL
,则通常设定该流为全缓冲的。但若该流与一个设备终端相关,则设为行缓冲的
- 如果
-
对于
setvbuf
函数:buf
:一个缓冲区的指针。缓冲区长度为size
。- 若
buf
为NULL
,且mode
为_IONBF
:则该流为不带缓冲的。因为此时忽略buf
和size
参数 - 若
buf
为NULL
,且mode
不是_IONBF
:则标准IO库将自动为该流分片合适长度的缓冲区(即BUFSIZE
长度),然后设定该流为指定的mode
- 若
mode
:指定缓冲类型。可以为:_IOFBF
:全缓冲。_IOLBF
:行缓冲_IONBF
:不带缓冲。此时忽略buf
和size
参数
size
:缓冲的长度
-
返回值:
- 成功: 返回0
- 失败: 返回非0(并不是-1)
注意:
- 如果在一个函数内分配一个自动变量类型的标准IO缓冲区,则从该函数返回之前,必须关闭流。因此自动变量是栈上分配,函数返回之后自动变量被销毁
- 某些操作系统将缓冲区的一部分存放它自己的管理操作信息,因此可以存放在缓冲区中的实际数据字节数将少于
size
- 通常推荐利用操作系统自动选择缓冲区长度并自动分配缓冲区。在这种情况下若关闭此流,则标准IO库会自动释放缓冲区
函数 | mode | buf | 缓存及长度 | 缓存地类型 |
---|---|---|---|---|
setbuf | nonnull | 长度为BUFSIZ的用户缓存 | 全缓存或行缓存 | |
setbuf | NULL | (无缓存) | 不带缓存 | |
setvbuf | _IOFBF | nonnull | 长度为size的用户缓存 | 全缓存 |
setvbuf | _IOFBF | NULL | 合适长度的系统缓存 | 全缓存 |
setvbuf | _IOLBF | nonnull | 长度为size的用户缓存 | 行缓存 |
setvbuf | _IOLBF | NULL | 合适长度的系统缓存 | 行缓存 |
setvbuf | _IONBF | 忽略 | 无缓存 | 不带缓存 |
3、fflush函数:手动冲洗一个流
#include <stdio.h>
int fflush(FILE *fp);
//返回:若成功则为0,若出错则为EOF
-
参数:
fp
:被打开的文件对象的指针
-
返回值:
- 成功:返回0
- 失败:返回
EOF
(并不是-1)
此函数使该流所有未写的数据都被传递至内核。作为一种特殊情形,如若fp是NULL,则此函数刷新所有输出流。
- 冲洗是双向的:输出流 —> 内核 —> 磁盘或者终端; 输入流—> 用户缓冲区
- 冲洗并不是立即写到磁盘文件中。冲洗只是负责数据传到内核
5.3 打开关闭流
下列三个函数可用于打开一个标准I/O流。
#include <stdio.h>
FILE *fopen(const char* pathname, const char* type);
FILE *freopen(const char* pathname, const char* type,FILE* fp);
FILE *fdopen(int filedes, const char* type);
//三个函数的返回:若成功则为文件指针,若出错则为NULL
-
参数:
-
type
:指定对该IO流的读写方式:"r"
或者"rb"
:为读打开"w"
或者"wb"
:写打开。若文件存在则把文件截断为0长;若文件不存在则创建然后写"a"
或者"ab"
:追加写打开;若文件存在每次都定位到文件末尾;若文件不存在则创建然后写"r+"
或者"r+b"
或者"rb+"
:为读和写打开"w+"
或者"w+b"
或者"wb+"
:若文件存在则文件截断为0然后读写;若文件不存在则创建然后读写"a+"
或者"a+b"
或者"ab+"
:若文件存在则每次都定位到文件末尾然后读写;若文件不存在则创建然后读写
- 其中
b
用于区分二进制文件和文本文件。但是由于UNIX
内核并不区分这两种文件,所以在UNIX环境中指定b
并没有什么用 - 创建文件时,无法指定文件访问权限位。POSIX默认要求为:
S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
-
对于 fopen
函数:
pathname
:待打开文件的路径名
对于freopen
函数:pathname
:待打开文件的路径名fp
:在指定的流上打开文件。若fp
已经打开,则先关闭该流;若fp
已经定向,则清除该定向。
对于 fdopen
函数:
-
fd
:打开文件的文件描述符 -
对于
fopen
,type
意义稍微有点区别。因为该描述符已经被打开,所以fdopen
为写而打开并不截断该文件。另外该文件既然被打开并返回一个文件描述符,则它一定存在。因此标准 IO追加写方式也不能创建文件 -
返回值:
- 成功: 返回文件指针
- 失败: 返回
NULL
这几个函数的常见用途:
fopen
常用于打开一个指定的文件,返回一个文件指针freopen
常用于将一个指定的文件打开为一个预定义的流(标准输入、标准输出或者标准错误)fdopen
常用于将文件描述符包装成一个标准IO流。因为某些特殊类型的文件(如管道、socket
文件)不能用fopen
打开,必须先获取文件描述符,然后对文件描述符调用fdopen
。
注意:当以读和写类型打开一个文件时(type
中带+
号的类型),有下列限制:
- 如果写操作后面没有
fflush,fseek,fsetpos,rewind
操作之一,则写操作后面不能紧跟读操作 - 如果读操作后面没有
fseek,fsetpos,rewind
操作之一,也没有到达文件末尾,则在读操作之后不能紧跟写操作
注意:按照系统默认,流被打开时是全缓冲的。但是如果流引用的是终端设备,则安装系统默认,流被打开时是行缓冲的。
fclose
:关闭一个打开的流
#include<stdio.h>
int fclose(FILE *fp);
- 参数:
fp
:待关闭的文件指针
- 返回值:
- 成功: 返回 0
- 失败: 返回 -1
在该文件被关闭之前:
fclose
会自动冲洗缓冲中的输出数据- 缓冲区中的输入数据被丢弃
- 若该缓冲区是标准IO库自动分配的,则释放此缓冲区
当一个进程正常终止时(直接调用exit
函数,或者从main
函数返回):
- 所有带未写缓存数据的标准IO流都被冲洗
- 所有打开的标准IO流都被关闭
这三个函数的区别:
- fopen打开路径名由pathname指示的一个文件。
- freopen在一个特定的流上(由fp指示)打开一个指定的文件(其路径名由pathname 指示),如若该流已经打开,则先关闭该流。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准出错。
- fdopen取一个现存的文件描述符(我们可能从open,dup,dup2,fcntl或pipe函数得到此文件描述符),并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数获得的插述符。因为这些特殊类型的文件不能用标准I/O fopen函数打开,首先必须先调用设备专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与该描述符相结合。
type参数指定对该I/O流的读、写方式,ANSI C规定type参数可以有15种不同的值,它们示于表5-2中。
5.4 读写流
一旦打开了流,可以在3中不同类型的非格式化IO中选择,对流进行读、写操作:
- 每次一个字符的I/O。一次读或写一个字符,如果流是带缓存的,则标准I/O函数处理所有缓存。
- 每次一行的I/O。**使用fgets和fputs一次读或写一行。每行都以一个换行符终止。**当调用fgets时,应说明能处理的最大行长。
- 直接I/O。fread和fwrite函数支持这种类型的I/O。每次I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中读或写一个结构。
直接I/O这个术语有时也被称为:二进制I/O、一次一个对象I/O、面向记录的I/O或面向结构的I/O。
5.4.1 输入函数
1、以下三个函数可用于一次读一个字符。
#include<stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
//三个函数的返回:若成功则为下一个字符,若已处文件尾端或出错则为EOF
- 参数:
fp
:打开的文件对象指针
- 返回值:
- 成功:则返回下一个字符
- 到达文件尾端:返回
EOF
- 失败:返回
EOF
**函数getchar等同于getc(stdin)。**它从标准输入中读取一个字符
- 前两个函数的区别是getc可被实现为宏,而fgetc则不能实现为宏。
- getc参数不应当是具有副作用的表达式。
- fgetc一定是一个函数,所以可以得到其地址。这就允许fgetc的地址作为一个参数传送给另一个参数。
- 调用fgetc所需时间很可能长于调用getc,因为调用函数通常所需的时间长于调用宏。
- 这三个函数在返回下一个字符时,将
unsigned char
类型转换成了int
类型。
因为需要通过返回
EOF
来标记到达末尾或者出错。而EOF
通常是常量 -1 。所以需要返回int
2、ferror/feof
函数:查看是读文件出错,还是到达读文件遇到尾端
#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
- 参数:
fp
:打开的文件对象指针
- 返回值:
- 若条件为真:则返回非 0
- 若条件为假: 则返回 0
当读流返回EOF
时,我们可能不清楚到底是遇到错误,还是读到了文件尾端。此时必须调用ferror
或者feof
来区别这两种情况。
3、clearerr
函数:清除文件出错标志和文件结束标志
#include <stdio.h>
void clearerr(FILE *fp)
- 参数:
fp
:打开的文件对象指针
在大多数操作系统中,每个流在FILE
对象中维护了两个标志:
- 出错标志
- 文件结束标志
4、调用clearerr
函数可以清除这两个标志
#include <stdio.h>
int ungetc(int c,FILE *fp);
- 参数:
c
:待压入字符转换成的整数值fp
:打开的文件对象指针
- 返回值:
- 成功:则返回
c
- 失败:返回
EOF
- 成功:则返回
注意:
- 若根据某个序列向流中压入一串字符,则再从该流中读取的字符序列是逆序的。即最后压入的字符最先读出
- 可以执行任意次数的压入单个字符,但是不支持一次压入多个字符
- 不能压入
EOF
。但是当已经读到文件尾端时,支持压入一个字符,此时ungetc
会清除该流的文件结束标志
5.4.2 输出函数
1、putc/fputc/putchar
函数:一次写一个字符
#include<stdio.h>
int putc(int c,FILE *fp);
int fputc(int c,FILE *fp);
int putchar(int c);
- 参数:
c
:待写字符转换成的整数值fp
:打开的文件对象指针
- 返回值:
- 成功:则返回
c
- 失败:返回
EOF
- 成功:则返回
注意:
putchar(c)
等价于putc(c,stdout)
。它向标准输出中写一个字符putc
和fputc
的区别在于:putc
可能通过宏定义来实现,而fputc
不能实现为宏
2、fgets/gets
函数:一次读一行字符:
#include <stdio.h>
char* fgets(char* buf, int n, FILE* fp);
char* gets(char *buf);
-
参数:
buf
:存放读取到的字符的缓冲区地址
对于
fgets
函数:n
:缓冲区长度fp
:打开的文件对象指针
-
返回值:
- 成功:则返回
buf
- 到达文件尾端:返回
NULL
- 失败:返回
NULL
- 成功:则返回
注意:
- 对于
fgets
函数,必须指定缓冲区的长度n
。该函数一直读到下一个换行符为止,但是不超过n-1
个字符。- 无论读到多少个字符,缓冲区一定以
null
字节结尾 - 若某一行包括换行符超过
n-1
个字节,则fgets
只返回一个不完整的行;下次调用fgets
会继续读该行
- 无论读到多少个字符,缓冲区一定以
- 对于
gets
函数,从标准输入总读取字符。由于无法指定缓冲区的长度,因此很可能造成缓冲区溢出漏洞。故该函数不推荐使用 - 对于发生错误和读到末尾,都是返回
NULL
3、fread/fwrite函数:执行二进制读写I/O
#include <stdio.h>
size_t fread(void *ptr,size_t size, size_t nobj, FILE* fp);
size_t fwrite(const void* ptr, size_t size, size_t nobj, FILE* fp);
- 参数:
ptr
:存放二进制数据对象的缓冲区地址size
:单个二进制数据对象的字节数(比如一个struct
的大小)nobj
:二进制数据对象的数量fp
:打开的文件对象指针
- 返回值:
- 成功或失败: 读/写的对象数
- 对于读:如果出错或者到达文件尾端,则此数字可以少于
nobj
。此时应调用ferror
或者feof
来判断究竟是那种情况 - 对于写:如果返回值少于
nobj
,则出错
- 对于读:如果出错或者到达文件尾端,则此数字可以少于
- 成功或失败: 读/写的对象数
使用二进制IO的基本问题是:它只能用在读取同一个操作系统上已写的数据。如果跨操作系统读写,则很可能工作异常。因为:
- 同一个
struct
,可能在不同操作系统或者不同编译系统中,成员的偏移量不同 - 存储多字节整数和浮点数的二进制格式在不同的操作系统中可能不同
案例:将一个浮点数组的第2至第5个元素写至一个文件上
float data[10];
if(fwrite(&data[2],sizeof(float),4,fp) != 4)
err_sys("fwrite error");
//其中,指定size为每个数组元素的长度,nobj为欲写的元素数。
案例:读或写一个结构。
struct{
short count;
long total;
char name[NAMESIZE];
}item;
if(fwrite(&item, sizeof(item),1,fp)!=1)
err_sys("fwrite error");
//其中,指定size为结构的长度,nobj为1(要写的对象数)。
5.5 定位流
-
通过
ftell/fseek
函数:#include<stdio.h> long ftell(FILE *fp);
-
参数:
fp
:打开的文件对象指针 -
返回值:
- 成功:返回当前文件位置指示
- 失败:返回 -1L
若是二进制文件,则文件指示器是从文件开始位置度量的,并以字节为度量单位。
ftell
就是返回这种字节位置。
#include<stdio.h> int fseek(FILE *fp,long offset,int whence);
-
参数:
fp
:打开的文件对象指针offset
:偏移量。其解释依赖于whence
whence
:偏移量的解释方式:SEEK_SET
常量:表示从文件的起始位置开始SEEK_CUR
常量:表示从文件的当前位置开始SEEK_END
常量:表示从文件的尾端开始
-
返回值:
- 成功:返回 0
- 失败:返回 -1
原书说,对文本文件和二进制文件,
fseek
定位有某些限制。但是经过在ubuntu 16.04
上测试,可以任意定位。并没有要求说不能定位到文件尾端,以及必须用SEEK_SET
等诸多限制。
#include<stdio.h> void rewind(FILE *fp);
-
参数:
fp
:打开的文件对象指针
rewind
函数将一个流设置到文件的起始位置
-
-
通过
ftello/fseeko
函数:除了偏移量类型为off_t
而不是long
以外,ftello/fseeko
与ftell/fseek
相同#include<stdio.h> off_t ftello(FILE *fp);
- 参数:
fp
:打开的文件对象指针 - 返回值:
- 成功:返回当前文件位置指示
- 失败:返回 (off_t)-1
#include<stdio.h> int fseeko(FILE *fp,off_t offset,int whence);
- 参数:
fp
:打开的文件对象指针offset
:偏移量。其解释依赖于whence
whence
:偏移量的解释方式:SEEK_SET
常量:表示从文件的起始位置开始SEEK_CUR
常量:表示从文件的当前位置开始SEEK_END
常量:表示从文件的尾端开始
- 返回值:
- 成功:返回 0
- 失败:返回 -1
- 参数:
-
fgetpos/fsetpos
函数:由 ISO C 引入#include<stdio.h> int fgetpos(FILE *restrict fp,fpos_t *restrict pos); int fsetpos(FILE * fp,const fpos_t * pos);
- 参数:
fp
:打开的文件对象指针pos
:存放偏移量的缓冲区
- 返回值:
- 成功: 返回 0
- 失败: 返回非 0
- 参数:
5.6 格式化I/O
1、执行格式化输出处理函数。
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *fp, const char *format, ...);
int dprintf(int fd, const char* format, ...);
int sprintf(char *buf,const char *format, ...);
int snprintf(char *buf,size_t n,const char format, ...);
-
参数:
format,...
:输出的格式化字符串
对于
fprintf
:fp
:打开的文件对象指针。格式化输出到该文件中
对于
dprintf
:fd
:打开文件的文件描述符。格式化输出到该文件中
对于
sprintf
:buf
:一个缓冲区的指针。格式化输出到该缓冲区中
对于
snprintf
:buf
:一个缓冲区的指针。格式化输出到该缓冲区中n
:缓冲区的长度。格式化输出到该缓冲区中
-
返回值:
- 成功:返回输出字符数(不包含
null
字节) - 失败:返回负数
- 成功:返回输出字符数(不包含
printf
将格式化输出写到标准输出;fprintf
写到指定的流;dprintf
写到指定的文件描述符;sprintf
写到数组buf
中;snprintf
也是写到数组buf
中,但是在该数组的尾端自动添加一个null
字节(该字节不包含在返回值中)。
- 通常不推荐使用
sprintf
,因为它可能引起缓冲区溢出流动 - 如果格式化输出一共 s 个字节,那么
snprintf
的数组缓冲区至少为s+1
个字节,否则发生截断
2、格式化输入
#include <stdio.h>
int scanf(const char* format, ...);
int fsacnf(FILE *fp,const char* format, ...);
int sscanf(const char* buf,const char* format, ...);
它们的作用如下:
-
scanf
函数:scanf
函数是标准库函数,**用于从标准输入(通常是键盘)**读取数据并根据格式字符串进行解析。- 它的第一个参数是格式字符串,后面可以有零个或多个附加参数,用于接收解析后的数据。
scanf
返回成功读取的数据项数,如果发生错误,则返回EOF。
-
fscanf
函数:fscanf
函数也是标准库函数,用于从指定的文件流(FILE
对象,通常是打开的文件)读取数据并根据格式字符串进行解析。- 第一个参数是文件流指针,第二个参数是格式字符串,后面可以有零个或多个附加参数,用于接收解析后的数据。
fscanf
返回成功读取的数据项数,如果发生错误,则返回EOF。
-
sscanf
函数:sscanf
函数也是标准库函数,用于从指定的字符串中读取数据并根据格式字符串进行解析。- 第一个参数是包含数据的字符串,第二个参数是格式字符串,后面可以有零个或多个附加参数,用于接收解析后的数据。
sscanf
返回成功读取的数据项数,如果发生错误,则返回EOF。
5.7 临时文件
1、两个创建临时文件的函数:
#include <stdio.h>
char* tmpnam(char* ptr);//返回:指向一唯一路径名的指针
FILE* tmpfile(void);//返回:若成功则为文件指针,若出错则为NULL
- tmpnam产生一个与现在文件名不同的一个有效路径名字符串。每次调用都将产生一个不同的路径名,最多调用TMP_MAX次。
C标准只要求其值至少应为25。但是XPG3却要求其值至少为10000。
-
若ptr是NULL,则所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回。下一次在调用tmpnam时,会重写该静态区。
- 这意味着,如果我们调用此函数多次,而且想保存路径名,则我们应当保存该路径名的副本,而不是指针的副本。
-
如若ptr不是NULL,则认为它指向长度至少是L_tmpnam个字符的数组。(常数L_tmpnam定义在头文件<stdio.h>中。)所产生的路径名存放在该数组中,ptr也作为函数值返回。
-
tmpfile创建一个临时二进制文件(类型wb+),在关闭该文件或程序结束时自动删除这种文件。
2、tempnam是tmpnam的一个变体,它允许调用者为所产生的路径名指定目录和前缀。
#include <stdio.h>
char *tempnam(const char* directory, const char* prefix);
//返回:指向一唯一路径名的指针
- 如果prefix非NULL,则它应该是最多包含5个字符的字符串,用其作为文件名的头几个字符。
- 该函数调用malloc函数分配动态存储区,用其存放所构造的路径名。当不再使用此路径名时就可释放此存储区。
案例:显示tempnam的引用
#include "ourhdr.h"
int main(int argc, char* argv[]){
if(argc != 3)
err_quit("usage: a.out <directory> <prefix>");
printf("%s\n",tempnam(argv[1][0] != ' ' ? argv[1] : NULL,argv[2][0] != ' ' ? argv[2] : NULL));
exit(0);
}
注意,如果命令行参数(目录或前缀)中的任一一个以空白开始,则将其作为null指针传送给该函数。
本章小结
大多数U N I X应用程序都使用标准I/O库。本章说明了该库提供的所有函数,某些实现细节和效率方面的考虑。应该看到标准I/O库使用了缓存机制,而这种机制是产生很多问题,引起很多混淆的一个领域。
习题
5.1 用setvbuf完成setbuf
void setbuf(FILE *stream, char *buf);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
setbuf
函数允许你为一个文件流提供自定义缓冲区,而 setvbuf
函数允许你更精细地控制缓冲方式。通常,setbuf
等同于使用 setvbuf
设置全缓冲(_IOFBF)模式,并提供一个自定义的缓冲区。
void setbuf(FIEL *fp, char *_buf){
int mode;
if(_buf == NULL){
node = _IONBF;//无缓冲
printf("No buf\n");
} else {
mode = _IOFBF;//全缓冲
printf("Line buf\n");
}
if(setvbuf(fp,_buf,mode,BUFSIZ) != 0){
err_sys("setvbuf error");
exit(-1);
}
return;
}
5.2 在5.8节中程序利用fgets和fputs函数拷贝文件,每次I/O操作只拷贝一行。若将程序中的MAXLINE改为4,当拷贝的行超过该最大值时会出现什么情况?
#include "ourhdr.h"
int main(void){
char buf[MAXLINE];
while(fgets(buf,MAXLINE,stdin) != NULL)
if(fputs(buf,stdout) == EOF)
err_sys("output error");
if(ferror(stdin))
err_sys("input error");
exit(0);
}
-
当将
MAXLINE
的值更改为 4,而拷贝的行的长度超过该最大值时,会出现截断或分割行的情况。 -
fgets
函数会尽量读取指定最大长度的字符,但如果行的长度超过MAXLINE - 1
,则只会读取部分行,而其余部分将被截断。这可能导致文本行在拷贝后不完整或损坏。 -
例如,如果你有一个输入文件包含一行长度超过 4 个字符的文本行,比如 “Hello, World!”,当
fgets
尝试读取时,只会读取前 3 个字符(“Hel”),然后将其写入输出文件。“lo, World!” 的其余部分会被截断,所以输出文件中只包含 “Hel”。 -
这会导致丢失部分信息,特别是对于长文本行来说。为避免这种情况,你可以增加
MAXLINE
的值,以确保足够的缓冲区大小来容纳最长的文本行,或者使用适应性缓冲策略,根据输入行的实际长度来动态分配足够大的缓冲区。
5.3 printf返回0值表示什么?
printf
函数返回值为表示成功格式化并输出的字符数量。- 如果
printf
成功执行并输出了字符,则返回的值是输出的字符数量。 - 如果
printf
未成功执行,返回值为负数,通常是-1
。 - 如果
printf
格式化字符串为空,返回值为0
。 - 所以,如果
printf
返回0
,这通常表示它成功执行,但没有输出任何字符。 - 这可能发生在你使用空格式化字符串(例如
printf("")
)或者在格式化字符串中的内容由于格式化说明符的原因被忽略。 - 这不代表
printf
失败,只是没有输出任何字符。
5.4 下面的代码在一些机器上运行正确,而在另外一些机器运行时出错,解释问题所在。
#include <stdio.h>
int main(void){
char c;
while((c = getchar()) != EOF)
putchar(c);
}
通常情况下,这段代码应该在大多数机器上运行正常,但可能会出现一些问题,具体取决于机器和操作系统的特性。
-
行尾符差异:不同的操作系统使用不同的行尾符(例如,Windows 使用 “\r\n”,Unix 使用 “\n”)。如果你在一个系统上创建的文本文件,然后尝试在另一个系统上运行这个程序,它可能会因行尾符的不同而导致不同的行为。在某些情况下,它可能会在输出中添加额外的字符或出现不正确的行尾。
-
字符编码问题:一些操作系统和编译器使用不同的字符编码,可能会导致字符的解释方式不同。这可能会导致一些字符无法正确显示或输出。
-
标准输入输出差异:某些系统可能对标准输入和标准输出的处理方式有所不同。特别是在一些非标准终端环境中,可能会导致不一致的行为。
要确保这段代码在不同系统上都能正常运行,你可以采取以下措施:
- 使用二进制模式打开文件,以确保不会受到文本模式的影响,这有助于避免行尾符差异的问题。
- 确保字符编码一致,或者进行必要的字符编码转换。
- 确保标准输入和标准输出的一致性,或者采取适当的措施来处理不同的标准输入输出情况。
此外,应该考虑使用标准化的I/O函数,如fgets
和fputs
,来处理文本数据,以提高可移植性并减少不一致性问题。
5.5 为什么tempnam限制前缀为5个字符?
在大多数系统上,tempnam
函数要求前缀(prefix)不超过 5 个字符的主要原因是为了确保生成的临时文件名的可读性和可管理性,同时避免潜在的命名冲突。
临时文件通常用于临时存储或传输数据,例如在程序之间共享数据,或者在程序的不同运行中存储中间结果。这些文件的目的是临时的,它们通常不需要有描述性的名称,因为它们不是为了用户查看或识别而创建的。因此,较短的前缀足够用于标识临时文件。
此外,使用较短的前缀还有助于减少潜在的文件名冲突。如果每个临时文件都有非常长的前缀,那么在相同目录中生成大量的临时文件时,可能会增加文件名冲突的概率。较短的前缀可以减少这种潜在问题。
虽然 tempnam
的前缀长度限制为 5 个字符是常见的,但这并不是标准的,不同系统和库可能有不同的前缀长度限制。在使用 tempnam
函数时,最好查阅相关文档以确定特定系统或库的限制。如果需要更长的前缀或其他自定义文件名生成规则,你可以编写自己的临时文件名生成函数来满足需求。
5.6 对标准I/O流如何使用fsync函数?
fsync
函数用于将数据和元数据(包括文件的修改时间等信息)刷写到磁盘,以确保数据的持久化存储。- 在标准I/O库中,可以使用
fflush
函数来刷新标准I/O流,但fflush
通常只确保数据被刷写到标准I/O库的缓冲区,而不一定刷写到磁盘。 - 如果需要确保数据被刷写到磁盘,可以结合使用
fflush
和fsync
函数。
以下是如何使用 fsync
函数刷写标准I/O流的示例:
#include <stdio.h>
#include <unistd.h>
int main(void) {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
perror("fopen");
return 1;
}
// 写入数据到标准I/O流
fprintf(file, "Hello, World!\n");
// 使用 fflush 刷新标准I/O流缓冲
fflush(file);
// 使用 fsync 刷写数据到磁盘,用fileno函数获取与标准I/O流相关的文件描述符。
int fileno_result = fileno(file);
if (fileno_result != -1) {
//使用fsync函数刷写数据到磁盘。
if (fsync(fileno_result) != 0) {
perror("fsync");
}
} else {
perror("fileno");
}
// 关闭文件
fclose(file);
return 0;
}
-
在这个示例中,我们首先使用
fopen
打开一个文件,然后使用fprintf
向标准I/O流写入数据。接着,我们使用fflush
刷新标准I/O流的缓冲区,确保数据被刷写到标准I/O库的缓冲区。 -
然后,我们获取文件描述符(使用
fileno
函数),并使用fsync
函数刷写数据到磁盘。最后,我们关闭文件。 -
注意,
fsync
的使用可能会导致性能下降,因为它强制执行磁盘写入,这可能比较慢。通常,只有在需要确保数据持久化存储时才使用fsync
。
5.7 在程序1-5和1-8中打印的提示信息没有包含换行符,程序也没有调用fflush函数,请解释提示信息是如何输出的?
在上述两段程序中,虽然打印的提示信息没有包含换行符,但程序依然能够正常输出提示信息。这是因为标准I/O库(例如 printf
)通常采用行缓冲的方式来处理输出。
标准I/O库的输出通常会在以下情况之一发生时刷新缓冲区:
- 当缓冲区已满时。
- 当
fflush
函数被调用时。 - 当程序正常终止时,缓冲区会自动刷新。
-
在你的程序中,
printf("%% ");
语句打印提示信息,但由于没有遇到上述刷新条件,所以输出被暂时保留在标准I/O库的缓冲区中。在这种情况下,由于缓冲区不是立即刷新,所以提示信息不会立即显示在屏幕上,而是等待适当的时机。 -
用户输入命令后,当使用
fgets
从标准输入读取一行输入时,通常会包含一个换行符(\n
),这个换行符来自用户按下回车键。此时,输入行被放入标准I/O库的缓冲区。 -
此后,在程序的主循环中,当
printf("%% ");
用于打印下一个提示信息时,由于之前的输入操作已经刷新了标准I/O库的缓冲区,所以新的提示信息将被显示在屏幕上,而且不会与上一行输入混合在一起。 -
在第一次输入之前,初始的提示信息也会被显示,因为程序正常终止时,标准I/O库通常会刷新输出缓冲区。
n打开一个文件,然后使用
fprintf向标准I/O流写入数据。接着,我们使用
fflush` 刷新标准I/O流的缓冲区,确保数据被刷写到标准I/O库的缓冲区。
-
然后,我们获取文件描述符(使用
fileno
函数),并使用fsync
函数刷写数据到磁盘。最后,我们关闭文件。 -
注意,
fsync
的使用可能会导致性能下降,因为它强制执行磁盘写入,这可能比较慢。通常,只有在需要确保数据持久化存储时才使用fsync
。
5.7 在程序1-5和1-8中打印的提示信息没有包含换行符,程序也没有调用fflush函数,请解释提示信息是如何输出的?
在上述两段程序中,虽然打印的提示信息没有包含换行符,但程序依然能够正常输出提示信息。这是因为标准I/O库(例如 printf
)通常采用行缓冲的方式来处理输出。
标准I/O库的输出通常会在以下情况之一发生时刷新缓冲区:
- 当缓冲区已满时。
- 当
fflush
函数被调用时。 - 当程序正常终止时,缓冲区会自动刷新。
-
在你的程序中,
printf("%% ");
语句打印提示信息,但由于没有遇到上述刷新条件,所以输出被暂时保留在标准I/O库的缓冲区中。在这种情况下,由于缓冲区不是立即刷新,所以提示信息不会立即显示在屏幕上,而是等待适当的时机。 -
用户输入命令后,当使用
fgets
从标准输入读取一行输入时,通常会包含一个换行符(\n
),这个换行符来自用户按下回车键。此时,输入行被放入标准I/O库的缓冲区。 -
此后,在程序的主循环中,当
printf("%% ");
用于打印下一个提示信息时,由于之前的输入操作已经刷新了标准I/O库的缓冲区,所以新的提示信息将被显示在屏幕上,而且不会与上一行输入混合在一起。 -
在第一次输入之前,初始的提示信息也会被显示,因为程序正常终止时,标准I/O库通常会刷新输出缓冲区。
-
虽然提示信息没有换行符,但标准I/O库的行缓冲机制确保了输出的适当显示。如果你希望立即显示提示信息,可以使用
fflush(stdout)
来强制刷新标准输出缓冲区。