这是我用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),那么一切都按预期工作。
这让我有两个问题:为什么原始程序错了?我怎样才能比我更好地修复它?
关于需要使用退出而不是返回的一点让我特别困惑。
答案 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
中输入的字符串的长度小于 100
或n
答案 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
来处理它。
答案 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
字节,所以如果你传入字符串exit
,cmd
可能(如果它是四个字节宽并具有适当的字节顺序) )等于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
此处详细介绍了段错误的最常见原因: