Модификация массивов на месте и понимание распределения памяти для него - PullRequest
0 голосов
/ 27 сентября 2019

У меня есть следующие две функции, которые принимают массивы строк и делают их строчными (на месте) -

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

void to_lower(char ** strings) {

    char * original_string;
    char * lower_string;

    for (int i=0; (original_string=strings[i]) != NULL; i++) {
        lower_string = malloc(strlen(original_string + 1) * sizeof(char));
        for (int j=0; j<=strlen(original_string); j++) {
            lower_string[j] = tolower(original_string[j]);
        }
        strings[i]=lower_string;
    }
}


int main(void) {
    char * strings[] = {"Hello", "Zerotom", "new", NULL };
    to_lower(strings);
    to_lower(strings); // this and successive calls won't change
    to_lower(strings); // anything but are here to try and understand malloc
    to_lower(strings);
    to_lower(strings);
    return 0;
}

В начале функции main перед вызовом to_lowerсколько памяти расходуется?Мое предположение было 16 байтов из массива символов (15 символов + 1 нулевой байт в конце).

После того, как to_lower запустился 5 раз и до возврата функции, сколько памяти было использовано?Где я должен быть «свободен» - строки, которые передаются в функцию (как я думал, вызывая malloc каждый раз, когда строка копируется / обрабатывается в нижнем регистре, она создает столько дополнительной памяти, но никогда ничего не освобождает.

Выглядит ли функция to_lower нормально или как ее можно изменить, чтобы она не теряла память, если она есть в данный момент?

Ответы [ 4 ]

2 голосов
/ 27 сентября 2019

Вы обвиваетесь вокруг оси (путаете себя), объединяя распределение и преобразование в понижение в одну void to_lower(char ** strings) функцию.Как вы обнаружили, если вы хотите дважды вызывать функцию для одного и того же объекта, вы должны free выделить память, выделенную для хранения строчных букв между вызовами, - но тогда вы потеряли указатели на исходные строки..

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

Ваше выделение и копирование strings требуется перед изменением строк, содержащихся в strings, поскольку вы инициализируете strings как массив указателей на строковые литералы .String-literal являются неизменяемыми (на всех, кроме очень немногих системах), созданными в постоянной памяти (обычно это раздел .rodata исполняемого файла).Попытка изменить строковый литерал почти гарантирует гарантию SegFault (на всех, кроме странных систем)

Кроме того, как вы собираетесь вернуть исходные строки, если вы уже перезаписали адреса указателей наоригиналы с указателями на выделенную память, содержащие строчные буквы?(не говоря уже о головной боли при отслеживании, которую вы создаете, заменяя указатели на литералы указателями на выделенную память, если это нормально, free эти указатели).

Здесь гораздо лучше оставить вас оригинальными strings нетронутым и просто выделите копию оригинала (выделив указатели, в том числе один для значения часового, и выделив хранилище для каждой из исходных строк, а затем скопируйте исходные строки перед преобразованием в нижний регистр. Это решает вашу проблему с утечками памятиа также проблема с потерей ваших оригинальных указателей на строковые литералы. Вы можете освободить строчные буквы по мере необходимости, и вы всегда можете сделать еще одну копию оригиналов для отправки в вашу функцию преобразования снова.

Так как быВы собираетесь реализовать это? Самый простой способ - просто объявить указатель на указатель на символ (например, двойной указатель), которому вы можете назначить блок памяти для любого количества указателей, которые вам нравятся.этот случай просто выделитьстолько же указателей, сколько у вас в массиве strings, например:

    char *strings[] = {"Hello", "Zerotom", "new", NULL };
    size_t nelem = *(&strings + 1) - strings;   /* number of pointers */
    /* declare & allocate nelem pointers */
    char **modstrings = malloc (nelem * sizeof *modstrings);

    if (!modstrings) {  /* validate EVERY allocation */
        perror ("malloc-modstrings");
    }

( примечание: , вы также можете использовать sizeof strings / sizeof *strings для получения количества элементов)

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

void copy_strings (char **dest, char * const *src)
{
    while (*src) {                          /* loop over each string */
        size_t len = strlen (*src);         /* get length */
        if (!(*dest = malloc (len + 1))) {  /* allocate/validate */
            perror ("malloc-dest");         /* handle error */
            exit (EXIT_FAILURE);
        }
        memcpy (*dest++, *src++, len + 1);  /* copy *src to *dest (advance) */
    }
    *dest = NULL;       /* set sentinel NULL */
}

( note: , передав параметр src как char * const *src вместо просто char **src, вы можете указать компилятору, что src не будет изменен, что позволит дальнейшей оптимизации компилятором.restrict было бы похоже, но это обсуждение оставлено на другой день)

Ваша to_lower функция затем уменьшается до:

void to_lower (char **strings) {

    while (*strings)                        /* loop over each string */
        for (char *p = *strings++; *p; p++) /* loop over each char */
            *p = tolower (*p);              /* convert to lower */
}

Для удобства, так как вы знаете, что вы захотитечтобы скопировать strings в modstrings перед каждым вызовом to_lower, вы можете объединить обе функции в одну оболочку (что имеет смысл объединять), например,

void copy_to_lower (char **dest, char * const *src)
{
    copy_strings (dest, src);   /* just combine functions above into single */
    to_lower (dest);
}

(можно даже добавитьprint_array и free_strings выше, а также, если вы всегда хотите выполнять эти операции в одном вызове - подробнее позже)

Между каждым copy_to_lower и print_array из modstringsвам нужно будет освободить память, назначенную каждому указателю, чтобы не допустить утечки памяти при повторном вызове copy_to_lower.Простая функция free_strings может быть такой:

void free_strings (char **strings)
{
    while (*strings) {          /* loop over each string */
        free (*strings);        /* free it */
        *strings++ = NULL;      /* set pointer NULL (advance to next) */
    }
}

Теперь вы можете выделять, копировать, преобразовывать в нижние, печатать и освобождать столько раз, сколько захотите в main().Вы просто будете делать повторные звонки на:

    copy_to_lower (modstrings, strings);    /* copy_to_lower to modstrings */
    print_array (modstrings);               /* print modstrings */
    free_strings (modstrings);              /* free strings (not pointers) */

    copy_to_lower (modstrings, strings);    /* ditto */
    print_array (modstrings);
    free_strings (modstrings);
    ...

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

    free (modstrings);                      /* now free pointers */

Поместив все это в пример, вы можете сделать следующее:

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

void print_array (char **strings)
{
    while (*strings)
        printf ("%s, ", *strings++);
    putchar ('\n');
}

void free_strings (char **strings)
{
    while (*strings) {          /* loop over each string */
        free (*strings);        /* free it */
        *strings++ = NULL;      /* set pointer NULL (advance to next) */
    }
}

void copy_strings (char **dest, char * const *src)
{
    while (*src) {                          /* loop over each string */
        size_t len = strlen (*src);         /* get length */
        if (!(*dest = malloc (len + 1))) {  /* allocate/validate */
            perror ("malloc-dest");         /* handle error */
            exit (EXIT_FAILURE);
        }
        memcpy (*dest++, *src++, len + 1);  /* copy *src to *dest (advance) */
    }
    *dest = NULL;       /* set sentinel NULL */
}

void to_lower (char **strings) {

    while (*strings)                        /* loop over each string */
        for (char *p = *strings++; *p; p++) /* loop over each char */
            *p = tolower (*p);              /* convert to lower */
}

void copy_to_lower (char **dest, char * const *src)
{
    copy_strings (dest, src);   /* just combine functions above into single */
    to_lower (dest);
}

int main(void) {

    char *strings[] = {"Hello", "Zerotom", "new", NULL };
    size_t nelem = *(&strings + 1) - strings;   /* number of pointers */
    /* declare & allocate nelem pointers */
    char **modstrings = malloc (nelem * sizeof *modstrings);

    if (!modstrings) {  /* validate EVERY allocation */
        perror ("malloc-modstrings");
    }

    copy_to_lower (modstrings, strings);    /* copy_to_lower to modstrings */
    print_array (modstrings);               /* print modstrings */
    free_strings (modstrings);              /* free strings (not pointers) */

    copy_to_lower (modstrings, strings);    /* ditto */
    print_array (modstrings);
    free_strings (modstrings);

    copy_to_lower (modstrings, strings);    /* ditto */
    print_array (modstrings);
    free_strings (modstrings);

    copy_to_lower (modstrings, strings);    /* ditto */
    print_array (modstrings);
    free_strings (modstrings);

    free (modstrings);                      /* now free pointers */
}

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

$ ./bin/tolower_strings
hello, zerotom, new,
hello, zerotom, new,
hello, zerotom, new,
hello, zerotom, new,

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

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

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

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

$ valgrind ./bin/tolower_strings
==5182== Memcheck, a memory error detector
==5182== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==5182== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==5182== Command: ./bin/tolower_strings
==5182==
hello, zerotom, new,
hello, zerotom, new,
hello, zerotom, new,
hello, zerotom, new,
==5182==
==5182== HEAP SUMMARY:
==5182==     in use at exit: 0 bytes in 0 blocks
==5182==   total heap usage: 13 allocs, 13 frees, 104 bytes allocated
==5182==
==5182== All heap blocks were freed -- no leaks are possible
==5182==
==5182== For counts of detected and suppressed errors, rerun with: -v
==5182== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

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

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

2 голосов
/ 27 сентября 2019

Выглядит ли функция to_lower нормально или как ее можно изменить, чтобы она не пропускала память, если она есть в данный момент?

Как указано @chux в комментариях,вам нужно добавить 1 к len orginal_string, а не к самому указателю.

Что касается вашего вопроса, то да, он протекает, каждый вызов malloc требует вызова free.Проблема в следующем: вам не разрешено вызывать free для начальных значений, поскольку они заполнены строковыми литералами .

Возможное решение:

extern char *strdup(const char *);

static char *dup(const char *str)
{
    char *res = strdup(str);

    if (res == NULL) {
        perror("strdup");
        exit(EXIT_FAILURE);
    }
    return res;
}

int main(void)
{
    char *strings[] = {
        dup("Hello"),
        dup("Zerotom"),
        dup("new"),
        NULL
    };

    ...

Теперь вы можете вызывать to_lower, и вам не нужно malloc внутри функции, просто вызовите free для каждого элемента в самом конце, когда массив больше не нужен.

Обратите внимание, что strdup не является частью стандарта (но доступен во многих реализациях)

0 голосов
/ 27 сентября 2019

В качестве альтернативы {"Hello", "Zerotom", "new", NULL }; и malloc() и друзьям, сформируйте массив указателей char * strings[] для инициализации указателями на изменяемые данные.

Начиная с C99, используйте составные литералы .

void inplace_strtolower(char * s) {
  while (*s) {
    *s = (char) tolower((unsigned char ) *s);
    s++;
  }
}

// "Non-const string literal"
// Compound literal from string literal"
#define NCSL(sl) ((char[sizeof(sl)]) { sl })

int main(void) {
  char * strings[] = {NCSL("Hello"), NCSL("Zerotom"), NCSL("new"), NULL};
  inplace_strtolower(strings[0]);
  inplace_strtolower(strings[1]);
  inplace_strtolower(strings[2]);
  puts(strings[0]);
  puts(strings[1]);
  puts(strings[2]);
  return 0;
}

Выход

hello
zerotom
new
0 голосов
/ 27 сентября 2019

Каждый раз, когда вызывается to_lower(), вы заменяете все строковые литералы динамическими указателями памяти.Если вы снова вызываете to_lower (), не освобождая существующую память, происходит утечка памяти.

lower_string = malloc(strlen(original_string + 1) * sizeof(char));
        for (int j=0; j<=strlen(original_string); j++) {
            lower_string[j] = tolower(original_string[j]);
        }
        strings[i]=lower_string;

Так что, когда массив strings[] больше не нужен, вы должны освободить всю его память.Например,

for (int i=0; strings[i] != NULL; i++) {
    free(strings[i]);
    strings[i] = NULL;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...