学习 C,希望了解此解决方案为何有效的意见

发布于 2024-09-04 13:18:41 字数 1462 浏览 4 评论 0原文

这实际上是我用 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”的参数 1

作为一个额外的好处,修改 scanf 和 strcmp 调用允许程序继续执行 return(0),此时程序会因 Abort 陷阱而崩溃。如果我将 return(0) 换成 exit(0) 那么一切都会按预期进行。

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

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

This is literally the first thing I've ever written in C, so please feel free to point out all it's flaws. :) My issue, however is this: if I write the program the way I feel is cleanest, I get a broken program:

#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)
{
}

As it stands, the above generates no errors at compile, but gives me a segfault when ran. I can fix this by changing the program to pass cmd by reference when calling scanf and strcmp. The segfault goes away and is replaced by warnings for each use of strcmp at compile time. Despite the warnings, the affected code works.

warning: passing arg 1 of 'strcmp' from incompatible pointer type

As an added bonus, modifying the scanf and strcmp calls allows the program to progress far enough to execute return(0), at which point the thing crashes with an Abort trap. If I swap out return(0) for exit(0) then everything works as expected.

This leaves me with two questions: why was the original program wrong? How can I fix it better than I have?

The bit about needing to use exit instead of return has me especially baffled.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

沉溺在你眼里的海 2024-09-11 13:18:42

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

尝试用一些空格来声明它:

char cmd[1000];

You aren't allocating memory for cmd, so it's NULL.

Try declaring it with some space:

char cmd[1000];
深巷少女 2024-09-11 13:18:42

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

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

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

As others have pointed out, you haven't allocated anything for scanf to read into. But you should also test the return value of scanf:

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

The scanf function returns the number of succesful conversions, so if someone types in XXXX when you expect an integer you want to be able to detect and deal with it. But frankly, user interface code that uses scanf() is never really going to be proof against this sort of thing. scanf() was actually intended for reading formatted files, not random input from humans.

静若繁花 2024-09-11 13:18:42

这个:

char    *cmd = NULL; 

应该是:

char cmd[100]; 

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

This :

char    *cmd = NULL; 

Should be:

char cmd[100]; 

Please note:
You should ensure that the string the user will input in cmd has length less than 100 or n

心凉怎暖 2024-09-11 13:18:42

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

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

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

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

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

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

cmd is initialised to a null pointer which never points at any memory. scanf doesn't check that cmd is valid before trying to write to what cmd points to.

A preliminary solution instead creates some space for cmd to point to:

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

but this is a very dangerous move because you may still get segfaults if the input is longer than expected and scanf tries to write to cmd[30] and beyond.

For this reason scanf is considered unsafe and should not be used in production code. Safer alternatives include using fgets to read a line of input and sscanf to process it.

Sadly, C I/O is very difficult to get right without introducing the possibility of buffer overflows into your program. You always need to be thinking about how much memory you have available and whether it will be enough to store the longest possible input you could receive. You also need to check the return values of most I/O functions for errors.

春夜浅 2024-09-11 13:18:42

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

char    *cmd = NULL; 

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

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

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

In your example, scanf() is being passed a null pointer.

char    *cmd = NULL; 

scanf() won't allocate space for the string - you'll need to allocate somewhere for the string to go.

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

Your getting a segmentation fault because scanf() is attempting to write its output to unallocated space.

若言繁花未落 2024-09-11 13:18:42

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

Others have pointed out the error in your program, but for a better understanding of pointers, since you are just starting to learn C, look at this question at SO.

世俗缘 2024-09-11 13:18:42

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

那么如何解决呢?您需要提供读入字符串可以驻留的内存。类似于 char str[5] = ""。这使得 str 成为一个五元素字符数组,足够大以容纳“exit”及其终止零字节。 (数组在最轻微的刺激下就会衰减为指针,所以我们在这方面做得很好。)然而,这是危险的。如果用户输入字符串 malicious,您将在 str 中写入 "malic" 以及 "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) 有效,我不确定,但我有一个猜测:你可能一直在写的返回地址主要。它也存储在堆栈中,如果它恰好出现在堆栈布局中的 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");
}

Your basic problem is that you haven't allocated memory for your string. In C, you are responsible for all memory management. If you declare variables on the stack, this is easy. With pointers, it's a little more difficult. Since you have the line char* str = NULL, when you attempt to scanf into it, you write bytes to NULL, which is illegal. What the %s specifier does is write into what str points to; it can't change str, as parameters are passed by value. This is why you have to pass &acct instead of just acct.

