在简单方程中用 scanf 替换 get 会使程序崩溃

Posted

技术标签:

【中文标题】在简单方程中用 scanf 替换 get 会使程序崩溃【英文标题】:Replacing gets with scanf in simple equation crashes the program 【发布时间】:2015-06-07 21:22:49 【问题描述】:

我正在使用 C for Dummies。它有一个使用 getsatoi 将英寸转换为厘米的示例代码(第 135 页,如果你有文本)。

现在,我想尝试使用scanf 而不是可怕的gets,这是我能想到的最好的方法(基于作者提供的代码)。

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

int main()

    float height_in_cm;
    char height_in_inches;

    printf("Enter your height in inches: ");
    scanf("%c"),&height_in_inches;
    height_in_cm = atoi(height_in_inches)*2.54;
    printf("You are %.2f centimetres tall.\n",height_in_cm);
    return(0);

程序启动,但输入后就崩溃了。我哪里错了?

【问题讨论】:

你确定这条线:scanf("%c"),&height_in_inches; ?我认为应该是 scanf("%c",&height_in_inches); 您可以在此处查看scanf函数(cplusplus.com/reference/cstdio/scanf)了解更多正确使用示例 错误:scanf("%c"),&amp;height_in_inches;。更好:scanf("%c", &amp;height_in_inches);。假设您要输入一个二进制的 8 位整数值(例如,对于“10”,请在键盘上输入 。如果您想要一个 STRING,(如字符“10 "); 我建议:BEST:int height_in_inches; scanf ("%d", &amp;height_in_inches); 在任何一种情况下,你都不需要 "atoi()"... @paulsm4 我同意你想使用 %d。我确实尝试过使用它,但是当它不起作用时,我不确定要更改哪个部分,所以这是其中之一。 实际上,Jonathan Leffler 的(新!)答案是最好的。总结:1)您最初的崩溃是因为“scanf()”语法不正确。 2)如果你想输入一个字符串,那么使用1字节的“char”也是错误的:你应该声明一个n字节的“char[]”数组。 3) 但坦率地说,让“scanf”为您完成工作并声明“height_in_inches”为您最终需要的类型(例如“float”或“double”)是最有意义的。强烈建议:再看看 Jonathan Leffler 的回复。 【参考方案1】:

既然scanf() 可以帮你转换答案,为什么还要花时间转换答案?

#include <stdio.h>

int main(void)

    float height_in_inches;

    printf("Enter your height in inches: ");
    if (scanf("%f", &height_in_inches) == 1)
    
        float height_in_cm = height_in_inches * 2.54;
        printf("You are %.2f inches or %.2f centimetres tall.\n",
               height_in_inches, height_in_cm);
    
    else
        printf("I didn't understand what you said\n");
    return(0);

如果你必须读取一个字符串,那么使用fgets():

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

int main(void)

    char line[4096];

    printf("Enter your height in inches: ");
    if (fgets(line, sizeof(line), stdin) != 0)
    
        double height_in = strtod(line, 0);
        double height_cm = height_in * 2.54;
        printf("You are %.2f inches or %.2f centimetres tall.\n",
               height_in, height_cm);
    
    return(0);

请注意,两个程序都会在使用输入结果之前检查输入是否发生。你可以争辩说,对strtod() 的调用的错误检查是非常懒惰的。我同意。请注意,我在第一个片段中的float 和第二个片段中的double 之间切换;要么可以工作。当结果为分数时,我认为没有特别的理由将输入限制为整数值。回显输入和输出通常也是有益的;如果你得到的回应不是你认为你输入的,这是一个很好的提示,表明某些地方出了问题,并让你在代码的正确区域进行搜索。

注意:有一些小细节被掩盖在地毯下,尤其是在第一个例子中,floatdoublescanf()printf()。鉴于下面的评论,它们目前与 OP 无关。

对原始代码的最小修复

由于上面的代码比 OP 识别的更复杂,这里是对原始代码的一组更简单的修复。输入到数组(字符串)中的字符串是关键点;这也需要更改 scanf() 调用。 通过使用大缓冲区,我们可以假设用户无法通过在终端键入来溢出输入。但是,对于机器驱动的输入来说就不行了。

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

int main(void)

    float height_in_cm;
    char height_in_inches[4096];    // Array, big enough to avoid most overflows

    printf("Enter your height in inches: ");
    // Missing error check on scanf() — too advanced as yet
    scanf("%s", height_in_inches);  // Format specifier, parentheses, ampersand
    height_in_cm = atoi(height_in_inches) * 2.54;
    printf("You are %s inches or %.2f centimetres tall.\n",
           height_in_inches, height_in_cm);
    return(0);

一行输入有多长?

user3629249commented:

可以通过以下方式节省大量堆栈空间:

    注意到“int”最多只能包含 12 个字符,因此输入缓冲区的最大长度为 13 个字符(以允许 NUL 字符串终止字节) 将scanf() 限制为12 个字符。 IE。 'scanf( "%12s", myCharArray ); 在 C 中,数组的名称降级为数组的地址,因此“myCharArray”上不需要前导“&”。

第 3 点是正确的;如果您使用char myCharArray[13];,则在调用scanf() 等时不要使用&amp;myCharArray;你只使用myCharArray。如果您滥用&amp;,一个好的编译器会指出您的方式错误。

不过,我对第 1 点和第 2 点有疑问。 很多的麻烦可以通过注意如果用户在线输入 9876432109876543210 来避免,那么使用 scanf()%12s 对消除无效输入没有太大帮助。它将在行上留下 8 个未读数字,而读取的内容仍会溢出一个 32 位整数。如果您在较长的字符串上使用strtoX() 系列函数而不是atoi(),那么它们会检测到诸如溢出之类的问题,而scanf()%datoi() 都不会。 (这是主要答案中掩盖的众多观点之一。)

此外,在具有兆字节(通常是千兆字节)主内存的系统上,堆栈上的 4 KiB 不是主要问题。也就是说,我使用 4 KiB 部分是因为它的冲击值;但 POSIX 要求 [LINE_MAX] 的最小值为 2048。

如果您正在阅读基于行的输入,这通常是在命令行应用程序中进行输入的好方法,那么您需要确保阅读整行,因为处理部分行会很混乱并且会导致错误报告难的。 fgets() 加上sscanf() 处理的一个主要优点是您可以在错误报告中使用完整的行,而不是scanf() 在处理部分行后留下的内容。如果第一次尝试失败,您也可以尝试以不同的方式扫描字符串; scanf() 系列中的直接文件 I/O 函数无法做到这一点。

如果您天真地没有认识到人们在您希望他们输入短行时会输入长行,那么您可以将剩菜作为新行 - 无需进一步的用户交互 - 当它实际上是前一行的渣滓时输入。例如,如果您扫描 20 位数字中的 12 位,那么下一个输入将获得剩余的 8 位数字,而无需等待用户输入任何新内容,即使您提示他们输入更多信息。 (另外,请注意using fflush(stdin);它是否有任何用处,充其量是特定于系统的。)

我使用 fgets() 和 4 KiB 缓冲区。如果您想防止程序在单行上发送数兆字节的 JSON 编码数据,则需要使用 POSIX 的 getline() 函数来读取该行。它为整行分配足够的空间,除非内存不足。对于大多数学生的练习作业,4 KiB 缓冲区和fgets() 是一个合理的替代品。只要指数至少为 8,我愿意就使用的 2 的幂进行协商——该值至少为 256。例如,将行缓冲区限制为 80 个字符并不会阻止用户输入超过一行 80 个字符。这只是意味着额外的字符不太可能得到适当的处理。将缓冲区限制为 13 个字符不会给您带来任何有价值的东西,IMO,而且“节省堆栈空间”是一种过早的优化。

【讨论】:

你的例子,尤其是第二个,有点超前了。我真的是一个初学者。虽然我很高兴从目前所见的情况中了解ifelse 很抱歉超出了您所学的范围。我没有这本书,但在输入大量内容之前,我会期待ifwhilefor 的基本报道——显然,我错了。一两周后回来;那样会更有意义。要带走的一个关键点是输入操作(特别是)可能会失败,并且在使用结果之前应该检查输入操作的成功/失败。回应您阅读的内容是个好主意。如果您认为自己输入了 10,但计算机认为您输入了 31415332132,那么它会很好地提示您有问题。 我明白了,这确实有道理。这本书非常“这个做这个,那个做那个”,在我的发现点上是可以的,但是当我弄乱这些例子时它会引起问题。尽管如此,K&R 还是太过分了,所以我现在很高兴。 大量堆栈空间可以通过以下方式节省 1) 注意到“int”最多只有 12 个字符,因此输入缓冲区的最大长度为 13 个字符(以允许 NUL 字符串终止byte) 2) 将 scanf() 限制为 12 个字符。 IE。 'scanf("%12s", myCharArray); 3) 在 C 中,数组的名称降级为数组的地址,因此在 'myCharArray' 上不需要前导 '&' @user3629249:查看解决您观察的答案的广泛补充。【参考方案2】:

