除了上述 cmets 中包含的信息之外,您最大的问题是分配的内存字节数太少。为字符串分配存储空间时,必须分配 length + 1 字节以为 nul-terminating 字符提供空间。有了上面的size1,您只是为每个字符分配了足够的存储空间,但没有为 nul-terminating 字符分配空间。您必须分配 size + 1 字节。
此外,您必须通过检查返回来验证每个用户输入。如果您未能检查返回,如果用户不小心为您的任何数字输入输入了不正确的字符而不是数字,则您正在邀请 Undefined Behavior。要在使用 scanf() 时验证每个输入,您必须检查返回是否等于指定的转换次数,例如
void readItem (Item *ptr)
{
int size;
printf ("\nSpecify the amount of letters of the product name: ");
if (scanf("%d", &size) != 1) { /* validate every user input */
fputs ("error: invalid integer input.\n", stderr);
exit (EXIT_FAILURE);
}
...
这适用于所有输入。您还应该考虑将readItem 的返回类型从void 更改为可以指示所有输入成功或失败的内容。 (一个简单的int 并返回1 或0 有效)。这样,您可以选择处理错误并恢复而不是退出,而不是在输入或匹配失败时调用 exit。
此外,在使用scanf() 输入后,您应该清空stdin 中剩余的所有字符,直到遇到'\n' 或EOF。这是使用scanf() 进行用户输入的主要缺陷之一1。这也是为什么建议使用面向行的 函数(例如fgets() 或getline())读取用户输入的原因。为了在使用scanf() 时处理这种情况,可以在每次输入后调用一个简单的辅助函数来清空stdin,例如
/* function to input stdin to end of line or EOF */
void empty_stdin (void)
{
int c = getchar();
while (c != '\n' && c != EOF)
c = getchar();
}
现在您可以确保stdin 中没有多余的字符会导致您的下一次输入失败。完成size的输入,你会这样做:
printf ("\nSpecify the amount of letters of the product name: ");
if (scanf("%d", &size) != 1) { /* validate every user input */
fputs ("error: invalid integer input.\n", stderr);
exit (EXIT_FAILURE);
}
empty_stdin(); /* empty all extraneous characters from stdin */
...
(注意:通常您不会要求用户输入下一个输入的字符数——而是使用足够大的临时缓冲区来存储输入并调用strlen() on缓冲区来获取字符数)
size 包含字符数(在您的情况下 - 字符串的长度),您必须分配 size + 1 字节以提供空间来存储 nul-terminating 字符( '\0'——或者只是简单的0)。你需要:
...
ptr->itemName = malloc (size + 1); /* you must allocate +1 chars for '\0' */
if (ptr->itemName == NULL) { /* validate every allocation */
perror ("malloc-ptr->itemName");
exit (EXIT_FAILURE);
}
...
(注意:失败时malloc 设置errno 允许您使用perror() 输出错误)
完成你的readItem() 函数,你会这样做:
...
printf ("\nSpecify the name of the product (MAX %d letters): ", size);
if (scanf ("%s", ptr->itemName) != 1) {
fputs ("error: read error ptr->itemName\n", stderr);
exit (EXIT_FAILURE);
}
empty_stdin(); /* empty all extraneous characters from stdin */
printf ("\nHow many products do the company have? ");
if (scanf ("%d", &ptr->quantity) != 1) {
fputs ("error: invalid integer input.\n", stderr);
exit (EXIT_FAILURE);
}
empty_stdin(); /* empty all extraneous characters from stdin */
printf ("\nHow expensive is the product? ");
if (scanf ("%f", &(ptr->price)) != 1) {
fputs ("error: invalid float input.\n", stderr);
exit (EXIT_FAILURE);
}
empty_stdin(); /* empty all extraneous characters from stdin */
ptr->amount = ptr->price * ptr->quantity;
}
您可能想要解决的另一个问题是您在指针值的声明中放置了'*'。通常'*' 与变量一起使用,而不是类型。为什么?
int* a, b, c;
不声明三个指向int 的指针,而是声明一个整数指针和两个整数。将'*' 与变量放在一起可以清楚地表明这一点,例如
int *a, b, c;
现在把它放在一起写一个简短的printItem()函数,你可以这样做:
#include <stdio.h>
#include <stdlib.h>
typedef struct Item {
char *itemName;
int quantity;
float price;
float amount;
} Item;
/* function to input stdin to end of line or EOF */
void empty_stdin (void)
{
int c = getchar();
while (c != '\n' && c != EOF)
c = getchar();
}
void readItem (Item *ptr)
{
int size;
printf ("\nSpecify the amount of letters of the product name: ");
if (scanf("%d", &size) != 1) { /* validate every user input */
fputs ("error: invalid integer input.\n", stderr);
exit (EXIT_FAILURE);
}
empty_stdin(); /* empty all extraneous characters from stdin */
ptr->itemName = malloc (size + 1); /* you must allocate +1 chars for '\0' */
if (ptr->itemName == NULL) { /* validate every allocation */
perror ("malloc-ptr->itemName");
exit (EXIT_FAILURE);
}
printf ("\nSpecify the name of the product (MAX %d letters): ", size);
if (scanf ("%s", ptr->itemName) != 1) {
fputs ("error: read error ptr->itemName\n", stderr);
exit (EXIT_FAILURE);
}
empty_stdin(); /* empty all extraneous characters from stdin */
printf ("\nHow many products do the company have? ");
if (scanf ("%d", &ptr->quantity) != 1) {
fputs ("error: invalid integer input.\n", stderr);
exit (EXIT_FAILURE);
}
empty_stdin(); /* empty all extraneous characters from stdin */
printf ("\nHow expensive is the product? ");
if (scanf ("%f", &(ptr->price)) != 1) {
fputs ("error: invalid float input.\n", stderr);
exit (EXIT_FAILURE);
}
empty_stdin(); /* empty all extraneous characters from stdin */
ptr->amount = ptr->price * ptr->quantity;
}
void printItem (Item *pitem)
{
printf ("\nitemName : %s\n"
" quantity : %d\n"
" price : %.2f\n"
" amount : %.2f\n",
pitem->itemName, pitem->quantity, pitem->price, pitem->amount);
}
int main (void) {
Item sample;
Item *p_sample = &sample;
readItem(p_sample);
printItem(p_sample);
free(p_sample->itemName);
}
使用/输出示例
$ ./bin/readitem
Specify the amount of letters of the product name: 9
Specify the name of the product (MAX 9 letters): lollypops
How many products do the company have? 100
How expensive is the product? .79
itemName : lollypops
quantity : 100
price : 0.79
amount : 79.00
内存使用/错误检查
在您编写的任何动态分配内存的代码中,对于分配的任何内存块,您都有 2 个职责:(1)始终保留指向起始地址的指针内存块,因此,(2) 当不再需要它时可以释放。
您必须使用内存错误检查程序来确保您不会尝试访问内存或写入超出/超出分配块的边界,尝试读取或基于未初始化的值进行条件跳转,最后, 以确认您已释放所有已分配的内存。
对于 Linux,valgrind 是正常的选择。每个平台都有类似的内存检查器。它们都易于使用,只需通过它运行您的程序即可。
$ valgrind ./bin/readitem
==9619== Memcheck, a memory error detector
==9619== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==9619== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==9619== Command: ./bin/readitem
==9619==
Specify the amount of letters of the product name: 9
Specify the name of the product (MAX 9 letters): lollypops
How many products do the company have? 100
How expensive is the product? .79
itemName : lollypops
quantity : 100
price : 0.79
amount : 79.00
==9619==
==9619== HEAP SUMMARY:
==9619== in use at exit: 0 bytes in 0 blocks
==9619== total heap usage: 3 allocs, 3 frees, 2,058 bytes allocated
==9619==
==9619== All heap blocks were freed -- no leaks are possible
==9619==
==9619== For counts of detected and suppressed errors, rerun with: -v
==9619== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
始终确认您已释放已分配的所有内存并且没有内存错误。
使用 fgets() 进行用户输入
如前所述,scanf() 的用户在用户输入方面存在许多缺陷,除非所有潜在的缺陷都得到解决,否则这些缺陷本质上最多会导致输入例程脆弱。使用临时缓冲区(足够大小的字符数组)来存储使用 fgets() 的用户输入,然后使用 sscanf() 解析所需的任何值,以确保每次都读取完整的输入行,并且没有多余的字符会导致下一次输入失败。除了sscanf()之外,它还开辟了许多不同的方法来从临时缓冲区解析您想要的信息。
使用面向行输入函数的唯一警告是函数读取并在缓冲区中包含'\n'(由用户按Enter生成)他们填满。因此,您只需在获得输入长度时用'\0' 覆盖缓冲区末尾的'\n' 即可。最可靠的方法是使用strcspn()。
将readItem() 的返回类型更改为int 以允许将成功或失败的指示返回到main(),您可以执行类似于以下的操作:
...
#include <string.h>
#define MAXC 1024 /* if you need a constant, #define one (or more) */
...
通过声明一个足够大的常量来设置临时缓冲区的大小以容纳任何预期的用户输入(包括踩在键盘上的猫)。不要吝啬缓冲区大小。 (如果为内存有限的微控制器编程,则相应减少)
如果用户通过在 Linux 上按 Ctrl+d 或在 Windows 上按 Ctrl+z 生成手册 EOF,该函数将返回 EOF。函数返回0成功或1如果遇到任何错误允许您处理main()中的返回(您可以调整是否使用0成功或失败以满足您的需求)
读取、分配和复制输入到ptr->itemName变成:
int readItem (Item *ptr)
{
char buf[MAXC]; /* buffer to hold each user input */
size_t len = 0; /* length of product name */
fputs ("\nProduct name: ", stdout); /* fputs all that is needed, no conversions */
if (fgets (buf, MAXC, stdin) == NULL) { /* read input into buffer and validate */
fputs ("(user canceled input)\n", stdout); /* handle error */
return EOF; /* return EOF if manual EOF generated by user */
}
len = strcspn (buf, "\n"); /* get length of chars (includes '\n') */
buf[len] = 0; /* overwrite '\n' with '\0' */
ptr->itemName = malloc (len + 1); /* allocate length + 1 bytes */
if (!ptr->itemName) { /* validate allocation */
perror ("malloc-ptr->itemName"); /* output error (malloc sets errno) */
return 1; /* return failure */
}
memcpy (ptr->itemName, buf, len + 1); /* copy buf to ptr->itemName */
(注意:无需使用strcpy() 复制到ptr->itemName 并让strcpy() 重新扫描字符串结尾,因为这已经在调用strcspn()。知道需要复制多少字节,您可以简单地使用memcpy()复制该字节数)
读取到int 和float 的转换除了与sscanf() 一起使用的措辞和转换说明符之外基本相同,例如
fputs ("Quantity : ", stdout); /* prompt and read quantity */
if (!fgets (buf, MAXC, stdin)) {
fputs ("(user canceled input)\n", stdout);
return EOF;
}
if (sscanf (buf, "%d", &ptr->quantity) != 1) { /* convert to int with sscanf */
fputs ("error: invalid integer input.\n", stderr);
return 1;
}
fputs ("Price : ", stdout); /* prompt and read price */
if (!fgets (buf, MAXC, stdin)) {
fputs ("(user canceled input)\n", stdout);
return EOF;
}
if (sscanf (buf, "%f", &ptr->price) != 1) { /* convert to float with sscanf */
fputs ("error: invalid integer input.\n", stderr);
return 1;
}
剩下的就是:
ptr->amount = ptr->price * ptr->quantity;
return 0;
}
使用更新后的readItem() 将一个等效示例放在一起,您将有:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXC 1024 /* if you need a constant, #define one (or more) */
typedef struct Item {
char *itemName;
int quantity;
float price;
float amount;
} Item;
int readItem (Item *ptr)
{
char buf[MAXC]; /* buffer to hold each user input */
size_t len = 0; /* length of product name */
fputs ("\nProduct name: ", stdout); /* fputs all that is needed, no conversions */
if (fgets (buf, MAXC, stdin) == NULL) { /* read input into buffer and validate */
fputs ("(user canceled input)\n", stdout); /* handle error */
return EOF; /* return EOF if manual EOF generated by user */
}
len = strcspn (buf, "\n"); /* get length of chars (includes '\n') */
buf[len] = 0; /* overwrite '\n' with '\0' */
ptr->itemName = malloc (len + 1); /* allocate length + 1 bytes */
if (!ptr->itemName) { /* validate allocation */
perror ("malloc-ptr->itemName"); /* output error (malloc sets errno) */
return 1; /* return failure */
}
memcpy (ptr->itemName, buf, len + 1); /* copy buf to ptr->itemName */
fputs ("Quantity : ", stdout); /* prompt and read quantity */
if (!fgets (buf, MAXC, stdin)) {
fputs ("(user canceled input)\n", stdout);
return EOF;
}
if (sscanf (buf, "%d", &ptr->quantity) != 1) { /* convert to int with sscanf */
fputs ("error: invalid integer input.\n", stderr);
return 1;
}
fputs ("Price : ", stdout); /* prompt and read price */
if (!fgets (buf, MAXC, stdin)) {
fputs ("(user canceled input)\n", stdout);
return EOF;
}
if (sscanf (buf, "%f", &ptr->price) != 1) { /* convert to float with sscanf */
fputs ("error: invalid integer input.\n", stderr);
return 1;
}
ptr->amount = ptr->price * ptr->quantity;
return 0;
}
void printItem (Item *pitem)
{
printf ("\nitemName : %s\n"
" quantity : %d\n"
" price : %.2f\n"
" amount : %.2f\n",
pitem->itemName, pitem->quantity, pitem->price, pitem->amount);
}
int main (void) {
Item sample = { .itemName = "" };
if (readItem (&sample) != 0)
exit (EXIT_FAILURE);
printItem (&sample);
free(sample.itemName);
}
使用/输出示例
$ ./bin/readitem_fgets
Product name: lollypops
Quantity : 100
Price : .79
itemName : lollypops
quantity : 100
price : 0.79
amount : 79.00
它同样可以处理空格分隔的单词和任意长度的字符串,不超过MAXC - 1 个字符:
$ ./bin/readitem_fgets
Product name: "My dog has fleas and my cat has none?" (McGraw-BigHill 1938)
Quantity : 10
Price : 19.99
itemName : "My dog has fleas and my cat has none?" (McGraw-BigHill 1938)
quantity : 10
price : 19.99
amount : 199.90
内存使用/错误检查
与上面相同的输出,分配的字节数与第一个示例所示的结果相同。
除了我提供的 cmets 之外,还有用户输入的基本细节方法、不同的注意事项以及处理字符串输入分配的方式。如果您还有其他问题,请仔细查看并告诉我。
脚注:
1. scanf() 对新的 C 程序员来说充满了陷阱。接受用户输入的推荐方法是使用 line-oriented 函数,例如 fgets() 或 POSIX getline()。这样,您可以确保读取完整的输入行,并且在发生匹配失败时字符不会保留在输入缓冲区 (stdin) 中。一些讨论正确使用scanf、C For loop skips first iteration and bogus number from loop scanf和Trying to scanf hex/dec/oct values to check if they're equal to user input和How do I limit the input of scanf to integers and floats(numbers in general)的链接