学习 C,希望了解此解决方案为何有效的意见
这实际上是我用 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
您没有为 cmd 分配内存,因此它是
NULL
。尝试用一些空格来声明它:
You aren't allocating memory for cmd, so it's
NULL
.Try declaring it with some space:
正如其他人指出的那样,您没有分配任何内容供 scanf 读取。但您还应该测试 scanf 的返回值:
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:
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.
这个:
应该是:
请注意:
您应该确保用户在
cmd
中输入的字符串的长度小于100
或n
This :
Should be:
Please note:
You should ensure that the string the user will input in
cmd
has length less than100
orn
cmd 被初始化为一个空指针,它从不指向任何内存。
scanf
在尝试写入 cmd 指向的内容之前不会检查 cmd 是否有效。相反,初步解决方案为 cmd 指向创建了一些空间:
但这是一个非常危险的举动,因为如果输入比预期长并且 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:
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 usingfgets
to read a line of input andsscanf
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.
在您的示例中,
scanf()
正在传递一个空指针。scanf() 不会为字符串分配空间 - 您需要为字符串分配空间。
您遇到分段错误,因为
scanf()
正在尝试将其输出写入未分配的空间。In your example,
scanf()
is being passed a null pointer.scanf() won't allocate space for the string - you'll need to allocate somewhere for the string to go.
Your getting a segmentation fault because
scanf()
is attempting to write its output to unallocated space.其他人已经指出了您程序中的错误,但是为了更好地理解指针,因为您刚刚开始学习 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.
您的基本问题是您没有为字符串分配内存。在 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
上写入字节,因此如果您传入字符串exit
,cmd
可能(如果它为四个字节宽并具有适当的字节顺序)等于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
位不是必需的。第三,您应该检查是否已成功读取所有三个参数,这样就不会出现错误/不一致;例如,如果您不解决此问题,则输入会调用
debit(1,2)
和debit(3,2)
,同时仍保留输入中的 a
会让你陷入困境。最后,您应该在 EOF 处干净地退出,而不是永远循环执行您所做的最后一件事。如果我们把它们放在一起,我们会得到以下代码:请注意,如果没有“清理代码”,您可以将所有对
done
的使用替换为break
并删除done
声明,提供更好的循环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 toscanf
into it, you write bytes toNULL
, which is illegal. What the%s
specifier does is write into whatstr
points to; it can't changestr
, as parameters are passed by value. This is why you have to pass&acct
instead of justacct
.So how do you fix it? You need to provide memory where the read-in string can live. Something like
char str[5] = ""
. This makesstr
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 stringmalicious
, you're going to write"malic"
intostr
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 tellscanf
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 entersmalformed 3 4
, you won't be able to find the 3 and the 4, since you'll be looking atormed
.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 achar**
; however, it was happy to treat it as achar*
. Thus, it wrote bytes overcmd
, so if you passed in the stringexit
,cmd
might (if it were four bytes wide and had the appropriate endianness) be equal to0x65786974
(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 atstrcmp
too, however, it will also treat the value ofstr
as a string, and everything will be consistent. As for whyreturn 0;
fails butexit(0)
works, I'm not sure, but I have a guess: you may have been writing over the return address ofmain
. That's stored on the stack too, and if it happens to come aftercmd
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, itsreturn
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 loopwhile (!done) { ... }
. Second, the current setup requires you to writeexit 1 1
to exit the program, even though the1 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 inputCalls
debit(1,2)
anddebit(3,2)
, while still leaving thea
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:Note that if there is no "cleanup code", you can replace all your uses of
done
withbreak
and remove the declaration ofdone
, giving the nicer loop为了完全理解这里发生的事情,您需要了解一些有关 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
这是由于 scanf 语句而发生的。
看看 cmd 如何指向 NULL。当scanf运行时,它会写入cmd的地址,该地址为NULL,从而产生段错误。
解决办法是为cmd创建一个缓冲区,如:
现在,你的缓冲区可以容纳20个字符。但是,如果用户输入超过 20 个字符,您现在需要担心缓冲区溢出。
欢迎使用 C。
编辑:另外,请注意,您的贷方、借方和服务费函数将不会按您编写的预期工作。这是因为参数是按值传递,而不是按引用传递。这意味着该方法返回后,任何更改都将被丢弃。如果您希望他们修改您提供的参数,请尝试将方法更改为:
void Credit(unsigned int *acct, int *amount)
然后像这样调用它们:
这样做将通过引用传递参数,这意味着您所做的任何更改即使在函数返回之后,信用函数内部也会影响参数。
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:
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:
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.