下面要说的事绝不能归入我所谓"C++ 遗忘录"中, 不是忘了, 而是根本就不知道, 从没考虑过.
要搞IT, 你得学C, 如果你只学一个C语言函数, 那是main, 如果行有余力, 则学printf, 再有余力, 就学scanf.
scanf有几个近亲: sscanf, fscanf, 前面的s和f都是代表输入源的不同. scanf的输入源是stdin, sscanf的输入源是一个C字符串(当然C中没有所谓字符串这种东西, 它只是一个char序列碰巧以NUL结尾), fscanf的输入源是一个FILE *(stream).
几乎所有资料上都是拿printf作类比, 对scanf的格式转换操作符作一番介绍, 对sscanf和fscanf几乎从不展开讨论, 略带一提说类似于scanf, 确实, 与scanf的行为很类似. 但这里我重点要说不同.
在 C: A reference manual中, 对sscanf的介绍说, 当它碰到NUL时, 等同于fscanf碰到EOF, 因为毕竟对于C语言的一个字符串来说, 碰到NUL就说明输入源就这么多内容了. 这个行为很直观, 符合普通作用者的心理. 问题是: 如果 fscanf碰到了NUL而当前输入流的位置还没到达EOF呢?
我在下面的程序片断中碰到问题:
int main()
{
FILE * fp = fopen("C:/tmp.dat", "wb");
assert(fp);
// will write the 0
fwrite("asdf", sizeof("asdf"), 1, fp);
fwrite("1234", sizeof("1234") - 1, 1, fp);
fclose(fp);
char buf[100] = {0};
char c = 1;
fp = fopen("C:/tmp.dat", "rb");
int n = 0;
int n_scanf = fscanf(fp, "%[^ \0]%n", buf, &n);
printf("%d bytes read ok: %s\n", n, buf);
assert(n_scanf == 1);
// clearerr(fp);
// fflush(fp);
// if( feof(fp) ) ERR_MSG_BOX("EOF");
int n_read = fread(&c, 1, 1, fp);
if(n_read != 1)
{
perror(NULL);
assert(false);
}
assert( c == 0 );
fclose(fp);
printf("Hello, world\n");
return 0;
}
程序首先在文件中与入一段内容, 写入后验证内容如下:
0000000: 61 73 64 66 00 31 32 33 34 asdf.1234
注意asdf后面有个NUL字符, 再后面才是1234.
一开始, 我以 %s 代替 "%[^ \0]%n"作为格式符, 来企图读取asdf 到buf中, 再读下一个字符时, 发现失败了. 有一个隐藏的假设: fscanf遇到NUL时读取字符串一定会终止, 毕竟, 它应该知道C语言中的字符串是怎么回事, 但事实不是这样. fscanf在处理%s 时, 碰到空白字符(空格, TAB, \r, \n还有没有其它?)时停止, 碰到EOF时停止, 但碰到NUL竟然不停止. 用"%[^ \0]%n" 替换了%s后, 能如愿读取"asdf", 在NUL处停止.
上面的叙述是倒果为因, 真正的情形是, 我在产品代码中碰到用 fscanf + %s读取二进制流中的字符串, 出现意外的错误, 在上述小程序中实验, 仍然会出现错误, 并没有想到用ftell检查当前输入流的指针指向什么位置, 也没想过用等同的%n 检查真正读了多少字节. 而是对 fscanf 和 fread能否同时作用于一个二进制流上大加怀疑, 怀疑归怀疑, 在google, <> <>中都查过一遍, 仍然没看到说不能混合使用这两个表面上看分别针对文本流和二进制流的函数, 事实上, 它们可以. 一顿妄加猜疑之后, 我甚至怀疑因为 fscanf碰到了NUL而把文件流的内部状态置为了EOF, 所以尝试通过clearerr, fflush来重置流的状态. 在linux上(Unix传统上不区分对待文本流和二进制流)以同样的程序试验(当然得把C:/tmp.dat换成linux下的文件名), 效果一样.
只是 fscanf对格式符%s的处理中对待NUL的方式着实让人感到意外!
另外, 看相关资料时, 对scanf系列函数还发现了几个意外的盲点:
* 不要拿scanf跟printf作类比, 这是<>中的忠告, 我非常认同, 因为我的确尝试过在 scanf中用 %.*s 来限制能读入的最多字符数, *在scanf的格式符中确有作用, 但它与printf中的大不相同, 这是scanf与printf作类比时最容易引起误解的地方, 不过, 具我所知, 很多程序员甚至不知道printf中可以用 *来作用宽度和精度的点位符, 用对应的参数进行运行时替换, 这样倒也不至于误解scanf中*的不同作用了.
* scanf中的宽度是限制所应读取的最多字符数, 而printf中的宽度是指最小宽度
* printf格式中的空格, +, -, #, 0 在scanf中统统没有对应之物.
* scanf中不能指定精度, 想想这一点的原因其实很明显, 精度是对输出而言, 输入到一个float或double时, 内部的数据总是能表达确定精度的数据, scanf理应将所能读取的输入都转换成对应内部数据表示形式.
另外, 可以在%[..]结构中用\0 也是我没想过的事情.
阅读(2108) | 评论(4) | 转发(0) |