Как я здесь неправильно использую malloc? - PullRequest
2 голосов
/ 05 апреля 2019

Я пытаюсь научиться правильно распределять память во время выполнения в C99.

Я написал минимальный пример, так как думаю, что он будет поучительным в отношении того, что я пытаюсь сделать.По какой-то причине «внутренние» вызовы malloc, где выделяются фрагменты памяти размером sizeof(letter_t), делают только то, что я ожидаю (т.е. выделяю память) для первого элемента в массиве.

#include <stdlib.h>
#include <stdio.h>

typedef struct letter_t {
    char *from;
    int lines;
} letter_t;

typedef struct letterbox_t {
    char *name;
    int n_letters;
    struct letter_t **letters;
} letterbox_t;

int main() {

    char *name[]    = { "amy", "bob", "claud" };
    int n_letters[] = { 1,     3,     2 };

    // layout memory and populate letterbox_t array
    struct letterbox_t *letterboxes;
    letterboxes = malloc(sizeof(letterbox_t) * 3);

    for (int i = 0; i < 3; i++) {
        letterboxes[i].name = name[i];
        letterboxes[i].n_letters = n_letters[i];

        struct letter_t *letters[n_letters[i]];
        for (int j = 0; j < n_letters[i]; j++) {
            letters[j] = malloc(sizeof(letter_t));
        }
        letterboxes[i].letters = letters;
    }

    // populate letter_t array for each letterbox_t
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < letterboxes[i].n_letters; j++) {
            // =========================================
            letterboxes[i].letters[j]->from = "spammer";
            // =========================================
            // the above line fails for i = 1, j = 1
        }
    }

    for (int i = 0; i < 3; i++) {
        printf("%s has %d letters from\n", letterboxes[i].name, letterboxes[i].n_letters);
        for (int j = 0; j < letterboxes[i].n_letters; j++) {
            printf("  %s\n", letterboxes[i].letters[j]->from);
        }
    }
    return 0;
};

Когда j во внутреннем цикле достигает 1, я вижу только мусорную память.Вот несколько выводов GDB в качестве иллюстрации.

Breakpoint 1, main () at example.c:40
40            letterboxes[i].letters[j]->from = "spammer";
(gdb) p i
$1 = 1
(gdb) p j
$2 = 1
(gdb) p letterboxes[i].letters[j]
$3 = (struct letter_t *) 0x400604 <main+228>
(gdb) p *letterboxes[i].letters[j]
$4 = {from = 0x904d8b48ac7d6348 <error: Cannot access memory at address 0x904d8b48ac7d6348>, lines = -117143224}

Ответы [ 2 ]

1 голос
/ 05 апреля 2019

У вас очень много мелких проблем.Для начала, как упоминалось в моем комментарии, ваша попытка выделить для struct letter_t* letters[n_letters[i]]; не соответствует struct letter_t** letters;

Далее, прежде чем идти дальше, нит '*' принадлежит имени переменной, а невведите в большинстве случаев.Почему?

int* a, b, c;

Выше, конечно, вы не объявляете 3-х указатели на int.Вместо этого вы объявляете целочисленный указатель a и целые числа b, c.Гораздо понятнее записывается как:

int *a, b, c;

Когда вы выделяете память, вы должны проверять, что выделение выполнено успешно - каждый раз, например,

    size_t n_people = sizeof name / sizeof *name;

    // layout memory and populate letterbox_t array
    struct letterbox_t *letterboxes;
    /* allocate letterboxes for each of the people */
    letterboxes = malloc (sizeof *letterboxes * n_people);
    if (!letterboxes) {     /* validate Every allocation */
        perror ("malloc-letterboxes");
        return 1;
    }