So how do you fix it? You need to provide memory where the read-in string can live. Something like char str[5] = "". This makes str a five-element character array, big enough to hold "exit" and its terminating zero byte. (Arrays decay into pointers at the slightest provocation, so we're fine on that front.) However, this is dangerous. If the user enters the string malicious, you're going to write "malic" into str and the bytes for "icious\0" into whatever comes after that at memory. This is a buffer overflow, and is a classic bug. The simplest way to fix it here is to require the user to enter a command of at most N letters, where N is the longest command you have; in this case, N = 4. Then you can tell scanf to read in at most four characters: scanf("%4s %u %i", cmd, &acct, &amt). The %4s says "read in at most four characters", so you can't screw up other memory. However, note that if the user enters malformed 3 4, you won't be able to find the 3 and the 4, since you'll be looking at ormed.

The reason you could do scanf("%s %u %i", &cmd, &acct, &amount) is that C is not type-safe. When you gave it &cmd, you gave it a char**; however, it was happy to treat it as a char*. Thus, it wrote bytes over cmd, so if you passed in the string exit, cmd might (if it were four bytes wide and had the appropriate endianness) be equal to 0x65786974 (0x65 = e, 0x78 = x, 0x69 = i, 0x74 = t). And then the zero byte, or any other bytes you passed in, you would start to write over random memory. If you change it at strcmp too, however, it will also treat the value of str as a string, and everything will be consistent. As for why return 0; fails but exit(0) works, I'm not sure, but I have a guess: you may have been writing over the return address of main. That's stored on the stack too, and if it happens to come after cmd in the stack layout, then you might be zeroing it or scribbling on it. Now, exit must do its cleanup manually, jumping to the right locations, etc. However, if (as I think is the case, although I'm not sure) main behaves like any other function, its return jumps to the space on the stack stored as the return address (which is probably a cleanup routine of some sort). However, since you've scribbled over that, you get an abort.

Now, there are a couple of other small improvements you could make. First, since you're treating done as a boolean, you ought to loop while (!done) { ... }. Second, the current setup requires you to write exit 1 1 to exit the program, even though the 1 1 bit shouldn't be necessary. Third, you should check to see if you have successfully read all three arguments, so you don't get errors/inconsistencies; for instance, if you don't fix this, then the input

deb 1 2
deb 3 a

Calls debit(1,2) and debit(3,2), while still leaving the a in the input to trip you up. Finally, you should exit cleanly on EOF, rather than looping forever doing the last thing you did. If we put this together, we get the following code:

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

Note that if there is no "cleanup code", you can replace all your uses of done with break and remove the declaration of done, giving the nicer loop

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");
}
缪败 2024-09-11 13:18:42

为了完全理解这里发生的事情,您需要了解一些有关 C 指针的基础知识。如果您真的是 C 语言新手,我建议您看一下这里:

​​http://www.cprogramming.com/tutorial.html#ctutorial" rel="nofollow noreferrer">http://www.cprogramming.com/tutorial.html#ctutorial" cprogramming.com/tutorial.html#ctutorial

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

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

In order to fully understand what is going on here you need to understand some basics about C pointers. I suggest you take a look here if you are really that new to C:

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

The most common cause of segfaults are detailed here:

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

但可醉心 2024-09-11 13:18:41

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

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

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

char cmd[20];

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

欢迎使用 C。

编辑:另外,请注意,您的贷方、借方和服务费函数将不会按您编写的预期工作。这是因为参数是按值传递,而不是按引用传递。这意味着该方法返回后,任何更改都将被丢弃。如果您希望他们修改您提供的参数,请尝试将方法更改为:

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

然后像这样调用它们:

credit(&acct, &amt);

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

This is happening because of the scanf statement.

Look how cmd is pointing to NULL. When scanf is run, it writes to the address of cmd, which is NULL, thus generating a segfault.

The solution is to create a buffer for cmd, such as:

char cmd[20];

Now, your buffer can hold 20 characters. However, you now need to worry about buffer overflows if a user enters more than 20 characters.

Welcome to C.

EDIT: Also, note that your credit, debit, and service fee functions won't work as expected as you have them written. This is because the parameters are passed by value, not by reference. This means that after the method returns, any changes will be discarded. If you want them to modify the arguments you give, try changing the methods to:

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

And then call them like:

credit(&acct, &amt);

Doing that will pass the parameters by reference, meaning that any changes you make inside the credit function will affect the parameters, even after the function returns.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文