你的演员阵容有问题。

atoi 接受 const char* 作为输入。

您正在传递一个 char,因此它隐式地将 char 转换为一个点,这是一件坏事。

根据 user3121023 的建议,将 height_in_inches 更改为字符串。

char height_in_inches[20];

并使用 %s 读取

scanf("%s", height_in_inches);

【讨论】:

@Ducoodi 想避免使用gets(),但你建议使用scanf()%s?至少使用%19s 以防止潜在的缓冲区溢出。 提供可编译的代码是明智的。 char [20] height_in_inches; 不适用于大多数 C 编译器(当然应该是 char height_in_inches[20];)。 通常的约定是指定完整的缓冲区大小,但标准 I/O 库早于该约定的广泛使用,它选择了可以存储的字符串长度。在char buffer[20]; 数组中,您可以存储最多19 个字符加上空终止字节的字符串。 strlen() 也不计算空终止字节。这是一个轻微的不一致,旨在使初学者的生活更加困难。嗯,不是真的。它只是 30 多年发展的遗产(除非您认为该设计是在 70 年代后期制作并在 80 年代后期冻结的)。 对不起我的错 char[] 是在 C# 而不是 C. @Jonathan Leffler Minor 不同意字符串 2019 理性。 "%19s" 中的数字 19 表示要扫描的最大(非空白)字符 - 它是扫描宽度。这与"%3d""%7f" 和所有限制扫描宽度的说明符一致。 width 参数并不代表存储对象所需的空间。只是对于字符串,所需的大小是宽度 + 1。OTOH,如果原始 scanf() 提供更简单的动态字符串大小限制,那就太好了。

以上是关于在简单方程中用 scanf 替换 get 会使程序崩溃的主要内容,如果未能解决你的问题,请参考以下文章

在 Notepad++ 中用 \t 替换正则表达式

如何在熊猫中用 NaN 替换浮点值?

在 Bash 中用另一个字符替换一个字符

在 awk 中用换行符替换“\n”

如何在应用程序启动脚本中用环境变量替换硬编码的 JAVA_HOME?

在 Node.js 中用 Promise 替换回调