分类: LINUX
2008-11-20 09:39:15
如果用C语言编写过程序,对那些按设计格式输出数据的库函数就应该比较熟悉。这些函数包括向一个文件流输出数据的printf系列函数和从一个文件流读取数据的scanf系列函数。
*
printf、fprintf 和sprintf 函数
printf系列函数能够对各种不同类型的参数进行格式编排和输出。每个参数在输出流中的表示形式是由格式参数format控制的,它是一个包含普通的可打印字符和称为“转换控制符”代码的字符串,转换控制符规定了其余的参数应该以何种方式被输出到何种地方。
printf函数把自己的输出送到标准输出。fprintf函数把自己的输出送到一个指定的文件流。sprintf函数把自己的输出和一个结尾空字符写到作为参数传递过来的字符串s里。这个字符串必须足够大以容纳所有的输出数据。Printf系列函数还有一些其他的成员,它们以各自不同的方式对其参数进行处理。详细资料请参考printf的使用手册。
普通字符在输出时不发生变化。转换控制符让printf取出传递过来的其他参数并对它们的格式进行编排。转换控制符总是以%字符开头。
要想输出%字符,就必须使用%%,这样就不会与转换控制符混淆了。
下面是一些常用的转换控制符:
%d:%i:以十进制格式输出一个整数。
%o:%x:以八进制或十六进制格式输出一个整数。
%c:输出一个字符。
%s:输出一个字符串。
%f:输出一个(单精度)浮点数。
%e:以科学计数法格式输出一个双精度浮点数。
%g:以一般格式输出一个双精度浮点数。
让传递到printf函数里的参数数目和类型与format字符串里的转换控制符匹配是非常重要的。整数参数的类型可以用一个可选的长度限定符来指定。它可以是h,例如%hd表示这是一个短整数(short int),或者l,例如%ld表示这是一个长整数(1ong int)。有的编译器能够对printf语句进行检查,但并不总是如此。如果你使用的是GNU编译器gcc,-Wformat选项可以完成这一工作。
以下略。
*
scanf、fscanf 和sscanf 函数
scanf系列函数的工作方式与printf系列函数很相似,只是前者的作用是从一个文件流里读取数据,并把数据值放到传递过来的指针参数指向的地址处的变量中。它们也使用一个格式字符串来控制输入数据的转换,其工作原理和许多转换控制符都与printf系列函数的情况一致。
#include
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char
*format, ...);
int sscanf(const char *s, const char *format, ...);
scanf函数读入的值将保存到对应的变量里去,这些变量的类型必须正确,并且它们必须精确匹配格式字符串。否则,内存就可能会发生冲突,从而使程序崩溃。编译器是不会对此做出错误提示的,但如果你运气够好,你可能会看到一个警告信息!
scanf系列函数的format格式字符串里同时包含着普通字符和转换控制符,就像printf函数中一样。但那些普通字符是用来指定在输入数据里必须出现的字符。
下面是一个简单的例子:
int num;
scanf(“Hello %d”, &num);
这个scanf调用只有在标准输入中接下来的五个字符匹配“Hello”的情况下才会成功。然后,如果后面的字符构成了一个可识别的十进制数字,该数字就将被读入并赋值给变量num。格式字符串中的空格用于忽略输入数据中转换控制符之间的各种空白字符(空格、制表符、换页符和换行符)。这意味着在下面两种输入情况下,这个scanf调用都会执行成功,并把1234放到变量num里:
Hello 1234
Hello1234
输入里的空白字符在进行数据转换时一般也会被忽略。也就是说,格式字符串%d将持续读取输入,忽略空格和换行符,直到找到一组数字为止。如果预期的字符没有在输入流里出现,转换将失败,scanf也将返回。
如果不注意,这会产生问题。如果用户在输入中应该出现一个整数的地方放的是一个非数字字符,就可能在程序里导致一个无限循环。
下面是一些其他的转换控制符:
%d:读取一个十进制整数。
%o、%x:读取一个八进制或十六进制整数。
%f、%e、%g:读取一个浮点数。
%c:读取一个字符(不会忽略空格)。
%s:读取一个字符串。
%[]:读取一个字符集合(见下面的说明)。
%%:读取一个%字符。
类似于printf,scanf的转换控制符里也可以加上对输入数据字段宽度的限制。长度限定符(h对应于短整数,l对应于长整数)指明接收参数的长度是否比默认情况更短或更长。也就是说,%hd表示要读入一个短整数,%ld表示要读入一个长整数,而%lg表示要读入一个双精度浮点数。
以星号(*)打头的控制符表示对应位置上的输入数据将被忽略,也就是说,这个数据不会被保存,因此不需要使用一个变量来接收它。
我们使用%c控制符从输入中读取一个字符。它不会跳过起始的空白字符。
我们使用%s控制符来扫描字符串,但使用时必须小心。它会跳过起始的空白字符,但会在字符串里出现的第一个空白字符处停下来,所以,我们最好还是用它来读取单词而不是一般意义上的字符串。此外,如果没有使用字段宽限定符,它能够读取的字符串的长度是没有限制的,所以接收字符串必须有足够的空间来容纳输入流中可能的最长字符串。较好的选择是使用一个字段限制符,或者结合使用fgets和sscanf,从输入中读入一行数据,再对它进行扫描。这样可以避免可能被恶意用户利用而造成缓冲区溢出的情况。
我们使用%[]控制符读取一个由一个字符集合中的字符构成的字符串。格式字符串%[A-Z]将读取一个由大写字母构成的字符串。如果字符集中的第一个字符是^,就表示将读取一个由不属于该字符集合中的字符构成的字符串。因此读取一个其中带空格的字符串,并且在遇到第一个逗号时停止,可以用%[^,]。
给定下面输入行:
Hello, 1234, 5.678, X, string to
the end of the line
以下会正确输入
char s[256];
int n;
float f;
char c;
scanf(“Hello,%d,%g,
%c, %[^\n]“, &n,&f,&c,s);
scanf函数的返回值是它成功读取的数据项个数,如果在读第一个数据项时失败了,返回值就将是零。如果在匹配第一个数据项之前就已经到达了输入的结尾,就会返回EOF。如果文件流发生读错误,流错误标志就会被设置并且错误变量errno将被设置以指明错误类型。详细情况请参考
一般来说,对scanf系列函数的评价并不高,这主要有三方面原因:
从历史来看,它们的具体实现都有漏洞。
它们的使用不够灵活。
使用它们编写的代码不容易看出究竟要读取什么。
尽量使用其他函数,如fread或fgets来读取输入行,再用字符串函数把输入分割成需要的数据项。
*
其他流函数
stdio函数库里还有一些其他的函数,它们使用流参数或标准流stdin、stdout和stderr:
fgetpos:获得文件流的当前(读写)位置。
fsetpos:设置文件流的当前(读写)位置。
ftell:返回文件流当前(读写)位置的偏移值。
rewind:重置文件流里的读写位置。
freopen:重新使用一个文件流。
setvbuf:设置文件流的缓冲机制。
remove:相当于unlink函数,但如果它的path参数是一个目录的话,其作用就相当于rmdir
函数。
所有这些库函数在man使用手册的第三节中都有说明。
你可以使用文件流函数重新实现前面的文件拷贝程序,这次使用库函数。请看下面的copy_stdio.c程序。
# vi copy_stdio.c
#include
#include
int main()
{
int c;
FILE *in, *out;
in =
fopen("file.in","r");
out =
fopen("file.out","w");
while((c = fgetc(in)) != EOF)
fputc(c,out);
exit(0);
}
运行结果:
# TIMEFORMAT="" time ./copy_stdio
0.02user 0.00system 0:00.02elapsed 91%CPU
(0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+105minor)pagefaults 0swaps
这一次程序运行了0.35秒,不如底层数据块拷贝版本快,但比那个一次拷贝一个字符的版本要快得多。因为stdio库在FILE结构里使用了一个内部缓冲区,只有在缓冲区满时才进行底层系统调用。读者可以利用stdio库函数自行编写出实现逐行拷贝和数据块拷贝的程序,看它们与我们在这一章里给出的三个示例程序相比性能到底如何。
*
文件流错误
为了表明错误,许多stdio库函数会返回一个超出范围的值,比如空指针或EOF常数。此时,错误由外部变量errno指出:
#include
extern int errno;
注意,有许多函数可能改变errno的值。它的值只有在函数调用失败时才有意义。你必须在函数表明失败之后立刻对其进行检查。在使用它之前,你应该总是将它拷贝到另一个变量中,因为像fprintf这样的输出函数本身就可能改变errno的值。
也可以通过检查文件流的状态来确定是否发生了错误,或者是否到达了文件尾。
#include
int ferror(FILE *stream);
int feof(FILE *stream);
void clearerr(FILE *stream);
ferror函数测试一个文件流的错误标识,如果该标识被设置就返回一个非零值,否则返回零。feof函数测试一个文件流的文件尾标识,如果该标识被设置就返回非零值,否则返回零。我们可以像下面这样使用它:
if(feof(some_stream))
/* We’re at the end */
clearerr函数的作用是清除由stream指定的文件流的文件尾标识和错误标识。它没有返回值,也未定义任何错误。你可以通过使用它从文件流的错误状态中恢复。例如,在“磁盘已满”错误解决之后,继续开始写入文件流。
*
文件流和文件描述符
每个文件流都和一个底层文件描述符相关联。你可以把底层的输入输出操作与高层的文件流操作混在一起使用,但一般来说这并不是一个明智的做法,因为数据缓冲的后果难以预料。
#include
int fileno(FILE *stream);
FILE *fdopen(int fildes, const char *mode);
我们可以通过调用fileno函数来确定文件流使用的是哪个底层文件描述符。它返回指定文件流使用的文件描述符,如失败就返回-1。在需要对一个己经打开的文件流进行底层访问时(例如想对它调用fstat),这个函数将很有用。
我们可以通过调用fdopen函数在一个已打开的文件描述符上创建一个新的文件流。实质上,这个函数的作用是为一个已经打开的文件描述符提供stdio缓冲区,这样的解释可能更容易理解一些。fdopen函数的操作方式与fopen函数是一样的,只是前者的参数不是一个文件名,而是一个底层的文件描述符。当我们已经通过open系统调用创建了一个文件(可能是出于为了更好地控制其访问权限的目的),但又想通过文件流来对它进行写操作时,这个函数就很有用了。fdopen函数的mode参数与fopen函数使用的完全一样,但它必须符合该文件在最初创建时所设定的访问权限。fdopen返回一个新的文件流,失败时返回NULL。