Теперь у вас есть память для 3 letterbox_tи может начать работу над содержанием.Вы можете назначить имена и количество букв каждому:

    for (size_t i = 0; i < n_people; i++) {

        /* assigning pointer to string literal */
        letterboxes[i].name = name[i];
        letterboxes[i].n_letters = n_letters[i];    /* int assignment */

( примечание: Будьте осторожны. Понимайте, что вы присваиваете String Literal из name[i]каждому letterboxes[i].name. Это означает, что letterboxes[i].name не может быть изменено и не должно быть освобождено. Как правило, вы должны выделить хранилище для name и копирования)

letterboxes[i].letters - это указатель науказатель на letter_t.Это означает, что вы должны сначала выделить указатели, а затем выделить хранилище для каждого letter и назначить начальный адрес для этого блока памяти каждому из указателей, например, letterboxes[i].letters[j].Например:

        /* allocate letterboxes[i].n_letters pointers */
        letterboxes[i].letters =
            malloc (sizeof *letterboxes[i].letters * letterboxes[i].n_letters);
        if (!letterboxes[i].letters) { /* validate allocation */
            perror ("malloc-letterboxes.letters");
            return 1;
        }

        /* allocate letters per-pointer */
        for (int j = 0; j < letterboxes[i].n_letters; j++) {
            letterboxes[i].letters[j] = 
                            malloc (sizeof *letterboxes[i].letters[j]);
            if (!letterboxes[i].letters[j]) {
                perror ("malloc-letterboxes[i].letters[j]");
                return 1;
            }
        }

Теперь, когда все ваше хранилище правильно выделено и проверено , вы можете заполнить каждую букву каждому из людей и затем вывестирезультаты, например

    // populate letter_t array for each letterbox_t
    for (size_t i = 0; i < n_people; i++) {
        for (int j = 0; j < letterboxes[i].n_letters; j++) {
            letterboxes[i].letters[j]->from = "spammer";
            /* added lines just to complete assignments */
            letterboxes[i].letters[j]->lines = letterboxes[i].n_letters * 10;
        }
    }

    // output all letterboxes and letters
    for (size_t i = 0; i < n_people; i++) {
        printf("%s has %d letters from\n", 
                        letterboxes[i].name, letterboxes[i].n_letters);
        for (int j = 0; j < letterboxes[i].n_letters; j++) {
            printf("  %s  %d\n", letterboxes[i].letters[j]->from,
                                letterboxes[i].letters[j]->lines);
        }
    }

Как только вы закончили использовать выделенную память, вы должны убедиться, что она правильно освобождена.(что становится критичным по мере роста ваших программ, и вы начинаете выделять их внутри функций).Неспособность освободить то, что вы используете, приводит к утечкам памяти в программах.Для этого имеет смысл написать простую функцию для полного освобождения letterbox_t, например,

/* simple function to free single letterbox_t completely */
void freeletterbox (letterbox_t *l)
{
    for (int i = 0; i < l->n_letters; i++)
        free (l->letters[i]);

    free (l->letters);
}

Затем, когда вы закончите с памятью, вы можете free() ее, например,

    for (size_t i = 0; i < n_people; i++)   /* free each letterbox */
        freeletterbox (&letterboxes[i]);
    free (letterboxes);                     /* free pointers */

Сложив все вместе, вы можете сделать:

#include <stdio.h>
#include <stdlib.h>

typedef struct letter_t {
    char *from;
    int lines;
} letter_t;

typedef struct letterbox_t {
    char *name;
    int n_letters;
    struct letter_t **letters;
} letterbox_t;

/* simple function to free single letterbox_t completely */
void freeletterbox (letterbox_t *l)
{
    for (int i = 0; i < l->n_letters; i++)
        free (l->letters[i]);

    free (l->letters);
}

int main (void) {

    char *name[]    = {"amy", "bob", "claud"};
    int n_letters[] = {1,     3,     2};
    size_t n_people = sizeof name / sizeof *name;

    // layout memory and populate letterbox_t array
    struct letterbox_t *letterboxes;
    /* allocate letterboxes for each of the people */
    letterboxes = malloc (sizeof *letterboxes * n_people);
    if (!letterboxes) {     /* validate Every allocation */
        perror ("malloc-letterboxes");
        return 1;
    }

    for (size_t i = 0; i < n_people; i++) {

        /* assigning pointer to string literal */
        letterboxes[i].name = name[i];
        letterboxes[i].n_letters = n_letters[i];    /* int assignment */

        /* allocate letterboxes[i].n_letters pointers */
        letterboxes[i].letters =
            malloc (sizeof *letterboxes[i].letters * letterboxes[i].n_letters);
        if (!letterboxes[i].letters) { /* validate allocation */
            perror ("malloc-letterboxes.letters");
            return 1;
        }

        /* allocate letters per-pointer */
        for (int j = 0; j < letterboxes[i].n_letters; j++) {
            letterboxes[i].letters[j] = 
                            malloc (sizeof *letterboxes[i].letters[j]);
            if (!letterboxes[i].letters[j]) {
                perror ("malloc-letterboxes[i].letters[j]");
                return 1;
            }
        }
    }

    // populate letter_t array for each letterbox_t
    for (size_t i = 0; i < n_people; i++) {
        for (int j = 0; j < letterboxes[i].n_letters; j++) {
            letterboxes[i].letters[j]->from = "spammer";
            /* added lines just to complete assignments */
            letterboxes[i].letters[j]->lines = letterboxes[i].n_letters * 10;
        }
    }

    // output all letterboxes and letters
    for (size_t i = 0; i < n_people; i++) {
        printf("%s has %d letters from\n", 
                        letterboxes[i].name, letterboxes[i].n_letters);
        for (int j = 0; j < letterboxes[i].n_letters; j++) {
            printf("  %s  %d\n", letterboxes[i].letters[j]->from,
                                letterboxes[i].letters[j]->lines);
        }
    }

    for (size_t i = 0; i < n_people; i++)   /* free each letterbox */
        freeletterbox (&letterboxes[i]);
    free (letterboxes);                     /* free pointers */

    return 0;
}

Пример использования / Вывод

$ ./bin/letters
amy has 1 letters from
  spammer  10
bob has 3 letters from
  spammer  30
  spammer  30
  spammer  30
claud has 2 letters from
  spammer  20
  spammer  20

Использование памяти / ОшибкаПроверьте

В любом написанном вами коде, который динамически выделяет память, у вас есть 2 обязанностей в отношении любого выделенного блока памяти: (1) всегда сохраняйте указатель на начальныйадрес для блока памяти, таким образом, (2) он может быть освобожден , когда он больше не нужен.

Обязательно, чтобы вы использовали программу проверки ошибок памяти для обеспеченияВы не пытаетесь получить доступ к памяти или выполнить запись за пределами / за пределами выделенного блока, пытаетесь прочитать или основать условный переход на неинициализированном значении и, наконец, подтвердить, что вы освобождаете всю выделенную память.

Для Linux valgrind - это нормальный выбор.Для каждой платформы есть похожие проверки памяти.Все они просты в использовании, просто запустите вашу программу через него.

$ valgrind ./bin/letters
==4735== Memcheck, a memory error detector
==4735== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==4735== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==4735== Command: ./bin/letters
==4735==
amy has 1 letters from
  spammer  10
bob has 3 letters from
  spammer  30
  spammer  30
  spammer  30
claud has 2 letters from
  spammer  20
  spammer  20
==4735==
==4735== HEAP SUMMARY:
==4735==     in use at exit: 0 bytes in 0 blocks
==4735==   total heap usage: 10 allocs, 10 frees, 216 bytes allocated
==4735==
==4735== All heap blocks were freed -- no leaks are possible
==4735==
==4735== For counts of detected and suppressed errors, rerun with: -v
==4735== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Всегда подтверждайте, что вы освободили всю выделенную память и что ошибок памяти нет.

Посмотрите вещии дайте мне знать, если у вас есть дополнительные вопросы.

0 голосов
/ 05 апреля 2019

Изменить это:

struct letter_t* letters[n_letters[i]];

к этому:

struct letter_t** letters = malloc(n_letters[i] * sizeof(struct letter_t*));

потому что, как прокомментировал @TomKarzes, вы создаете letters внутри тела цикла for, таким образом, он выходит из области видимости после завершения цикла.

По этой причине вам необходимо динамически распределять память для него, чтобы память не освобождалась после завершения цикла.

PS: не забудьте освободить память в конце вашей программы, следуя порядку, обратному порядку, который вы выполняли при динамическом выделении памяти.

...