标准IO库处理了缓冲区分配、以优化的块长度执行IO等细节,使得用户不必担心如何选择使用正确的块长度。
对于write、read等函数,是围绕文件描述符的,打开文件的描述符被用于后续的IO操作,而对于标准IO库,它们的操作是围绕流的,当标准IO库打开或创建一个文件时,我们使一个流与一个文件相关联。
ASCII字符集一个字符用一个字节表示,对于国际字符集,一个字符用多个字节表示。标准IO文件流可用于单字节或多字节(宽)字符集。流的定向决定了所读、写的字符是单还是多字节的。一个流最初被创建时,它并没有被定向,如果在其上使用一个多字节IO函数(头文件wchar.h中的),则将该流的定向设置为宽定向的;如果在未定向的流上使用一个单字节IO函数,则将流的定向设为字节定向的。
改变流的定向的两个函数:freopen,清除一个流的定向;fwide,设置流的定向。
mode参数的值:
1.负数,试图将fp指定为字节定向的。
2.正数,试图将fp指定为宽定向的。
3.0,返回标识该流定向的值。
函数fwide不改变已定向流的定向,且无出错返回,如果流是无效的,我们只能通过在调用fwide前先清除errno,然后fwide函数返回时检查errno来发现错误。
打开一个流时,标准IO函数fopen返回一个指向FILE对象的指针,该对象是一个结构,包含了标准IO库为管理该流所需的所有信息,包括用于实际IO的文件描述符、指向用于该流缓冲区的指针、缓冲区长度、当前在缓冲区中的字符数和出错标志等。
本章中称指向FILE对象的指针为文件指针。为引用一个流,需将FILE指针作为参数传递给标准IO函数。
一个进程预定义三个流(标准输入stdin、标准输出stdout、标准错误stderr),这三个文件指针定义在头文件stdio.h中。
标准IO库提供缓冲的目的是尽可能减少read和write调用的次数,也对每个IO流自动进行缓冲管理。
标准IO提供三种缓冲:
1.全缓冲:在填满标准IO缓冲区后才进行实际IO操作。磁盘文件通常是由IO库实施全缓冲。在一个流上第一次执行IO操作时,会调用malloc获得要使用的缓冲区。术语冲洗说明标准IO缓冲区的写操作,缓冲区可由标准IO例程自动冲洗(如,当填满缓冲区时),或调用fflush冲洗一个流。
2.行缓冲:在输入和输出中遇到换行符时,标准IO库执行IO操作。这允许我们一次输出一个字符(fputc函数),但只有在写了一行后才进行实际IO。流涉及一个终端(如标准输入、标准输出)时,通常使用行缓冲。但标准IO库用来收集每行的缓冲区长度是固定的,只要填满了缓冲区,即使还没写换行符,也进行IO操作。只要通过标准IO库的函数从(1)一个不带缓冲的流,或(2)一个行缓冲的流(当请求的数据不存在于缓冲区,即还没读到缓冲区时,它需要从内核请求需要的数据)得到输入数据,就会冲洗所有行缓冲区。
3.不带缓冲:不对字符进行缓冲存储,如fputs函数写字符到不带缓冲的流中,这些字符我们期望能立即输出。
标准错误流stderr通常是不带缓冲的,可以使出错信息尽快显示出来。
ISO C要求下列缓冲特征:
1.当且仅当标准输入和标准输出不指向交互式设备时,它们才是全缓冲的。
2.标准错误不是全缓冲的。
但以上特征并没有说明当标准输入和标准输出指向交互式设备时是行缓冲的还是不带缓冲的,以及标准错误是不带缓冲的还是行缓冲的。很多系统实现如下:
1.标准错误不带缓冲。
2.指向终端设备的流,是行缓冲的,否则是全缓冲的。
更改缓冲类型:
这些函数要在流已被打开后调用,也应在对流执行任何操作前调用。
setbuf函数可打开或关闭缓冲机制,为带缓冲进行IO,参数buf需要指向一个长度为BUFSIZ(定义在头文件stdio.h)的缓冲区,通常在此之后,该流是全缓冲的,如果该流与一个终端设备相关,某些系统也将其设为行缓冲的,为关闭缓冲,将buf参数设为NULL。
setvbuf函数可精确地说明所需的缓冲类型,这由mode参数实现:
1._IOFBF:全缓冲。
2._IOLBF:行缓冲。
3._IONBF:不带缓冲。
如果指定一个不带缓冲的流,则忽略buf和size参数。如指定全缓冲或行缓冲,则buf和size指定一个缓冲区及其长度。如果该流是带缓冲的,而buf参数为NULL,则标准IO库自动为该流分配适当长度(指BUFSIZ的长度)的缓冲区。
有些C库使用stat结构中的成员st.blksize指定的值作为最佳IO缓冲区长度。
如果在一个函数内分配一个自动变量类的标准IO缓冲区,则从该函数返回前,必须关闭流。有些实现将缓冲区的一部分用来存放管理操作信息,所以可以存放在缓冲区中的实际数据字节数少于size。应由系统选择缓冲区长度,并自动分配缓冲区,此时关闭流时,标准IO库会自动释放缓冲区。
我们可强制冲洗一个输出流:
它使该流所有未写数据都被传送至内核。当参数fp为NULL时,将导致所有输出流被冲洗。如果是参数fp是输入流,则POSIX.1-2001及之前的标准中,此行为是未定义的。在POSIX.1-2008标准中,规定如果输入流fp关联的是seekable的(如磁盘文件,但管道和终端不是),则fflush函数丢弃所有从底层文件中取到的,但应用还没有读取的数据。可通过man查看当前系统fflush函数的标准:
scanf函数可获取一行的输入(不含最后的回车),但输入缓存中的内容不会变:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char buf[BUFSIZ]; // BUFSIZ是stdio.h中的常量,作为用户提供的缓冲区长度
setbuf(stdin, buf);
char str[20];
char str1[20];
int i;
scanf("%[^\n]s", str); // 不会读第一行最后的\n
printf("str: %s\n", str);
printf("after scanf, buf is: %s\n", buf);
fgets(str1, 20, stdin);
int j = 0;
while (str1[j]) {
if (str1[j] == '\n') {
printf("str1 j: \\n\n", str1[j]);
} else {
printf("str1 j: %c\n", str1[j]);
}
++j;
}
return 0;
}
运行以上代码:
可见输入流缓冲中还有内容。
在POSIX.1-2008标准前,有些编译器对于参数fp是输入流时有自己实现的处理,如某些编译器会清空输入流中还未读取的内容,不管参数fp引用的流是磁盘文件还是终端,此时可解决以下代码中的问题:
#include <stdio.h>
#include <stdlib.h>
int main() {
char str[20];
int i;
for (i = 0; i < 2; i++) {
scanf("%[^\n]s", str);
printf("%s\n", str);
// fflush(stdin);
}
return 0;
}
如果没有fflush调用,则如输入以下内容:
aaa
bbb
输出会是:
aaa
aaa
原因是输入流没有丢弃,scanf函数第二次在输入流中又匹配到的第一次的输出,而在该编译器的实现中,fflush调用会清空终端的输入流,从而使输出为:
aaa
bbb
但不推荐以上清除标准输入流的方法,它依赖于编译器的实现,对于语言来说是未定义行为。
C中需要冲洗输出缓存的另一个例子:
#include <stdio.h>
int main()
{
char str[80], ch;
// Scan input from user -
// abcd for example
scanf("%s", str);
// Scan character from user-
// 'p' for example
ch = getchar();
// Printing character array,
// prints “abcd”)
printf("%s\n", str);
// This does not print
// character 'p'
printf("%c", ch);
return 0;
}
运行以上程序,输入:
abcd
p
输出:
abcd
C++中需要冲洗输出缓存的例子:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int a;
char ch[80];
// Enter input from user
// - 4 for example
cin >> a;
// Get input from user -
// "abcd" for example
cin.getline(ch,80);
// Prints 4
cout << a << endl;
// Printing string : This does
// not print string
cout << ch << endl;
return 0;
}
运行以上程序,输入:
4
abcd
输出:
4
对于C,遵循标准的避免以上问题的方法如下:
#include <stdio.h>
int main()
{
char str[80], ch;
// scan input from user -
// abcd for example
scanf("%s", str);
// flushes the standard input
// (clears the input buffer)
while ((getchar()) != '\n');
// scan character from user -
// 'p' for example
ch = getchar();
// Printing character array,
// prints “abcd”)
printf("%s\n", str);
// Printing character a: It
// will print 'p' this time
printf("%c", ch);
return 0;
}
对于C++,遵循标准的避免以上问题的方法一:
#include <iostream>
// for streamsize
#include <ios>
// for numeric_limits
#include <limits>
using namespace std;
int main()
{
int a;
char str[80];
// Enter input from user
// - 4 for example
cin >> a;
// discards the input buffer
// cin.ignore从输入流cin中提取字符,提取的字符被丢弃
// 当丢弃了numeric_limits<streamsize>::max()个字符,或丢弃了\n时返回
cin.ignore(numeric_limits<streamsize>::max(),'\n');
// Get input from user -
// abcd for example
cin.getline(str, 80);
// Prints 4
cout << a << endl;
// Printing string : This
// will print string now
cout << str << endl;
return 0;
}
对于C++,避免以上问题的方法二,即cin.sync方法,但它不属于标准的一部分,其行为是实现定义的,不能保证一定可以冲洗输入流:
#include<iostream>
#include<ios>
#include<limits>
using namespace std;
int main()
{
int a;
char str[80];
// Enter input from user
// - 4 for example
cin >> a;
// Discards the input buffer
cin.sync();
// Get input from user -
// abcd for example
cin.getline(str, 80);
// Prints 4
cout << a << endl;
// Printing string - this
// will print string now
cout << str << endl;
return 0;
}
对于C++,遵循标准的避免以上问题的方法三:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int a;
string s;
// Enter input from user -
// 4 for example
cin >> a;
// Discards the input buffer and
// initial white spaces of string
cin >> ws;
// Get input from user -
// abcd for example
getline(cin, s);
// Prints 4 and abcd:
// will execute print a and s
cout << a << endl;
cout << s << endl;
return 0;
}
打开标准IO流:
区别:
1.fopen函数打开路径名为pathname参数的指定文件。
2.freopen函数在一个指定的流上打开一个指定文件。若该流已打开,则会先关闭该流;若该流已定向,则会清除该定向;一般用于将一个指定文件打开为一个预定义的流(如标准输入、标准输出、标准错误)。
3.fdopen函数使一个标准IO流与参数fd指定的描述符相结合。常用于创建管道或网络通信通道函数返回的描述符,这些特殊类型文件不能调用fopen打开。
type参数指定对IO流的读写方式:
字符b作为参数type的一部分,使得标准IO系统可区分文本文件和二进制文件,因为UNIX内核不对这两种文件区分,在UNIX环境下指定b无作用。
对于fdopen函数,type参数w、wb、w+、w+b、wb+不表示截断该文件,因为文件已经存在,不会截断为写而打开文件。且不会创建文件,因为文件已经存在。
type参数中的e字符代表开启O_CLOEXEC标志。
以读和写类型打开文件时(type参数中+号),有限制:
1.如果中间没有fflush、fseek、fsetpos、rewind函数,输出后面不能跟随输入。
2.如果中间没有fseek、fsetpos、rewind函数,或一个输入操作没有到达文件尾端,则输入后不能跟输出。
指定w或a类型创建一个新文件时,无法说明该文件的访问权限位。但可以通过umask函数调整权限。
调用fclose关闭一个打开的流:
该文件被关闭前,冲洗缓冲中的输出数据,缓冲区中任何输入数据被丢弃,如果标准IO库已经为该流自动分配了缓冲区,则释放此缓冲区。
进程正常终止(调用exit或从main函数返回)时,所有未写缓冲数据都被冲洗,所有打开的标准IO流都被关闭。
可对打开流进行三种非格式化IO:
1.每次一个字符的IO。
2.每次一行IO。每行以一个换行符终止。
3.直接IO。
一次读一个字符的函数:
函数getchar等同于getc(stdin)。前两个函数的区别为,getc函数可实现为宏,fgetc函数不能实现为宏,这意味着:
1.getc函数的参数不应当是具有副作用的表达式,它可能会被计算多次(例如#define MAX(a, b) ((a) > (b) ? (a) : (b))
,当这样调用此宏时int z = MAX(x++, y++);
,宏会展开为int z = ((x++) > (y++) ? (x++) : (y++));
,从而导致x或y被自增两次)。
2.fgetc函数一定是个函数,所以可以得到其地址,这允许将fgetc函数的地址作为一个参数传递给另一个函数。
3.调用fgetc所需时间一般比调用getc要长,因为函数调用所需的时间通常比调用宏长。
一次读一个字符的函数返回值时,会将unsigned char转换为int类型。以unsigned char读时,所有字符都不会是负数;返回类型是int,可用负值表示读到了文件尾或执行出错(头文件stdio.h中的常量EOF被要求是一个负值,经常是-1)。因此这三个函数的返回值不能存放在一个字符变量中,因为可能会把返回值与EOF进行比较。
以上三个函数不管是读到文件尾还是执行出错,都返回同样值,可用以下函数区分这两种情况:
大多实现为流在FILE对象中维护了两个标志:
1.出错标志。
2.文件结束标志。
调用clearerr可清除这两个标志。
从流中读取数据后,可调用ungetc将字符再压送回流中:
压送回流中的字符以后又可从流中读出,但读出的顺序与压送回的顺序相反。虽然ISO C允许实现支持任何次数的回送,但要求实现一次只回送一个字符。
回送的字符不一定是上次读到的字符,不能回送EOF,但当已经到达文件尾时,仍可回送一个字符,下次读时将返回这个字符,再读则返回EOF,能这样做的原因是,一次成功的ungetc调用会清除该流的文件结束标志。
有时需要先看一下下个字符,以决定如何处理当前字符,如果标准IO库不提供回送能力,就需将该字符存放到一个自己的变量中,并设置一个标志判别下次需要一个字符时需要调用getc还是从自己的变量中取这个字符。
用ungetc函数压回字符时,没有将它写到底层文件或设备上,只是将它写回标准IO库的流缓冲中。
每次输出一个字符:
与输入函数类似,putchar(c)
等同于put(c, stdout)
,putc函数可被实现为宏,而fputc函数不能实现为宏。
每次输入一行:
参数buf指定了缓冲区的地址,读入的行将被送入其中。函数gets从标准输入读,函数fgets从指定的流读。
fgets函数需要指定缓冲长度n,此函数一直读到下一个换行符为止(换行符也被读入),但整行包括换行符长度加起来不超过n-1个字符,读入的字符被送入缓冲区,该缓冲区以null字节结尾。如果该行包括最后一个换行符的字符数超过n-1,fgets函数只返回一个不完整的行,缓冲区总是以null字节结尾,对fgets的下一次调用会继续读该行。
不推荐使用gets函数,它不能指定缓冲区长度,这样可能造成缓冲区溢出,从而写到缓冲区后的存储空间中。这种缺陷曾被利用,造成1988年的因特网蠕虫事件。函数gets不将换行符存入缓冲区。
在SUSv4中,gets函数已被标记为弃用的接口,且在ISO C标准的最新版本中已被忽略。
每次输出一行:
函数fputs将一个以null字节终止的字符串写道指定的流,尾端的null不会被写出,它不一定每次输出一行,因为参数str不一定以换行符作为最后一个非null字节。
函数puts将一个以null字节终止的字符串写到标准输出,null不会被写出,之后,它将一个换行符写到标准输出。
函数puts不像它对应的gets一样不安全,但还是应避免使用它,以免需要记住输出的串最后是否有换行符。
用getc和putc函数将标准输入复制到标准输出:
#include <stdio.h>
#include <stdlib.h>
int main() {
int c;
while ((c = getc(stdin)) != EOF) {
if (putc(c, stdout) == EOF) {
printf("putc error\n");
exit(1);
}
}
if (ferror(stdin)) {
printf("getc error\n");
exit(1);
}
exit(0);
}
运行它:
用fgets和fputs函数将标准输入复制到标准输出:
#include <stdio.h>
#include <stdlib.h>
#define MAXLINE 1024
int main() {
char buf[MAXLINE];
while (fgets(buf, MAXLINE, stdin) != NULL) {
if (fputs(buf, stdout) == EOF) {
printf("fputs error\n");
exit(1);
}
}
if (ferror(stdin)) {
printf("fgets error\n");
exit(1);
}
exit(0);
}
运行它:
以上两个程序中没有显式关闭标准IO流,因为exit函数将冲洗任何未写的数据,然后关闭所有打开的流。
以上使用一次一行IO的程序中,如果MAXLINE很小,不能容纳一行,那么会调用多次一次一行IO函数。
以下是对同一文件(98.5M,300万行)进行读写所用时间,其中图3-6中的最佳时间是直接以缓冲区4096调用read时的时间:
三个标准IO版本的每一个的用户CPU时间都大于最佳read版本,因为每次读一个字符的标准IO版本要执行1亿此循环,而每次读一行的版本要执行3144984次循环,但最佳的read使用了4096字节的缓冲区,因此循环只需25224次。而系统CPU时间几乎没有差别,原因在于所有这些程序对内核提出的读、写请求数基本相同。使用标准IO例程的最大优点是不需考虑缓冲及最佳IO长度的选择,虽然使用fgets时需要考虑最大行长,但与选择最佳IO长度比还是方便得多。
上图中最后一列是每个main函数的文本空间字节数(由C编译器产生的机器指令),可见getc、putc函数与fgetc、fputc函数的文本空间长度大体相同,通常getc和putc实现为宏,但在GNU C库中,实现为函数。
上图中使用每次一行IO的版本的速度比每次一个字符版本的速度快很多,如果fgets和fputs函数是用getc和putc实现的,那么每次一行的版本会更慢,因为除了每行的循环外,函数内部还会循环输出行中每个字符。而本次测试中的每次一行函数是用memccpy实现的,通常,为提高效率,memccpy函数用汇编语言而非C语言编写。
上图中,fgetc函数比每次调用read读一个字符的版本要快,因为read函数每次执行都要执行一次系统调用,而fgetc函数版本由于有流缓冲区,因此系统调用次数远小于read函数。系统调用比普通函数调用要花费更多的时间。
上图时间结果只在某些系统上有效,不同UNIX系统实现不同。
从上例了解到,标准IO库与直接调用read和write函数相比并不慢很多,最主要的用户CPU时间是由应用本身的各种处理消耗的。
如果进行二进制IO操作,我们更愿意一次读或写一个完整结构,而以上IO例程只一次读写一个字符或一次读写一行,且如果读写字节中null字节时就会停止。以下函数执行二进制IO操作:
以上两函数的用法:
1.读写一个二进制数组,如将数组中的第2~5个元素写到一个文件上:
float data[10];
if (fwrite(&data[2], sizeof(float), 4, fp) != 4) {
printf("fwrite error\n");
}
2.读写一个结构:
struct {
short count;
long total;
char name[NAMESIZE];
} item;
if (fwrite(&item, sizeof(item), 1, fp) != 1) {
printf("fwrite error\n");
}
fread和fwrite函数返回读或写的对象数,对于读,如果出错或读到文件尾,此数字可以少于参数nobj,此时应调用ferror或feof判断是哪种情况;对于写,如果返回值少于参数nobj的要求,则出错。
二进制IO的问题是它只能用于读在同一系统上的数据,多年前,所有UNIX系统都运行在PDP-11上,这并无问题,但现在很多异构系统通过网络相互连接,此时在一个系统上写的数据,在另一个系统上读取会导致二进制IO函数失效,失效的原因是:
1.同一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同(由于不同的对齐要求)。某些编译程序有一个选项,选择它的不同值,可以使结构中的各成员紧密排列(这可以节省空间,但运行性能可能下降),或使得各成员准确对齐(这可以在运行时易于存取结构中的各成员),这意味着即使在同一系统上,一个结构的二进制存放方式也可能因编译程序选项的不同而不同。
2.存储多字节整数和浮点值的二进制格式在不同系统结构间也可能不同。
定位标准IO流的函数:
1.ftell和fseek函数:它们假定偏移可以存放在一个长整型中。
2.ftello和fseeko函数:SUS引入了这两个函数,使文件的偏移量不必一定是长整型,它们使用off_t数据类型代替长整型。
3.fgetpos和fsetpos函数:由ISO C引入,使用fpos_t类型记录偏移量,这种数据类型可根据需要定义为一个足够大的数。
需要移植到非UNIX系统上的应用应使用fgetpos和fsetpos函数:
对于二进制文件,偏移量从文件起始位置开始度量,并以字节为单位。
fseek函数的whence参数是偏移的相对位置,可取的值:
1.SEEK_SET:从文件起始位置开始。
2.SEEK_CUR:从当前偏移量开始。
3.SEEK_END:从文件尾端开始。ISO C不要求实现对二进制文件支持SEEK_END,因为某些系统要求二进制文件的长度是某个幻数的整数倍,不足的部分结尾补0,但UNIX中,对于二进制文件是支持SEEK_END的。
对于文本文件,偏移值不能以字节偏移量来度量,主要是因为在非UNIX系统中,可能以不同的格式存放文本文件。设置文本文件偏移量时,whence参数只能是SEET_SET,且参数offset只能选0(偏移量设为文件起始位置)或对该文件调用ftell的返回值。
rewind函数也可将一个流的偏移量设为起始位置。
除了偏移量的类型外,ftello函数和ftell函数完全相同,fseeko函数也与fseek函数完全相同:
实现可将off_t类型定义为长于32位。
fgetpos函数将文件偏移量存入参数pos指向的对象中,fsetpos函数将文件偏移量设为参数pos的值:
格式化输出的函数:
printf函数将格式化的数据写到标准输出;fprintf将其写入参数fp指定的流;dprintf函数将其写入参数fd指定的文件描述符对应的文件;sprintf函数将其写入参数buf指定的字符数组中,并在数组的末尾添加一个null字节,null字节不包含在返回值中。
sprintf函数可能造成buf指向的缓冲区的溢出,因此引入了snprintf函数,该函数中缓冲区长度是一个显式的参数n,超过缓冲区尾端的所有字符都被丢弃,如果缓冲区足够大,snprintf函数返回写入缓冲区的字符数(不包括null字节),如果snprintf函数返回小于缓冲区长度n的正值,那么没有截断输出。
格式化串参数format中,转换说明的结构如下:
转换说明以百分号开始,上图中的方括号中内容是可选部分。
flags字段可选值:
fldwidth字段说明最小字段宽度,转换后如果字符数小于宽度,则用空格填充,字段宽度值是一个非负十进制数,或是一个星号。
precision字段说明整型转换后最少输出数字位数或浮点数转换后小数部分的最小位数或字符串转换后的最大字节数。精度的格式是一个点.
,后跟一个可选的非负十进制数或一个星号。
宽度和精度字段都可为星号,此时可变参数列表中可用整型参数指定这两个字段的值,整型参数的位置位于被转换的参数前。
lenmodifier字段说明参数长度:
convtype字段可选值:
下面是5个printf族函数的变体,它们把可变参数表改成了一个参数arg:
执行格式化输入的函数:
scanf族函数用于分析输入字符串,并将字符序列转换成指定类型的变量,可变参数列表中是各个变量的地址,用于存放转换结果。
参数format中包含转换说明,转换说明的结构如下:
除了转换说明和空白字符外,格式字符串中其他字符必须与输入匹配。
可选的星号用于移植转换,它会按照转换说明进行转换,但结果不存放在参数中。
可选字段fldwidth说明最大宽度,可选字段lenmodifier与printf函数族的转换说明中的同名字段含义相同。
字段fldwidth和lenmodifier之间的可选字段m是赋值分配符,它可用于%c、%s、%[,使内存缓冲区分配空间以容纳转换字符串,此时,相关参数必须是指针的地址,分配的缓冲区地址会复制给该指针,这个分配的空间由调用者负责释放。
convtype字段类似printf函数族的同名字段,不同的是,输入中的带符号类型可赋予无符号类型:
scanf函数族也有将可变参数列表替换为arg参数的版本:
获取标准IO流相关联的文件描述符:
fileno函数是POSIX.1的扩展。
打印流的状态信息:
#include <stdlib.h>
#include <stdio.h>
void pr_stdio(const char *, FILE *);
int is_unbuffered(FILE *);
int is_linebuffered(FILE *);
int buffer_size(FILE *);
int main() {
FILE *fp;
fputs("enter any character\n", stdout);
if (getchar() == EOF) {
printf("getchar error\n");
exit(1);
}
fputs("one line to standard error\n", stderr);
pr_stdio("stdin", stdin);
pr_stdio("stdout", stdout);
pr_stdio("stderr", stderr);
if ((fp = fopen("/etc/passwd", "r")) == NULL) {
printf("fopen error\n");
exit(1);
}
if (getc(fp) == EOF) {
printf("getc error\n");
exit(1);
}
pr_stdio("/etc/passwd", fp);
exit(0);
}
void pr_stdio(const char *name, FILE *fp) {
printf("stream = %s, ", name);
if (is_unbuffered(fp)) {
printf("unbuffered");
} else if (is_linebuffered(fp)) {
printf("linebuffered");
} else {
printf("fully buffered");
}
printf(", buffer size = %d\n", buffer_size(fp));
}
/*
* The following is nonportable
*/
#if defined(_IO_UNBUFFERED)
int is_unbuffered(FILE *fp) {
return fp->_flags & _IO_UNBUFFERED;
}
int is_linebuffered(FILE *fp) {
return fp->_flags & _IO_LINE_BUF;
}
int buffer_size(FILE *fp) {
return fp->_IO_buf_end - fp->_IO_buf_base;
}
#elif defined(__SNBF)
int is_unbuffered(FILE *fp) {
return fp->_flags & __SNBF;
}
int is_linebuffered(FILE *fp) {
return fp->_flags & __SLBF;
}
int buffer_size(FILE *fp) {
reutnr fp->_bf.size;
}
#elif defined(_IONFB)
#ifdef _LP64
#define _flag __pad[4]
#define _ptr __pad[1]
#define _base __pad[2]
#endif
int is_unbuffered(FILE *fp) {
return fp->_flag & _IONBF;
}
int is_linebuffered(FILE *fp) {
return fp->_flag & _IOLBF;
}
int buffer_size(FILE *fp) {
#ifdef _LP64
return fp->_base - fp->_ptr;
#else
return BUFSIE; // just a guess
#endif
}
#else
#error unknown stdio implementation!
#endif
运行它:
上例在打印流缓冲信息之前,先对每个流执行IO操作,第一个IO操作通常会为该流分配缓冲区。
如果将标准输入流、标准输出流、标准错误流都定向到文件中,再运行以上程序:
可见该系统终端默认行缓冲,行缓冲长度为1024字节(如果要写2048字节,需要调用两次write系统调用);普通文件默认全缓冲,其缓冲区长度是该文件系统优先选用的IO长度(stat结构中的st_blksize值);标准错误永远是不带缓冲的。
ISO C标准IO库提供的有关创建临时文件的函数:
tmpnam函数产生一个与现有文件名不同的一个有效路径名字符串,每次调用它都会产生一个不同的路径名,最多调用TMP_MAX次,TMP_MAX定义在头文件stdio.h中。
ISO C标准要求TMP_MAX值至少为25,但SUS却要求符合XSI的系统的TMP_MAX值至少为10000。
tmpnam函数在SUSv4中被标记为弃用,但ISO C标准还继续支持它。
如果参数ptr是NULL,则产生的路径名存放在一个静态区中,指向该静态区的指针作为函数返回值返回,后续调用tmpnam时,会重写该缓冲区;如果参数ptr不是NULL,则认为它是指向长度至少是L_tmpnam个字符的数组(常量L_tmpnam定义在头文件stdio.h中),所产生的路径名存放在该数组中,参数ptr会作为函数返回值被返回。
tmpfile函数创建一个临时二进制文件并以wb+打开,在关闭该文件或程序结束时将自动删除这个文件。UNIX对二进制文件不进行特殊区分。
创建一个临时文件:
#include <stdio.h>
#include <stdlib.h>
#define MAXLINE 1024
int main() {
char name[L_tmpnam], line[MAXLINE];
FILE *fp = NULL;
printf("%s\n", tmpnam(NULL)); // first temp name
tmpnam(name);
printf("%s\n", name); // second temp name
if ((fp = tmpfile()) == NULL) { // create temp file
printf("temfile error\n");
exit(1);
}
fputs("one line of output\n", fp); // write to temp file
rewind(fp);
if (fgets(line, sizeof(line), fp) == NULL) { // then read it back
printf("fgets error\n");
exit(1);
}
fputs(line, stdout); // print the line we wrote
exit(0);
}
运行它:
但在编译以上程序时出现以下警告:
tmpfile函数的实现经常是先调用tmpnam产生临时文件名,然后创建该文件,并立即unlink它,对一个文件解除链接时,如果文件是打开的,会在关闭该文件时删除其中内容。
SUS为处理临时文件定义了以下函数,它们是XSI的扩展部分:
mkdtemp函数创建了一个目录,该目录名字唯一;mkstemp函数创建了一个文件,该文件名字也唯一。创建的目录或文件名是从参数template字符串中选择的,这个字符串是一个路径,其后六位是XXXXXX,函数会将后六位替换成不同的字符来构建一个唯一的路径名,如果这两个函数运行成功,则会修改参数template来反应临时文件的名字。
mkdtemp函数创建的目录权限为S_IRUSR | S_IWUSR | S_IXUSR
,调用进程的文件模式创建屏蔽字会进一步限制这些权限,该函数运行成功时会返回新目录名。
mkstemp函数创建一个普通文件并打开,该函数返回的文件描述符以读写方式打开。由mkstemp函数创建的文件权限为S_IRUSR | S_IWUSR
。
mkstemp创建的临时文件不会自动删除,需要手动删除。
使用tmpnam和tempnam(tempnam函数是tmpnam函数的一个变体,它允许调用者为所产生的路径名指定目录和前缀)函数获取唯一路径名后,用该名字创建文件前有一个时间窗口,而tmpfile和mkstemp函数不存在这个问题。
使用mkstemp函数:
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
void make_temp(char *template);
int main() {
char good_template[] = "/tmp/dirXXXXXX"; // right way
char *bad_template = "/tmp/dirXXXXXX"; // wrong way
printf("trying to create first temp file...\n");
make_temp(good_template);
printf("trying to create second temp file...\n");
make_temp(bad_template);
exit(0);
}
void make_temp(char *template) {
int fd;
struct stat sbuf;
if ((fd = mkstemp(template)) < 0) {
printf("can't create temp file\n");
exit(1);
}
printf("temp name = %s\n", template);
close(fd);
if (stat(template, &sbuf) < 0) {
if (errno == ENOENT) {
printf("file doesn't exist\n");
exit(1);
} else {
printf("stat error\n");
exit(1);
}
} else {
printf("file exists\n");
unlink(template);
}
}
运行它:
对于第一个模板,因为使用了数组,内存分配在栈上,而第二个模板使用的是指针,此时只有指针本身的内存分配在栈上,而第二个模板串存放在可执行文件的只读段,当mkstemp函数试图修改字符串时,出现了段错误。
由于标准IO库将数据缓存在内存中,因此每次一字符和每次一行的IO更有效。
SUSv4中支持了内存流,我们仍可以使用FILE指针来进行访问,但实际并没有底层文件,所有的IO都是在缓冲区与主存之间传送字节。
第一个创建内存流的函数:
fmemopen函数允许调用者提供缓冲区用于内存流,buf参数指向缓冲区开始的位置,size参数指定了缓冲区大小的字节数。如果buf参数为NULL,fmemopen函数会分配参数size字节数的缓冲区,这种情况下,关闭流时会释放缓冲区。
type参数可选值:
以上取值与基于文件的标准IO流的相同取值的差别:
1.以追加写打开内存流时,当前文件位置设为缓冲区中第一个null字节,如果缓冲区中不存在null字节,则当前位置设为缓冲区的尾后字节。由于追加模式通过第一个null字节确定数据的尾端,因此内存流不适合存储二进制数据。
2.如果buf参数为NULL,以只读或只写打开没有意义,因为这种情况下缓冲区是fmemopen函数分配的,没有方法获取缓冲区指针,因此只写打开时写入的数据无法读出,只读打开时没有数据可读。
使用内存流:
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#define BSZ 48
int main() {
FILE *fp;
char buf[BSZ];
memset(buf, 'a', BSZ - 2);
buf[BSZ - 2] = '\0';
buf[BSZ - 1] = 'X';
if ((fp = fmemopen(buf, BSZ, "w+")) == NULL) {
printf("fmemopen failed\n");
exit(1);
}
printf("initial buffer contents: %s\n", buf);
fprintf(fp, "hello, world");
printf("before flush: %s\n", buf);
fflush(fp);
printf("after fflush: %s\n", buf);
printf("len of string in buf = %ld\n", (long)strlen(buf));
memset(buf, 'b', BSZ - 2);
buf[BSZ - 2] = '\0';
buf[BSZ - 1] = 'X';
fprintf(fp, "hello, world");
fseek(fp, 0, SEEK_SET);
printf("after fseek: %s\n", buf);
printf("len of string in buf = %ld\n", (long)strlen(buf));
memset(buf, 'c', BSZ - 2);
buf[BSZ - 2] = '\0';
buf[BSZ - 1] = 'X';
fprintf(fp, "hello, world");
fclose(fp);
printf("after fclose: %s\n", buf);
printf("len of string in buf = %ld\n", (long)strlen(buf));
return 0;
}
运行它:
另外两个创建内存流的函数:
open_menstream函数创建的流是面向字节的,open_wmenstream函数创建的流是面向宽字节的,这两个函数与fmemopen函数的区别在于:
1.这两个函数创建的流只能写打开。
2.这两个函数不能指定自己的缓冲区,但可以通过bufp和sizep参数访问缓冲区地址和大小。
3.这两个函数关闭流后需要自行释放缓冲区。
4.这两个函数对流添加字节会增加缓冲区大小。
以上两个函数的bufp和sizep参数的使用:
1.只有在调用fclose或fflush后才有效。
2.只有在1后的下一次流写入或调用fclose前有效。
3.缓冲区可以增长,因此缓冲区的内存地址值会改变,改变更新发生在1后。
对于使用标准IO流来访问临时文件的函数来说,使用内存流只会访问主存,会有很大的性能提升。
标准IO库并不完善,比如效率不高(这与它需要复制的数据量有关,当使用每次IO一行的函数时,需要复制两次数据:一次是在内核和标准IO缓冲区之间(发生在调用read或write时),一次是在标准IO缓冲区和用户程序的缓冲区之间)。快速IO库fio避免了这一点,它使读一行的函数返回指向该行的指针,而非将该行复制到进程缓冲区。
sfio软件包与fio相近,它推广了IO流,使其不仅可以代表一个文件,也可代表存储区,可以编写处理模块,并将其以栈方式压入IO流。
mmap函数使用映射文件,它所在软件包为ASI(Alloc Stream Interface),与sfio软件包相同,ASI使用指针力图减少数据复制量。
用setvbuf函数实现setbuf函数:
#include <stdio.h>
#define BUFFSIZE 4096
int mySetbuf(FILE *restrict fp, char *restrict buf) {
if (buf == NULL || fp == stderr) {
if (0 != setvbuf(fp, buf, _IONBF, BUFFSIZE)) {
printf("setvbuf error\n");
return 1;
}
}
if (fp == stdin || fp == stdout) {
if (0 != setvbuf(fp, buf, _IOLBF, BUFFSIZE)) {
printf("setvbuf error\n");
return 1;
}
} else {
if (0 != setvbuf(fp, buf, _IOFBF, BUFFSIZE)) {
printf("setvbuf error\n");
return 1;
}
}
return 0;
}
printf函数返回0说明没有输出任何字符。
如果用char来存getchar函数或getc函数的返回值,再通过这两个函数的返回值是否是EOF来判断是否继续读,则在有些系统上可以正确运行,有些系统不能正确运行。如果系统使用的是有符号的字符类型,则程序可以正常工作;如果系统使用的是无符号字符类型,则EOF保存到字符c后不再是-1。
对标准IO流使用fsync函数时,需要先调用fflush,fsync函数的参数由fileno函数获得,如果不调用fflush,所有数据仍在内存缓冲区,此时fsync函数没有任何效果。
在使用标准IO交互程序读终端前,一般会输出提示信息,输出提示符后不需要调用fflush就能把提示信息输出到终端,即使提示信息中不带换行符(终端IO是行缓冲的),这是由于每次调用fgets时标准输出设备将冲洗。
基于BSD的系统提供了funopen函数使我们可以提供自己的读、写、定位、关闭流的函数,用它实现fmemopen函数:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/*
* Our internal structure tracking a memory stream
*/
struct memstream {
char *buf; // in-memory buffer
size_t rsize; // real size of buffer
size_t vsize; // virtual size of buffer
size_t curpos; // current position in buffer
int flags; // see below
};
// flags
#define MS_READ 0x01 // open for reading
#define MS_WRITE 0x02 // open for writing
#define MS_APPEND 0x04 // append to stream
#define MS_TRUNCATE 0x08 // truncate the stream on open
#define MS_MYBUF 0x10 // free buffer on close
#ifndef MIN
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif
static int mstream_read(void *, char *, int);
static int mstream_write(void *, const char *, int);
static fpos_t mstream_seek(void *, fpos_t, int);
static int mstream_close(void *);
static int type_to_flags(const char *__restrict type);
static off_t find_end(char *buf, size_t len);
FILE *fmemopen(void *__restrict buf, size_t size, const char *__restrict type) {
struct memstream *ms;
FILE *fp;
if (size == 0) {
errno = EINVAL;
return NULL;
}
if ((ms = malloc(sizeof(struct memstream))) == NULL) {
errno = ENOMEM;
return NULL;
}
if ((ms->flags = type_to_flags(type)) == 0) {
errno = EINVAL;
free(ms);
return NULL;
}
if (buf == NULL) {
if ((ms->flags & (MS_READ | MS_WRITE)) != (MS_READ | MS_WRITE)) {
errno = EINVAL;
free(ms);
return NULL;
}
if ((ms->buf = malloc(size)) == NULL) {
errno = ENOMEM;
free(ms);
return NULL;
}
ms->rsize = size;
ms->flags |= MS_MYBUF;
ms->curpos = 0;
} else {
ms->buf = buf;
ms->rsize = size;
if (ms->flags & MS_APPEND) {
ms->curpos = find_end(ms->buf, ms->rsize);
} else {
ms->curpos = 0;
}
}
if (ms->flags & MS_APPEND) { // "a" mode
ms->vsize = ms->curpos;
} else if (ms->flags & MS_TRUNCATE) { // "w" mode
ms->vsize = 0;
} else { // "r" mode
ms->vsize = size;
}
fp = funopen(ms, mstream_read, mstream_write, mstream_seek, mstream_close);
if (fp == NULL) {
if (ms->flags & MS_MYBUF) {
free(ms->buf);
}
free(ms);
}
return fp;
}
static int type_to_flags(const char *__restrict type) {
const char *cp;
int flags = 0;
for (cp = type; *cp != 0; cp++) {
switch (*cp) {
case 'r':
if (flags != 0) {
return 0; // error
}
flags |= MS_READ;
break;
case 'w':
if (flags != 0) {
return 0; // error
}
flags |= MS_WRITE | MS_TRUNCATE;
break;
case 'a':
if (flags != 0) {
return 0; // error
}
flags |= MS_APPEND;
break;
case '+':
if (flags == 0) {
return 0; // error
}
flags |= MS_READ | MS_WRITE;
break;
case 'b':
if (flags == 0) {
return 0; // error
}
break;
default:
return 0; // error
}
}
return flags;
}
static off_t find_end(char *buf, size_t len) {
off_t off = 0;
while(off < len) {
if (buf[off] == 0) {
break;
}
off++;
}
return off;
}
static int mstream_read(void *cookie, char *buf, int len) {
int nr;
struct memstream *ms = cookie;
if (!(ms->flags & MS_READ)) {
errno = EBADF;
return -1;
}
if (ms->curpos >= ms->vsize) {
return 0;
}
// can only read from curpos to vsize
nr = MIN(len, ms->vsize - ms->curpos);
memcpy(buf, ms->buf + ms->curpos, nr);
ms->curpos += nr;
return nr;
}
static int mstream_write(void *cookie, const char *buf, int len) {
int nw, off;
struct memstream *ms = cookie;
if (!(ms->flags & (MS_APPEND | MS_WRITE))) {
errno = EBADF;
return -1;
}
if (ms->flags & MS_APPEND) {
off = ms->vsize;
} else {
off = ms->curpos;
}
nw = MIN(len, ms->rsize - off);
memcpy(ms->buf + off, buf, nw);
ms->curpos = off + nw;
if (ms->curpos > ms->vsize) {
ms->vsize = ms->curpos;
if (((ms->flags & (MS_READ | MS_WRITE)) == (MS_READ | MS_WRITE))
&& (ms->vsize < ms->rsize)) {
*(ms->buf + ms->vsize) = 0;
}
}
if ((ms->flags & (MS_WRITE | MS_APPEND)) && !(ms->flags & MS_READ)) {
if (ms->curpos < ms->rsize) {
*(ms->buf + ms->curpos) = 0;
} else {
*(ms->buf + ms->rsize - 1) = 0;
}
}
return nw;
}
static fpos_t mstream_seek(void *cookie, fpos_t pos, int whence) {
int off;
struct memstream *ms = cookie;
switch (whence) {
case SEEK_SET:
off = pos;
break;
case SEEK_END:
off = ms->vsize + pos;
break;
case SEEK_CUR:
off = ms->curpos + pos;
break;
}
if (off < 0 || off > ms->vsize) {
errno = EINVAL;
return -1;
}
ms->curpos = off;
return off;
}
static int mstream_close(void *cookie) {
struct memstream *ms = cookie;
if (ms->flags & MS_MYBUF) {
free(ms->buf);
}
free(ms);
return 0;
}