添加额外的int时,C程序崩溃

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

#include  #include  #include  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  #include  #include  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; } 

程序将以相同的输入崩溃。 谁能告诉我为什么?

你说你的第一个程序“有效”,但它只能偶然起作用。 它就像一辆汽车在路上行驶,前轮上没有lugnuts,只有一些奇迹他们没有掉下来 – 但是。

你说

 char *lineName; 

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

接下来你说

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

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

使用未初始化指针的情况有点难以思考,因为一如既往,使用指针我们必须区分指针的值而不是指针指向 的内存中的数据 。 早些时候我提到说过int i并询问我的价值是什么。 现在, i会有一些位模式 – 它可能是0,或1,或-23,或8675309。

类似地, lineName会有一些位模式 – 它可能“指向”内存位置0x00000000,或0xffe01234或0xdeadbeef。 但问题是:那个位置是否有任何内存,我们是否有权写入它,是否还用于其他任何事情? 如果有内存并且我们确实拥有许可并且它没有被用于其他任何东西,那么该程序似乎可以正常工作 – 现在。 但那些是三个相当大的ifs! 如果内存不存在,或者我们没有写入权限,则程序尝试时可能会崩溃。 如果内存被用于其他东西,当我们要求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个或更多字符怎么办? 这会导致scanf写入20个或更多字符到lineName ,超过lineName的内存允许持有的结尾,我们又回到写入我们不拥有的内存的情况,这可能是用于别的东西。

一种解决方案是使lineName更大 – 将其声明为char lineName[100] ,或调用malloc(100) 。 但这只会解决问题 – 现在我们不得不担心用户输入100个或更多字符的可能性(可能更小)。 所以接下来要做的就是告诉scanf不要写更多关于lineName不是我们安排它保存。 这实际上非常简单:如果lineName仍设置为包含20个字符,则只需调用

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

格式说明符%19s告诉scanf它只允许读取和存储长达19个字符的字符串。


现在,我已经在这里说了很多,但我意识到我实际上还没有回答过为什么你的程序从工作变为崩溃的问题,当你做出看似微不足道的,看似无关的变化时。 这最终成为一个难以回答的难题。 回到我开始的比喻,这就像问你为什么能够没有问题地开车到商店没有问题,但是当你试图开车到奶奶的房子时,车轮掉下来你撞到了沟里。 有一百万种可能的因素可能会发挥作用,但它们都没有改变一个潜在的事实,即驾驶一辆没有固定车轮的汽车是一个疯狂的想法,这根本不能保证工作。

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

无论如何,在程序的第一个版本中, lineName最终包含一个随机值,该值对应于指向实际内存的指针,您可以通过写入来逃避。 但是当你添加了第四个变量b ,事情就会移动到足以使lineName最终成为一个指向不存在的内存的指针,或者你没有写入权限,并且你的程序崩溃了。

合理?


现在,还有一件事,如果你还在我身边。 如果你停下来思考,整件事情可能会让人感到不安。 你有一个程序(你的第一个程序)似乎工作得很好,但实际上有一个相当可怕的错误。 它写入随机,未分配的内存。 但是当你编译它时你没有致命的错误消息,当你运行它时没有任何迹象表明有任何不妥。 那是怎么回事?

正如一些评论所提到的那样,答案涉及我们称之为未定义的行为

事实certificate,有三种C程序,我们可以称之为好,坏和丑。

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

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

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

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

那么你应该怎么做呢?

如果可以,有一点是使用现代编译器,并打开警告,并注意它们。 (好的编译器甚至有一个名为“将警告视为错误”的选项,而关心正确程序的程序员通常会启用此选项。)尽管如我所说,他们并不需要,编译器会越来越好如果你问他们,检测未定义的行为并警告你。

然后另一件事,如果你要做很多C编程,就要注意学习语言,你可以做什么,不应该做什么。 重点编写符合正当理由的程序 。 不要满足于似乎只是工作的程序。 如果有人指出你依赖于未定义的行为,不要说,“但我的计划有效 – 我为什么要关心?” (你没有说这个,但有些人这样做。)