15

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

 3 years ago
source link: https://blog.popkx.com/%E5%88%9D%E5%AD%A6%E8%80%85%E5%B8%B8%E7%94%A8%E7%9A%84stdio%E5%BA%93-%E5%8E%9F%E6%9D%A5%E8%BF%98%E6%9C%89%E8%BF%99%E4%B9%88%E5%A4%9A%E7%9F%A5%E8%AF%86%E7%82%B9/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

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

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

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

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

827735cf6b101dc84d521114e6371aed.png

可知,其实 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语言的过程中遇到难题,欢迎在评论区回复,或者私信我,我将尽力解答。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK