学习C,会欣赏有关此解决方案工作原理的信息

时间:2010-06-09 17:18:16

标签: c segmentation-fault

这是我用C编写的第一件事,所以请随意指出它的所有缺陷。 :)我的问题是,如果我按照我认为最干净的方式编写程序,我会得到一个破碎的程序:

#include <sys/queue.h> 

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

/* Removed prototypes and non related code for brevity */

int
main()
{
    char    *cmd = NULL; 
    unsigned int acct = 0; 
    int amount = 0; 
    int done = 0; 

    while (done==0) {
        scanf ("%s %u %i", cmd, &acct, &amount);

        if (strcmp (cmd, "exit") == 0)
            done = 1;
        else if ((strcmp (cmd, "dep") == 0) || (strcmp (cmd, "deb") == 0))
            debit (acct, amount);
        else if ((strcmp (cmd, "wd") == 0) || (strcmp (cmd, "cred") == 0))
            credit (acct, amount);
        else if (strcmp (cmd, "fee") == 0)
            service_fee(acct, amount);
        else
            printf("Invalid input!\n");
    }
    return(0);
}

void
credit(unsigned int acct, int amount)
{
}

void
debit(unsigned int acct, int amount)
{
}

void
service_fee(unsigned int acct, int amount)
{
}

就目前而言,上面的代码在编译时没有产生错误,但在运行时会给我一个段错误。我可以通过在调用scanf和strcmp时通过引用更改程序来传递cmd来解决这个问题。段错误消失,在编译时每次使用strcmp时都会被警告取代。尽管有警告,受影响的代码仍可正常工作。

  

警告:从不兼容的指针类型

传递'strcmp'的arg 1

作为一个额外的好处,修改scanf和strcmp调用允许程序进展得足够远以执行return(0),此时事件会与Abort陷阱崩溃。如果我换出return(0)退出(0),那么一切都按预期工作。

这让我有两个问题:为什么原始程序错了?我怎样才能比我更好地修复它?

关于需要使用退出而不是返回的一点让我特别困惑。

9 个答案:

答案 0 :(得分:11)

由于scanf语句,这种情况正在发生。

看看cmd是如何指向NULL的。当scanf运行时,它会写入cmd的地址,该地址为NULL,从而生成段错误。

解决方案是为cmd创建一个缓冲区,例如:

char cmd[20];

现在,您的缓冲区可以容纳20个字符。但是,如果用户输入超过20个字符,您现在需要担心缓冲区溢出。

欢迎来到C.

编辑:此外,请注意您的信用卡,借记卡和服务费功能将无法按预期编写。这是因为参数是passed by value,而不是参考。这意味着在方法返回后,将丢弃任何更改。如果您希望它们修改您提供的参数,请尝试将方法更改为:

void credit(unsigned int * acct,int * amount)

然后称他们为:

credit(&acct, &amt);

这样做会通过引用传递参数,这意味着您在credit函数内所做的任何更改都会影响参数,即使在函数返回后也是如此。

答案 1 :(得分:7)

您没有为cmd分配内存,因此它是NULL

尝试用一些空格声明:

char cmd[1000];

答案 2 :(得分:5)

正如其他人所指出的那样,你还没有为scanf分配任何内容。但是你也应该测试scanf的返回值:

if ( scanf ("%s %u %i", cmd, &acct, &amount) != 3 ) {
   // do some error handling
}

scanf函数返回成功转换的次数,因此如果有人在您希望能够检测并处理它的整数时输入XXXX。但坦率地说,使用scanf()的用户界面代码永远不会成为这种事情的证明。 scanf()实际上是用于读取格式化文件,而不是来自人类的随机输入。

答案 3 :(得分:4)

这:

char    *cmd = NULL; 


应该是:

char cmd[100]; 



请注意: 您应该确保用户在cmd中输入的字符串的长度小于 100n

答案 4 :(得分:2)

在您的示例中,scanf()正在传递空指针。

char    *cmd = NULL; 

scanf()不会为字符串分配空间 - 你需要为字符串分配一些地方。

char   cmd[80];
...
scanf ("%s",cmd);

您的分段错误是因为scanf()正在尝试将其输出写入未分配的空间。

答案 5 :(得分:2)

cmd初始化为空指针,该指针从不指向任何内存。在尝试写入cmd指向的内容之前,scanf未检查cmd是否有效。

初步解决方案为cmd创建了一些空间以指向:

char cmd[30]; /* DANGEROUS! */

但这是非常危险的举动,因为如果输入超出预期并且scanf尝试写入cmd [30]及更高版本,您仍可能会遇到段错误。

因此scanf被认为是不安全的,不应在生产代码中使用。更安全的替代方法包括使用fgets来读取输入行,使用sscanf来处理它。

遗憾的是,如果没有在程序中引入缓冲区溢出的可能性,C I / O很难做到。您总是需要考虑可用的内存量以及是否足以存储您可以接收的最长输入。您还需要检查大多数I / O函数的返回值是否有错误。

答案 6 :(得分:1)

其他人已经指出了你的程序中的错误,但为了更好地理解指针,因为你刚刚开始学习C,请在SO处查看question

答案 7 :(得分:1)

您的基本问题是您没有为字符串分配内存。在C中,您负责所有内存管理。如果在堆栈上声明变量,这很容易。有了指针,它就有点困难了。由于您有char* str = NULL行,当您尝试scanf时,您将字节写入NULL,这是非法的。 %s说明符所做的是写入str指向的内容;它不能更改str,因为参数是按值传递的。这就是为什么你必须传递&acct而不仅仅是acct

