你的基本问题是你没有为字符串分配内存。在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
上写入字节,所以如果你传入字符串
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 ");
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 ");
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");
}
}
}
return 0;
}
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 ");
continue;
}
if (strcmp(cmd, "exit") == 0) break;
if (scanf(" %u %i", &acct, &amount) != 2) {
fprintf(stderr, "Could not read the arguments!\n");
scanf(" %*s ");
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");
}
fgets
或getline
,这些东西的工作原理可以在https://dev59.com/aHE85IYBdhLWcg3w8IbK#2532450等许多地方找到一些说明。 - dmckee --- ex-moderator kitten