【问题标题】:C Program crashes when adding an extra int添加额外的 int 时 C 程序崩溃
【发布时间】:2016-05-07 10:19:52
【问题描述】:

我是 C 新手并使用 Eclipse IDE 以下代码工作正常:

#include <stdio.h>
#include <stdlib.h>
#include <String.h>
int main()
{
    char *lineName;
    int stationNo;
    int i;
    while (scanf("%s (%d)", lineName, &stationNo)!=EOF) {
        for (i=0; i<5 ; i++ ){
            printf("%d %d",i);
        }
    }
    return 0;
}

输入:

Green (21)

Red (38)

输出:

Green (21)

Red (38)
0123401234

但是,当简单地添加一个新的 int 时:

#include <stdio.h>
#include <stdlib.h>
#include <String.h>
int main()
{
    char *lineName;
    int stationNo;
    int i,b=0;
    while (scanf("%s (%d)", lineName, &stationNo)!=EOF) {
        printf("%d",b);
        for (i=0; i<5 ; i++ ){
            printf("%d",i);
        }
    }
    return 0;
}

程序会因相同的输入而崩溃。 谁能告诉我为什么?

【问题讨论】:

  • 试试char *lineName; --> char lineName[32];
  • @BLUEPIXY 可以,但你能告诉我为什么吗?
  • 它们是 UB(未定义行为)。例如scanf("%s (%d)", lineName:写入不存在要保存的区域的位置。 printf("%d %d",i);:参数不匹配。
  • 你的指针lineName 没有指向分配的内存

标签: c


【解决方案1】:

您说您的第一个程序“有效”,但它只是偶然有效。这就像一辆汽车在路上疾驰而过,前轮上没有螺帽,只是奇迹般地它们还没有掉下来——还没有。

你说

char *lineName;

这为您提供了一个可以指向某些字符的指针变量,但它还没有指向任何地方。此指针的值未定义。这有点像说“int i”并询问i 的值是什么。

接下来你说

scanf("%s (%d)", lineName, &stationNo)

您要求scanf 读取行名并将字符串存储在lineName 指向的内存中。但那段记忆在哪里?我们什么都不知道!

考虑未初始化指针的情况有点棘手,因为与往常一样,对于指针,我们必须区分指针的值内存中的数据指针指向的。之前我提到说int i 并询问i 的值是什么。现在,i 中会有 一些 位模式——它可能是 0、1、-23 或 8675309。

同样,lineName 中也会有一些位模式——它可能“指向”内存位置 0x00000000、0xffe01234 或 0xdeadbeef。但接下来的问题是:该位置实际上是否有任何内存,我们是否有权写入它,它是否被用于其他任何事情? 如果有内存并且我们确实有权限并且它没有被用于其他任何事情,那么该程序可能似乎可以工作 - 现在。但这是三个相当大的如果!如果内存不存在,或者我们没有写入权限,程序可能会在尝试时崩溃。如果内存被用于其他用途,那么当我们要求scanf 将其字符串写入那里时,就会出现问题——如果不是现在,那么以后。

而且,真的,如果我们关心的是编写有效的程序(并且因为正确的原因而有效),我们不必问任何这些问题。当我们不初始化它时,我们不必问lineName 指向哪里,或者那里是否有任何内存,或者我们是否有权写入它,或者它是否被用于其他用途。相反,我们实际上应该简单地初始化 lineName!我们应该明确指出我们确实拥有并且我们允许写入并且被用于任何事情的内存否则!

有几种方法可以做到这一点。最简单的方法是为lineName 使用数组,而不是指针:

char lineName[20];

或者,如果我们决定使用指针,我们可以调用malloc

char *lineName = malloc(20);

但是,如果我们这样做,我们必须检查以确保 malloc 成功:

if(lineName == NULL) {
    fprintf(stderr, "out of memory!\n");
    exit(1);
}

如果您进行其中任何一项更改,您的程序都会运行。

...嗯,实际上,我们仍然处于您的程序似乎可以工作的情况,即使它还有另一个非常严重的潜伏问题。我们为 lineName 分配了 20 个字符,这给了我们 19 个实际字符,加上结尾的 '\0'。但是我们不知道用户要输入什么。如果用户键入 20 个或更多字符怎么办?这将导致scanflineName 写入超过20 个字符,超出lineName 的内存允许保存的内容的末尾,我们又回到了我们不写入内存的情况' t 拥有,并且可能正在用于其他用途。

一种解决方案是使lineName 更大——将其声明为char lineName[100],或调用malloc(100)。但这只是解决了问题——现在我们不得不担心用户输入 100 个或更多字符的可能性(可能更小)。所以接下来要做的是告诉scanf 不要给lineName 写比我们安排的要多的东西。这实际上很简单。如果lineName 仍然设置为容纳 20 个字符,只需调用

scanf("%19s (%d)", lineName, &stationNo)

格式说明符 %19s 告诉 scanf,它只允许读取和存储最多 19 个字符的字符串,留一个字节用于终止 '\0',它还将添加。


现在,我已经在这里说了很多,但我意识到我实际上还没有回答这个问题:当您做出看似微不足道、看似无关的更改时,为什么您的程序会从工作变为崩溃。这最终成为一个很难令人满意地回答的问题。回到我开始回答这个问题时的类比,这就像问为什么你能够毫无问题地把车开到商店,但是当你试图开车去奶奶家时,车轮掉了下来,你撞到了一条沟。可能有上百万种可能的因素在起作用,但没有一个能改变一个根本的事实,即在没有固定车轮的情况下驾驶汽车是一个疯狂的想法,根本不能保证会奏效。

就您而言,您所说的变量——lineNamestationNoi,然后是b——都是局部变量,通常分配在堆栈上。现在,堆栈的一个特点是它可以用于各种东西,并且在使用之间它永远不会被清除。因此,如果您有一个未初始化的局部变量,它最终包含的特定随机位取决于上次使用该堆栈的任何内容。如果您稍微更改程序以便调用不同的函数,那么这些不同的函数可能会在堆栈上留下不同的随机值。或者,如果您更改函数以分配不同的局部变量,编译器可能会将它们放置在堆栈上的不同位置,这意味着它们最终会从上次那里获取不同的随机值。

无论如何,不​​知何故,在您的程序的第一个版本中,lineName 最终包含一个随机值,该值对应于一个指向实际内存的指针,您可以通过写入来逃避。但是,当您添加第四个变量 b 时,事情发生了翻天覆地的变化,以至于 lineName 最终指向了不存在或您无权写入的内存,并且您的程序崩溃了。

有意义吗?


现在,还有一件事,如果你还在我身边的话。如果你停下来想一想,这整件事可能有点令人不安。你有一个程序(你的第一个程序)看起来工作得很好,但实际上有一个相当可怕的错误。它写入随机的、未分配的内存。但是当你编译它时,你没有收到致命的错误消息,当你运行它时,也没有任何迹象表明有任何问题。这是怎么回事?

正如几个 cmets 所提到的,答案涉及我们所谓的未定义行为

原来存在三种 C 程序,我们可以称之为好的、坏的和丑陋的。

  • 好的程序有正确的原因。他们不违反任何规则,他们不做任何违法的事情。当您编译它们时,它们不会收到任何警告或错误消息,而当您运行它们时,它们就可以正常工作。

  • 坏程序破坏了某些规则,编译器捕捉到这一点,并发出致命错误消息,并拒绝生成一个坏程序供您尝试运行。

  • 但也有一些丑陋的程序,它们参与未定义的行为。这些是违反一组不同规则的规则,由于各种原因,编译器没有有义务抱怨这些规则。 (实际上,编译器可能甚至可能无法检测到它们)。参与未定义行为的程序可以做任何事情

让我们再考虑一下最后一点。当您编写使用未定义行为的程序时,编译器没有义务生成错误消息,因此您可能没有意识到您已经完成了它。并且程序可以做任何事情,包括你期望的工作。但是,既然它被允许做任何事情,它可能会在明天停止工作,似乎完全没有理由,要么是因为你对它做了一些看似无害的改变,要么只是因为你不在身边保护它,因为它悄悄地运行amok 并删除所有客户的数据。

那么你应该怎么做呢?

一件事是尽可能使用现代编译器,并打开它的警告,并注意它们。 (好的编译器甚至有一个叫做“将警告视为错误”的选项,关心正确程序的程序员通常会打开这个选项。)尽管,正如我所说的,它们不是必需的,编译器在如果您要求他们检测未定义的行为并警告您。

另外一件事,如果你要做大量的 C 编程,那就是注意学习这门语言,你可以做什么,你不应该做什么。重点编写出于正确原因工作的程序。不要满足于只在今天看来有效的程序。如果有人指出你依赖于未定义的行为,不要说,“但我的程序有效——我为什么要关心?” (你没这么说,但有些人这么说。)

【讨论】:

  • 感谢您花时间和精力写这个答案!
  • 优秀。唯一可能更好的方法是解释与 EOF 比较有什么问题......
  • @R.I.P.Seb 甚至没有注意到!但它最终会成为一个分散注意力的切线,偏离已经太长的答案的主旨。我想说,如果您愿意,请发表您自己的答案来说明这一点。
  • 哇...这个答案对我来说比我在互联网上找到的任何其他答案都更清楚。感谢您的精彩回答!
猜你喜欢
  • 2018-12-02
  • 1970-01-01
  • 2017-07-13
  • 2018-05-15
  • 2011-09-12
  • 1970-01-01
  • 2012-11-19
  • 2013-04-27
  • 2012-03-03
相关资源
最近更新 更多