初学者常用的stdio库,原来还有这么多知识点

初学者在入门C语言时,第一个示例程序往往是向控制台打印“hello world!”字符串。在随后的学习中,也常常要使用 stdio 库的 getchar(),scanf(),sprintf() 等函数,有时会遇到一些奇怪的问题,这些奇怪的问题,其实是对 stdio 库的理解不够深入。

因此,本节将讨论C语言初学者使用 stdio 库时可能会遇到的几个问题,希望对初学者有所帮助。。

下面这段C语言代码有什么问题?

小明知道 getchar() 函数可以从标准缓冲区读取一个字符,他在做C语言练习题时,写出了一段代码,但是在编译运行时,发现自己写的代码是有问题的,经过排查,确定问题代码是下面这两段,请看:

char c;
while((c=getchar()) != EOF){
    ...
}

出了什么问题呢?其实这是小明对 getchar() 函数的功能理解不够深入导致的,查看该函数的使用说明:

可知,其实 getchar() 读取字符后,会将其转换为 int 型返回。所以首先,上述C语言代码中的变量 c 应该修改为 int 型的。EOF 是一个宏,它被定义在 stdio.h 文件里:

#ifndef EOF
# define EOF (-1)
#endif

看了我之前文章的读者应该明白,C语言程序在处理数值运算时,一般都会有个“整形提升”的过程,因此 EOF 实际上是作为区别所有其他字符的存在。很多初学者认为 EOF 是文件结束符,所有文件都是以 EOF 结尾的。其实不是的,EOF 不是文件的实际尾字符,它只是一个标志位,表示没有更多字符的信号。

所以,getchar() 函数的返回值必须是一个能够完整包含所有字符的数据类型(int 型就可以包含所有 char 型数值),以便它可以表示任意字符和 EOF 等标志位。

如果像小明那样,使用 char 型变量 c 接收 getchar() 函数的返回值,可能会出现两种问题:
* 如果在小明的C语言编译器中,char 是有符号的,并且 EOF 被定义为 -1,那么要是有字符恰好等于 0xff,getchar() 就会提早结束。
* 如果 char 是无符号的,则实际的 EOF 值会被截断,不再会被识别为 EOF,C语言程序将陷入无限死循环。

当然了,如果 char 是有符号的,并且小明接着输入的全部是 7 位以下的字符,那么他的C语言程序可能很长一段时间也无法遇到上面提到的错误。

怎样才能使用键盘输入 EOF 呢?

小明的C语言程序使用 getchar() 函数读取用户输入,并且在遇到 EOF 时跳出 while() 循环,那么怎样输入 EOF 呢?前面提到它在 stdio.h 的定义是 -1,只需要输入 -1 就可以了吗?

读者应该明白,我们不可能输入 -1 给 getchar() 函数的,因为“-1”其实是由两个字符('-' 和 '1')组成的,而 getchar() 一次只读取一个字符。

所以,在上述C语言程序中的 EOF 基本上与读者可能用来从键盘输入的字符无关,EOF 本质上是一个信号,它告诉C语言程序到底是什么原因导致的输入无字符可用了(例如磁盘文件结束,用户输入完成,网络流关闭,I/O错误等)。

那是不是小明的C语言程序只能死循环了呢?也不是,读者可以根据自己的文件系统,使用按键组合(一般是 ctrl+d 或者 ctrl+z)来指示文件结束,然后操作系统和 stdio 库会安排C语言程序接收 EOF 值。但是,读者应该明白,按键组合输入的 EOF 其实只是一种约定,正常情况下,我们不应该是显式的检查按键组合的值(你也找不到,蛤蛤)。

下面这个 scanf() 代码为什么不能正常工作呢?

小明在他的C语言代码中需要用户输入一个浮点数,因此他写下了如下代码,可是为什么不能正常工作呢?

#include <stdio.h>

int main()
{
    double d;

    scanf("%f", &d);
    printf("%f\n", d);

    return 0;
}

编译并执行这段C语言代码,得到如下输出,输入的是 4.32,输出怎么编程 0.0 了呢?

# gcc t.c
# ./a.out 
4.32
0.000000