那么你如何解决它?您需要提供读入字符串可以存在的内存。像char str[5] = ""这样的东西。这使得str成为一个五元素字符数组,大到足以保持“退出”及其终止零字节。 (阵列在最轻微的挑衅下会变成指针,所以我们在这方面很好。)然而,这很危险。如果用户输入字符串malicious,您将把"malic"写入str,将"icious\0"的字节写入内存之后的任何内容。这是一个缓冲区溢出,是一个经典的bug。在这里修复它的最简单方法是要求用户输入最多N个字母的命令,其中N是你拥有的最长命令;在这种情况下,N = 4.然后,您可以告诉scanf最多读取四个字符:scanf("%4s %u %i", cmd, &acct, &amt)%4s表示“最多读入 四个字符”,因此您无法搞砸其他内存。但请注意,如果用户输入malformed 3 4,您将无法找到3和4,因为您将查看ormed

你可以做scanf("%s %u %i", &cmd, &acct, &amount)的原因是C不是类型安全的。当你给它&cmd时,你给它一个char**;但是,很高兴将其视为char*。因此,它写了 over cmd字节,所以如果你传入字符串exitcmd可能(如果它是四个字节宽并具有适当的字节顺序) )等于0x65786974(0x65 = e,0x78 = x,0x69 = i,0x74 = t)。然后是零字节或传入的任何其他字节,您将开始在随机存储器上写入。但是,如果您也在strcmp更改了它,那么它str视为字符串,并且所有内容都将保持一致。至于为什么return 0;失败但exit(0)有效,我不确定,但我猜测:你可能一直在写main的返回地址。它也存储在堆栈中,如果它恰好出现在堆栈布局中的cmd之后,那么您可能会将其归零或在其上涂鸦。现在,exit必须手动清理,跳转到正确的位置等。但是,如果(我认为是这种情况,虽然我不确定)main的行为与任何其他功能一样, 它的 return跳转到存储为返回地址的堆栈空间(这可能是某种类型的清理例程)。但是,既然你已经潦草地写了一遍,就会中止。

现在,您可以进行其他一些小改进。首先,由于您将done视为布尔值,因此您应该循环while (!done) { ... }。其次,当前设置要求您编写exit 1 1以退出程序,即使1 1位不是必需的。第三,你应该检查你是否已经成功阅读了所有三个参数,这样就不会出现错误/不一致;例如,如果您不解决此问题,那么输入

deb 1 2
deb 3 a

拨打debit(1,2)debit(3,2),同时仍然将a留在输入中以引导您。最后,你应该在EOF上彻底退出,而不是永远循环做你做的最后一件事。如果我们把它放在一起,我们得到以下代码:

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

void credit(unsigned int acct, int amount);
void debit(unsigned int acct, int amount);
void service_fee(unsigned int acct, int amount);

int main() {
  char         cmd[5] = ""; 
  unsigned int acct   = 0; 
  int          amount = 0; 
  int          done   = 0; 

  while (!done) {
    if (feof(stdin)) {
      done = 1;
    } else {
      if (scanf("%4s", cmd, &acct) != 1) {
        fprintf(stderr, "Could not read the command!\n");
        scanf(" %*s "); /* Get rid of the rest of the line */
        continue;
      }

      if (strcmp(cmd, "exit") == 0) {
        done = 1;
      } else {
        if (scanf(" %u %i", &acct, &amount) != 2) {
          fprintf(stderr, "Could not read the arguments!\n");
          scanf(" %*s "); /* Get rid of the rest of the line */
          continue;
        }

        if ((strcmp(cmd, "dep") == 0) || (strcmp(cmd, "deb") == 0))
          debit(acct, amount);
        else if ((strcmp(cmd, "wd") == 0) || (strcmp(cmd, "cred") == 0))
          credit(acct, amount);
        else if (strcmp(cmd, "fee") == 0)
          service_fee(acct, amount);
        else
          fprintf(stderr, "Invalid input!\n");
      }
    }
    /* Cleanup code ... */
  }

  return 0;
}

/* Dummy function bodies */

void credit(unsigned int acct, int amount) {
  printf("credit(%u, %d)\n", acct, amount);
}

void debit(unsigned int acct, int amount) {
  printf("debit(%u, %d)\n", acct, amount);
}

void service_fee(unsigned int acct, int amount) {
  printf("service_fee(%u, %d)\n", acct, amount);
}

请注意,如果没有“清理代码”,您可以将done的所有用途替换为break并删除done的声明,从而提供更好的循环

while (1) {
  if (feof(stdin)) break;

  if (scanf("%4s", cmd, &acct) != 1) {
    fprintf(stderr, "Could not read the command!\n");
    scanf(" %*s "); /* Get rid of the rest of the line */
    continue;
  }

  if (strcmp(cmd, "exit") == 0) break;

  if (scanf(" %u %i", &acct, &amount) != 2) {
    fprintf(stderr, "Could not read the arguments!\n");
    scanf(" %*s "); /* Get rid of the rest of the line */
    continue;
  }

  if ((strcmp(cmd, "dep") == 0) || (strcmp(cmd, "deb") == 0))
    debit(acct, amount);
  else if ((strcmp(cmd, "wd") == 0) || (strcmp(cmd, "cred") == 0))
    credit(acct, amount);
  else if (strcmp(cmd, "fee") == 0)
    service_fee(acct, amount);
  else
    fprintf(stderr, "Invalid input!\n");
}

答案 8 :(得分:0)

为了完全理解这里发生了什么,你需要了解一些关于C指针的基础知识。如果你真的是C的新手,我建议你看看这里:

http://www.cprogramming.com/tutorial.html#ctutorial

此处详细介绍了段错误的最常见原因:

http://www.cprogramming.com/debugging/segfaults.html