Как правильно восстановить структуру? - PullRequest
0 голосов
/ 18 января 2019

Я пытаюсь понять, какова общая идиома (хорошая практика) для обеспечения создания / восстановления функций структуры. Вот что я попробовал:

struct test_struct_t{
    int a;
};

struct test_struct_t *create(int a){
    struct test_struct_t *test_struct_ptr = malloc(sizeof(*test_struct_ptr));
    test_struct_ptr -> a = a;
    return test_struct_ptr;
}

void release(struct test_struct_t *test_struct_ptr){
    free((void *) test_struct_ptr);
}

int main(int argc, char const *argv[])
{
    const struct test_struct_t *test_struct_ptr = create(10);
    release(test_struct_ptr); // <--- Warning here
}

Я получил предупреждение

passing argument 1 of ‘release’ discards ‘const’ qualifier from pointer 
   target type [-Wdiscarded-qualifiers]

что понятно. Поэтому я склонен определять метод утилизации следующим образом:

void release(const struct test_struct_t *test_struct_ptr){
    free((void *) test_struct_ptr);
}

Предупреждение исчезло, но я не уверен, что оно не подвержено ошибкам.

Так что является обычной практикой для определения параметра метода struct reclamation в качестве указателя на const struct, чтобы мы могли в любой момент избежать преобразования в non-const и выполнить это «грязное» приведение один раз в реализации метода reclamation?

1 Ответ

0 голосов
/ 18 января 2019

Так что является обычной практикой для определения параметра метода struct reclamation в качестве указателя на const struct, чтобы мы могли в любой момент избежать преобразования в non-const и выполнить это грязное приведение в реализации метода восстановления?

Нет. Чаще всего не следует использовать const с динамически распределенными структурами или со структурами, содержащими указатели на динамически распределенную память.

Вы отмечаете только const вещами, которые не собираетесь изменять; и освобождение его или данных, на которые ссылаются его члены, является изменением. Просто посмотрите, как объявлено free(): void free(void *), а не void free(const void *).

Это основная проблема в коде OP, и использование struct test_struct_t *test_struct_ptr = create(10); без квалификатора const является правильным решением.


Здесь есть интересный вопрос, который я хочу немного обсудить, потому что формулировка этого вопроса такова, что те, кто ищет ответы на него, столкнутся с этим вопросом через веб-поиск.

Как правильно восстановить структуру?

Давайте посмотрим на реальный случай: динамически размещаемый строковый буфер. Есть два основных подхода:

typedef struct {
    size_t          size;  /* Number of chars allocated for data */
    size_t          used;  /* Number of chars in data */
    unsigned char  *data;
} sbuffer1;
#define  SBUFFER1_INITIALIZER  { 0, 0, NULL }

typedef struct {
    size_t          size;  /* Number of chars allocated for data */
    size_t          used;  /* Number of chars in data */
    unsigned char   data[];
} sbuffer2;

Можно объявить и инициализировать первую версию с помощью макроса инициализатора препроцессора:

    sbuffer1  my1 = SBUFFER1_INITIALIZER;

Используется, например, в POSIX.1 pthread_mutex_t мьютексы и pthread_cond_t условные переменные.

Однако, поскольку второй имеет элемент гибкого массива, он не может быть объявлен статически; Вы можете только объявить указатели на него. Итак, вам нужна функция конструктора:

sbuffer2 *sbuffer2_init(const size_t  initial_size)
{
    sbuffer2  *sb;

    sb = malloc(sizeof (sbuffer2) + initial_size);
    if (!sb)
        return NULL; /* Out of memory */

    sb->size = initial_size;
    sb->used = 0;
    return sb;
}

который вы используете таким образом:

    sbuffer2 *my2 = sbuffer2_init(0);

хотя я лично реализую соответствующие функции, чтобы вы могли выполнять

    sbuffer2 *my2 = NULL;

как эквивалент sbuffer1 my1 = SBUFFER1_INITIALIZER;.

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

Например, если мы хотим установить содержимое буфера из какого-либо источника, возможно,

int  sbuffer1_set(sbuffer1 *sb, const char *const source, const size_t length);

int  sbuffer2_set(sbuffer2 **sb, const char *const source, const size_t length);

Функции, которые только получают доступ к данным, но не изменяют их, также различаются:

int  sbuffer1_copy(sbuffer1 *dst, const sbuffer1 *src);

int  sbuffer2_copy(sbuffer2 **dst, const sbuffer2 *src);

Обратите внимание, что const sbuffer2 *src не является опечаткой. Поскольку функция не будет изменять указатель src (мы могли бы сделать его const sbuffer2 *const src!), Ей не нужен указатель на указатель на данные, только указатель на данные.

Действительно интересная часть - функции возврата / освобождения.

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

void sbuffer1_free(sbuffer1 *sb)
{
    free(sb->data);
    sb->size = 0;
    sb->used = 0;
    sb->data = NULL;
}

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

void sbuffer2_free1(sbuffer2 **sb)
{
    free(*sb);
    *sb = NULL;
}

но поскольку программисты привыкли к шаблону void *v = malloc(10); free(v); (в отличие от free(&v);!), Они обычно ожидают, что функция будет

void sbuffer2_free2(sbuffer2 *sb)
{
    free(sb);
}

вместо; и этот не может отравить указатель. Если пользователь не сделает эквивалент sbuffer2_free2(sb); sb = NULL;, существует риск повторного использования содержимого sb впоследствии.

Библиотеки C обычно не сразу возвращают память в ОС, а просто добавляют ее в свой собственный внутренний свободный список для использования при последующих вызовах malloc(), calloc() или realloc(). Это означает, что в большинстве ситуаций указатель может быть разыменован после free() без ошибки времени выполнения, но данные, на которые он указывает, будут чем-то совершенно другим. Вот что делает эти ошибки такими неприятными для воспроизведения и отладки.

Отравление - это просто установка недопустимых значений для элементов структуры, так что использование после освобождения легко обнаруживается во время выполнения благодаря легко видимым значениям. Установка указателя, используемого для доступа к динамически выделенной памяти, на NULL означает, что если разыменовать указатель, программа должна завершиться с ошибкой сегментации . Это намного легче отладить с помощью отладчика; по крайней мере, вы можете легко определить, где именно и как произошла авария.

Это не так важно в автономном коде, но для библиотечного кода или кода, используемого другими программистами, это может повлиять на общее качество объединенного кода. Это зависит; Я всегда оцениваю это в каждом конкретном случае, хотя я склонен использовать версию-указатель-член-отравление для примеров.

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

...