不像 printf() 函数可以自动提升数据类型(将 float 提升为 double),scanf() 函数对格式符的要求更加严格:%f 只能够使用 float 变量接收!如果希望使用 double 变量接收 scanf() 传递的值,就应该使用 %lf 格式符了,因此,上述C语言代码修改为:

...
scanf("%lf", &d);
...

小明的C语言程序就能正常工作了。类似的问题可能初学者还会遇到,例如定义了 short int s; 再调用 scanf("%d", &s); 也不会得到预期结果,原因和小明的问题是一致的,留给读者自己思考了。

sprintf()函数可以方便的组合数字和字符,但是如何判断需要事先分配多少内存空间呢?

在C语言程序开发中,处理字符串与数字的组合时,使用 sprintf() 函数是非常方便的。但是,有时候我们并不能事先确定最终得到的字符串长度,那么该如何分配内存供 sprintf() 函数使用,以避免内存溢出呢?

如果需求相对简单,我们有时可以直接指定一个“足够大”的内存空间供 sprintf() 函数使用。在某个函数中, sprintf() 仅供开发者自己使用,并且最终字符串长度不会超过 80,那么为了方便,完全可以指定一个固定长度的数组,相关C语言代码如下,请看:

char buf[128];
sprintf(buf, "answer is \"%s\"", answer);

当然了,这么做并不是一直安全的,如果C语言程序某次得出的 answer 长度超过 128,那么程序就很可能崩溃了。更安全的做法是使用动态内存分配:

int bufsize = sizeof("answer is \"%s\"") + strlen(answer);
char *buf = malloc(bufsize);
if(buf != NULL)
    sprintf(buf, "answer is \"%s\"", answer);

可见,安全的代价是让C语言代码变得复杂,开销增大。当使用 sprintf() 处理数字时,为了方便,可以保守估计出所需内存,参考C语言代码如下,请看:

#include <limits.h>
char buf[(sizeof(int) * CHAR_BIT + 2) / 3 + 1 + 1];
sprintf(buf, "%d", n);

这段C语言代码可以计算出 int 型数组占用内存的字节数。sizeof(int)*CHAR_BIT表示 int 型的位数,+2是为了处理其除以 3 被截断的部分,+1+1 则是为数字前面的负号 '-',以及字符串结束符 '\0' 预留的内存空间。

如果 sprintf() 的格式符更复杂一点,预测其将要使用的内存空间就更加复杂了,甚至不可预测。一个小技巧是先使用 fprintf() 将相同的字符串打印到临时文件中,根据 fprintf() 的返回值或文件大小判断字符串长度,再动态分配出所需内存。

在Linux中,fprintf() 可以将字符串打印到 /dev/null 中,即所谓的“黑洞”中,这样就无需创建临时文件了。

如果实在不能确定缓冲区的长度,那么为了保证缓冲区不会溢出覆盖其他内存区域,就不再建议使用 sprintf() 函数了,而是使用可以指定固定长度的 snprintf() 函数。

snprintf(buf, bufsize, "answer is \"%s\"", answer);

snprintf()函数已经在C99中正式采纳了。C99 中的 snprintf() 函数也可以获取最终的字符串长度,示例C语言代码如下,请看:

nch = snprintf(NULL, 0, fmtstring, /* other arguments */ );

这样一来,我们就可以根据返回值 nch,使用 malloc() 申请一个足够大的缓冲区,并再次调用 snprintf() 填充它。

小结

其实,本节讨论的几个问题都是我的读者或者粉丝向我反馈的,而且这几个问题比较典型,不止一个读者有这样的疑问。不过,也可以看出,这些问题其实都是对 stdio 库的理解不够深入导致的,这对于我们的教训是,当在学习C语言程序开发的过程,发现函数没有按照预期工作时,首选的解决方案就是查看其详细的使用说明,这对于在 Linux 下学习C语言的同学来说很简单,一般只需要在终端输入 man + 函数名 即可。

限于篇幅,本节不可能对 stdio 库函数一一分析,当然了,如果读者在阅读文章或者学习C语言的过程中遇到难题,欢迎在评论区回复,或者私信我,我将尽力解答。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif