【问题标题】:Learning C, would appreciate input on why this solution works学习 C,将不胜感激有关此解决方案为何有效的意见
【发布时间】:2010-06-09 17:18:15
【问题描述】:

这实际上是我用 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) 换成 exit(0),那么一切都会按预期进行。

这给我留下了两个问题:为什么原来的程序是错误的?我怎样才能比我更好地修复它?

关于需要使用exit而不是return的问题让我特别困惑。

【问题讨论】:

    标签: c segmentation-fault


    【解决方案1】:

    这是由于 scanf 语句而发生的。

    看看 cmd 是如何指向 NULL 的。运行scanf时,写入cmd的地址为NULL,从而产生segfault。

    解决办法是为cmd创建一个缓冲区,如:

    char cmd[20];
    

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

    欢迎来到 C.

    编辑:另外,请注意,您的信用卡、借记卡和服务费功能不会像您写的那样按预期工作。这是因为参数是passed by value,而不是引用。这意味着在方法返回后,任何更改都将被丢弃。如果您希望他们修改您提供的参数,请尝试将方法更改为:

    无效信用(unsigned int *acct, int *amount)

    然后像这样称呼他们:

    credit(&acct, &amt);
    

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

    【讨论】:

    • 喜欢这个答案的最后一句话;)
    • 值得注意的是下一个问题“那么我如何在没有任意限制输入长度的情况下管理这个问题?” 这个问题已经回答了好几个SO上的时间。简短的回答是使用fgetsgetline,可以在stackoverflow.com/questions/2532425/… 中找到一些关于这些东西的工作人员的注释。
    • 啊,有道理。 dmckee 抢占了我的下一个问题。 :) 值得一提的是,贷记、借记和服务费函数实际上用于使用传入的值来操作帐户结构(部分代码)。传入的值不会被修改。我想声明参数 const 是个好主意?
    • 在这种情况下,是的。 Const 可能是一个很好的安全网。
    【解决方案2】:

    你没有为 cmd 分配内存,所以它是NULL

    尝试用一些空格声明它:

    char cmd[1000];
    

    【讨论】:

      【解决方案3】:

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

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

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

      【讨论】:

      • 这就是返回值的用途!
      【解决方案4】:

      这个:

      char    *cmd = NULL; 
      


      应该是:

      char cmd[100]; 
      



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

      【讨论】:

      • 第二个是 c++ 在这种情况下,他应该只使用 std::string
      • 我认为 C++ cmets 有点令人困惑,但是 +1,因为您更正了它们。 :)
      【解决方案5】:

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

      一个初步的解决方案会为 cmd 指向:

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

      但这是一个非常危险的举动,因为如果输入比预期长并且 scanf 尝试写入 cmd[30] 及以后,您仍然可能会遇到段错误。

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

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

      【讨论】:

        【解决方案6】:

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

        char    *cmd = NULL; 
        

        scanf() 不会为字符串分配空间 - 您需要为字符串分配空间。

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

        因为scanf() 正在尝试将其输出写入未分配的空间,所以您遇到了分段错误。

        【讨论】:

          【解决方案7】:

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

          【讨论】:

            【解决方案8】:

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

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

            您可以这样做scanf("%s %u %i", &amp;cmd, &amp;acct, &amp;amount) 的原因是C 不是类型安全的。当你给它&amp;cmd时,你给它一个char**;但是,它很高兴将其视为char*。因此,它写了字节over cmd,所以如果你传入字符串exitcmd 可能(如果它是四个字节宽并且具有适当的字节序)等于@987654347 @ (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");
            }
            

            【讨论】:

            • 出于好奇,为什么要投反对票?是不是答案有问题,或者我应该改进的地方?
            【解决方案9】:

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

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

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

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2021-05-06
              • 2019-03-14
              • 1970-01-01
              • 1970-01-01
              • 2019-06-30
              • 2015-05-13
              • 2015-09-12
              • 1970-01-01
              相关资源
              最近更新 更多