学习C语言,想请教为什么这个解决方案可行。

7

这是我第一次用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 时的警告。尽管有警告,受影响的代码仍然可以执行。

警告:从不兼容的指针类型传递arg 1 of 'strcmp'

通过修改 scanf 和 strcmp 调用,程序可以顺利执行到 return(0),此时程序崩溃并显示“Abort trap”。如果将 return(0) 替换为 exit(0),则一切都按预期进行。

这让我有两个问题:原始程序为什么是错误的?我如何更好地修复它?
需要注意的是,需要使用 exit 而不是 return 的部分让我特别困惑。
9个回答

11

这是因为 scanf 语句导致的。

看一下 cmd 指向 NULL。运行 scanf 时,它会写入 cmd 的地址,而此时 cmd 是 NULL,因此会生成段错误。

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

char cmd[20];

现在,你的缓冲区可以容纳20个字符。然而,如果用户输入超过20个字符,你现在需要担心缓冲区溢出的问题。
欢迎来到C语言。
编辑:此外,请注意,由于你编写的参数是按值传递(passed by value),所以你的信用、借记和服务费函数将无法按预期工作。这意味着在方法返回后,任何更改都将被丢弃。如果你想让它们修改你给出的参数,请尝试将方法更改为:
void credit(unsigned int *acct, int *amount)
然后像这样调用它们:
credit(&acct, &amt);

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

2
喜欢这个答案中的最后一句话 ;) - rubenvb
2
值得注意的是,下一个问题“那么我如何在没有输入长度限制的情况下进行管理?”已经在SO上多次得到回答。简短的答案是使用fgetsgetline,这些东西的工作原理可以在https://dev59.com/aHE85IYBdhLWcg3w8IbK#2532450等许多地方找到一些说明。 - dmckee --- ex-moderator kitten
啊,那样就有意义了。而 dmckee 预先回答了我的下一个问题。 :)值得一提的是,信贷、借记和服务费功能实际上是用来操作一个账户结构体(代码片段的一部分),使用传递进来的值进行操作的。传递进来的值不会被修改。我想声明参数为 const 也是个好主意吧? - Keifer
在这种情况下,是的。使用const可能是一个很好的安全网。 - samoz

7

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

尝试声明一些空间:

char cmd[1000];

5

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

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

scanf函数返回成功转换的数量,因此如果有人在您期望整数时输入了XXXX,则希望您能检测并处理它。但是,实际上使用scanf()的用户界面代码永远无法真正防止这种情况的发生。scanf()实际上是用于读取格式化文件,而不是来自人类用户的随机输入。


这就是返回值的作用! - BobbyShaftoe

4

这是:

char    *cmd = NULL; 


Should be:

char cmd[100]; 

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

第二个是C++,在这种情况下,他应该使用std::string。 - Aif
我觉得C++的注释有点令人困惑,但由于你已经纠正了它们,所以给个赞+1。 :) - BobbyShaftoe

2
在你的示例中,scanf()被传递了一个空指针。
char    *cmd = NULL; 

scanf()不会为字符串分配空间 - 你需要在某处分配空间来存储字符串。

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

你遇到了段错误,这是因为scanf()试图将其输出写入未分配的内存空间。


2

变量cmd被初始化为空指针,永远不会指向任何内存。scanf在尝试写入cmd所指向的内容之前未检查cmd是否有效。

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

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

但是这是一种非常危险的做法,因为如果输入的长度超出了预期范围,scanf 仍然可能会导致段错误,并试图写入 cmd[30] 以及其后面的位置。
因此,scanf 被认为是不安全的,在生产代码中不应使用。更安全的替代方案包括使用 fgets 读取输入行并使用 sscanf 处理它。
遗憾的是,C I/O 很难在不引入缓冲区溢出的可能性的情况下正确处理程序。您必须始终考虑可用内存量是否足够存储最长可能接收到的输入。您还需要检查大多数 I/O 函数的返回值以检测错误。

1
你的基本问题是你没有为字符串分配内存。在C语言中,你需要负责所有的内存管理。如果你在栈上声明变量,这很容易。但是对于指针来说,就有点困难了。由于你有一行代码 char* str = NULL,当你尝试从scanf读取时,你会将字节写入到NULL,这是非法的。而%s格式化符号所做的是将数据写入到str指向的位置;它不能改变str,因为参数是按值传递的。这就是为什么你必须传递&acct而不是只传递acct的原因。
那么如何解决?您需要提供读入字符串所需的内存。类似于char str[5] = ""。这将使str成为一个具有五个元素的字符数组,足以容纳"exit"及其终止零字节。(数组在轻微的挑衅下就会衰减为指针,因此我们在这方面没问题。)然而,这是危险的。如果用户输入字符串malicious,则会将"malic"写入str,并将"icious\0"的字节写入内存中紧随其后的任何位置。这是缓冲区溢出,并且是经典的漏洞。在这里修复它的最简单方法是要求用户输入最多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*。因此,它会在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");
}

出于好奇,为什么要踩我?我的回答有什么问题,或者我需要改进些什么吗? - Antal Spector-Zabusky

1

其他人已经指出了你程序中的错误,但是为了更好地理解指针,因为你刚开始学习C语言,可以看一下这个问题在SO上。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接