【问题标题】:Free memory correctly正确释放内存
【发布时间】:2020-07-29 22:13:02
【问题描述】:

我只想知道我分配给传递给readItem() 的指针的内存在main() 中是否正确为freed,如果它是错误的,我应该怎么做才能使它正确.

感谢您的帮助,祝您有美好的一天!

typedef struct Item
{
    char* itemName;
    int quantity;
    float price;
    float amount;
} Item;

void readItem(Item* ptr);

int main(int argc, char* argv[])
{
    Item sample;
    Item* p_sample = &sample;

    readItem(p_sample);
    printItem(p_sample);

    free(p_sample->itemName);
}

void readItem(Item* ptr)
{
    int size;
    printf("\n Specify the amount of letters of the product name: ");
    scanf("%d", &size);

    ptr->itemName = (char*) malloc (size * sizeof(char));

    if (ptr->itemName != NULL)
    {
        printf("\n Specify the name of the product (MAX %d letters): ", size);
        scanf("%s", ptr->itemName);

        printf("\n How many products do the company have? ");
        scanf("%d", &(ptr->quantity));

        printf("\n How expensive is the product? ");
        scanf ("%f", &(ptr->price));

        ptr->amount = (ptr->price)*(ptr->quantity);

    }
    else {
        exit(-1);
    }
}

【问题讨论】:

  • 找到了——是的,你对free() 的使用是正确的——你对malloc 的使用不是。在C语言中,malloc的返回不需要强制转换,没有必要。请参阅:Do I cast the result of malloc?。另外sizeof(char) 被定义为1 并且应该被省略。除非您检查返回,否则您无法正确使用任何用户输入功能。
  • 没有必要将malloc 的返回值强制返回,因为计算机会自动为我做,对吧?
  • 听朋友说这个程序导致内存泄漏。我应该改变什么才能使它正确?如果不使用函数打印出所有有趣的变量,那么检查我的回报的好方法是什么?
  • 没有必要强制转换 malloc 的返回值,因为任何指针都可以在没有强制转换的情况下转换为 void* 类型(void 指针)或从类型转换而来。 malloc 返回类型 void*(分配的内存块的起始地址)。你朋友错了。您有一个分配 ptr->itemName = malloc (size); 然后在指针 p_sample->itemName 丢失之前释放 free(p_sample->itemName);。 (虽然内存在程序退出时被释放——这对于培养好习惯和释放分配的内存来说是件好事!)养成好习惯比改掉坏习惯要容易得多......
  • 你不应该 exit(-1) 向你的 shell 返回一个负值。 C 定义了应该使用的两个宏 EXIT_SUCCESS (0) 和 EXIT_FAILURE (1)。值01 是C 定义的仅有的两个返回值以指示成功/失败。进一步的 POSIX 指定仅将 0unsigned char 范围内的正值返回到 shell,而大于 127 的值通常保留用于特定目的。所以将01 返回到shell。

标签: c dynamic-memory-allocation


【解决方案1】:

除了上述 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 并返回10 有效)。这样,您可以选择处理错误并恢复而不是退出,而不是在输入或匹配失败时调用 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-&gt;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-&gt;itemName 并让strcpy() 重新扫描字符串结尾,因为这已经在调用strcspn()。知道需要复制多少字节,您可以简单地使用memcpy()复制该字节数)

读取到intfloat 的转换除了与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) 中。一些讨论正确使用scanfC For loop skips first iteration and bogus number from loop scanfTrying to scanf hex/dec/oct values to check if they're equal to user inputHow do I limit the input of scanf to integers and floats(numbers in general)的链接

【讨论】:

  • 这个答案对我帮助很大。你知道 Windows 有什么好的内存使用检查器吗?而且我已经在研究这种错误处理方法和推荐的用户输入方式。一直在进步!
  • 这里Is there a good Valgrind substitute for Windows? 有一系列适用于Windows 的好方法。我忘记提及fgets()/sscanf() 方法的最后一个好处是您可以独立验证(1)使用fgets() 读取用户的信息,以及(2)使用sscanf() 验证转换。这使您可以编写健壮的输入例程,要求用户通过不断循环输入正确的值,直到提供该输入为止。祝你的编码好运!
猜你喜欢
  • 2020-06-23
  • 2023-03-11
  • 2020-11-28
  • 2021-04-21
  • 2014-06-20
  • 2011-05-16
  • 2011-04-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多