9

C语言scanf()处理用户输入时,不能事先获知用户的输入长度,怎么办?

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80scanf%E5%A4%84%E7%90%86%E7%94%A8%E6%88%B7%E8%BE%93%E5%85%A5%E6%97%B6-%E4%B8%8D%E8%83%BD%E4%BA%8B%E5%85%88%E8%8E%B7%E7%9F%A5%E7%94%A8%E6%88%B7%E7%9A%84%E8%BE%93%E5%85%A5/
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.

不少初学者刚开始学习C语言时,都非常乐意练习写可以交互的程序。所谓“可交互”,其实就是程序允许我们输入一些信息,然后根据这些信息输出对应的结果。在这一过程中,最常使用的两个库函数就是 printf() 函数和 scanf() 函数了。

不能事先得到字符串长度,怎么办?

现在来考虑一个最简单的C语言交互程序:读取用户输入的姓名,并将其打印出来。这样的问题,相信即使是初学者也能轻易的解决:

#include <stdio.h>

int main()
{
    char buf[128];
    scanf("%s", buf);
    printf("you input: %s\n", buf);

    return 0;
}
file

这样的C语言代码的确能够在一定程度解决问题,但是还不够完美,因为只有当用户输入 128 字节长度以内的姓名时,它才能正常工作。

也许没有人的姓名长度超过 128 字节,但是我们不能假设没有人恶意的,或者调皮的输入超出 128 字节的字符串,这种情况下,上述C语言程序就不能工作了,程序会表现出不能预知的行为。(常常直接就崩溃了。)

C语言要求我们在使用变量之前声明它,因此 scanf() 函数使用变量 buf 之前要先定义。可是,没有人能够事先预知程序的使用者究竟会输入多少字符,所以上面那个“最简单的交互”问题并不简单,而且似乎这是一个不可能解决的问题。

诚然,没有人能够未卜先知,但是这并不表示不能完美解决“最简单的交互”问题。事实上,“无所不能”的C语言并非浪得虚名,有 N 种方法解决该问题。当然,本文不可能将这些方法一一列举,只将一些常用的方法举出。

非标准扩展

上述问题的确是个难题,因此有些设计者增加了一些C语言扩展,应对不能事先获知输入长度的问题。一个非常好用的扩展就是 scanf() 的“%ms”占位符,下面是一段相关的C语言代码示例:

char *m = NULL;
printf("please input a string\n");
scanf("%ms",&m);
if (m == NULL)
    fprintf(stderr, "That string was too long!\n");
else
{
    printf("this is the string %s\n",m);
    /* ... any other use of m */
    free(m);
}
file

介于 % 和 s 之间的 m 有 measure(测量)的含义,它可以测量输入字符串的长度,然后 scanf() 根据字符串的长度分配内存,并将字符串拷贝到这段内存,之后将首地址返回给 m。显然,在使用完毕后,需要调用 free() 函数释放这段内存。

不过值得说明的是,并不是所有的平台都支持这一的用法,正如小标题所述,这是C语言的“非标准扩展”。

指定 scanf() 的读取长度

一般来说,没有(中国)人的名字长度超过 128 字节,所以要解决文章开头的问题,定义长度为 128 字节的内存足够使用了。这样看来,要是仅仅为了避免用户恶意输入过长的字符串,可以为 scanf() 指定读取长度。下面是一段C语言代码示例:

char name[128];
scanf("%127s",&name);

介于 % 和 s 之间的数字应小于 name 的长度,这个数字表示 scanf() 一次最多读取 127 字节的数据放入 name。如果用户输入的字符串超出了 127 字节,剩下的字符将留在缓冲区内,等待下一次读取。

可见,在C语言程序开发中需要读取用户输入时,即使不能事先获知输入长度,也是有办法写出高稳定性的程序的。不过在实践中,一般不推荐使用 scanf() 函数处理用户输入,即使我们解决了输入长度的问题。

我们不能保证用户一定会遵守程序的设计,例如 scanf() 使用了 %d 占位符,意味着需要用户输入一个整数,但是总有人会输入别的东西(浮点数,字符串等)。

自己实现“扩展”

相比于几十年前,现代计算机的性能已经得到大大的提高,所以要实现“健壮”的交互程序,似乎只要定义一段“足够长”的内存供输入使用就可以了,但是姑且不谈“足够长”实在是一个模糊的概念,万一恶意用户有足够的耐心,他总是能够输入比“足够长”还要长的字符串。

事实上,考虑到C语言程序主要用于嵌入式开发,不应该浪费一点资源,实践中常用的方法是一次最多只读取固定长度的字节,重复这一过程,直到将所有字节全部读取,再做一个简单的组合就可以了。

下面是一段C语言代码示例,该例子每次只读取 200 个字节,而且考虑到 scanf() 的不安全,下面的代码使用了 fgets() 函数处理输入。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* readinput()
{
#define CHUNK 200
   char* input = NULL;
   char tempbuf[CHUNK];
   size_t inputlen = 0, templen = 0;
   do {
       fgets(tempbuf, CHUNK, stdin);
       templen = strlen(tempbuf);
       input = realloc(input, inputlen+templen+1);
       strcpy(input+inputlen, tempbuf);
       inputlen += templen;
    } while (templen==CHUNK-1 && tempbuf[CHUNK-2]!='\n');
    return input;
}
file

敏锐的读者应该已经发现了,这其实就是前文提到的“非标准扩展”的一种实现。以后要读取用户的输入,调用我们自定义的 readinput() 就可以了:
int main()
{
    char* result = readinput();
    printf("And the result is [%s]\n", result);
    free(result);
    return 0;
}

readinput() 函数没有浪费一个字节,它总是能够根据输入字符串的长度调节要使用的内存长度。不过,因为 readinput() 使用了 realloc() 函数,不要忘记收尾时调用 free() 释放掉内存。

读者应注意,为了便于讨论,上述C语言代码没有做错误检查和处理。

看似简单的C语言交互程序,其实也暗含着较难解决的问题。没有人能够未卜先知,事先知道用户究竟会输入多长的字符,但是使用C语言总是有办法写出“健壮”的程序的。值得注意的是,可能初学者对 scanf() 函数更加熟悉,但是在实践中并不推荐使用它处理输入,fgets() 函数是更好的选择。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK