Так что является обычной практикой для определения параметра метода 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
означает, что если разыменовать указатель, программа должна завершиться с ошибкой сегментации . Это намного легче отладить с помощью отладчика; по крайней мере, вы можете легко определить, где именно и как произошла авария.
Это не так важно в автономном коде, но для библиотечного кода или кода, используемого другими программистами, это может повлиять на общее качество объединенного кода. Это зависит; Я всегда оцениваю это в каждом конкретном случае, хотя я склонен использовать версию-указатель-член-отравление для примеров.
В этом ответе я гораздо больше и больше обращался к элементам указателя, чем к элементам гибкого массива . Это может быть интересно тем, кто интересуется, как вернуть / освободить структуры и как выбрать, какой тип (член указателя или член гибкого массива) использовать в различных случаях.