Каков наилучший способ освободить память после возврата от ошибки?
Предположим, что у меня есть функция, которая выделяет память для вызывающего:
int func(void **mem1, void **mem2) {
*mem1 = malloc(SIZE);
if (!*mem1) return 1;
*mem2 = malloc(SIZE);
if (!*mem2) {
/* ... */
return 1;
}
return 0;
}
Я хотел бы услышать ваши отзывы о наилучшем способе освобождения() выделенной памяти в случае неудачи второго malloc(). Вы можете представить более сложную ситуацию с большим количеством точек выхода из-за ошибки и более выделенной памяти.
Ответы
Ответ 1
Я знаю, что люди не любят их использовать, но это идеальная ситуация для goto
в C.
int func( void** mem1, void** mem2 )
{
int retval = 0;
*mem1 = malloc(SIZE);
if (!*mem1) {
retval = 1;
goto err;
}
*mem2 = malloc(SIZE);
if (!*mem2) {
retval = 1;
goto err;
}
// ...
goto out;
// ...
err:
if( *mem1 ) free( *mem1 );
if( *mem2 ) free( *mem2 );
out:
return retval;
}
Ответ 2
Вот где, по-моему, подходит. Раньше я придерживался догмы антигуто, но я изменил это, когда мне было указано, что {...} while (0); компилируется в один и тот же код, но не так легко читать. Просто следуйте некоторым основным правилам, например, не отказывайтесь от них, сохраняя их до минимума, используя их только для условий ошибок и т.д.
int func(void **mem1, void **mem2)
{
*mem1 = NULL;
*mem2 = NULL;
*mem1 = malloc(SIZE);
if(!*mem1)
goto err;
*mem2 = malloc(SIZE);
if(!*mem2)
goto err;
return 0;
err:
if(*mem1)
free(*mem1);
if(*mem2)
free(*mem2);
*mem1 = *mem2 = NULL;
return 1;
}
Ответ 3
Это немного противоречиво, но я думаю, что подход goto
, используемый в ядре Linux, действительно работает в этой ситуации очень хорошо:
int get_item(item_t* item)
{
void *mem1, *mem2;
int ret=-ENOMEM;
/* allocate memory */
mem1=malloc(...);
if(mem1==NULL) goto mem1_failed;
mem2=malloc(...);
if(mem2==NULL) goto mem2_failed;
/* take a lock */
if(!mutex_lock_interruptible(...)) { /* failed */
ret=-EINTR;
goto lock_failed;
}
/* now, do the useful work */
do_stuff_to_acquire_item(item);
ret=0;
/* cleanup */
mutex_unlock(...);
lock_failed:
free(mem2);
mem2_failed:
free(mem1);
mem1_failed:
return ret;
}
Ответ 4
Это читаемая альтернатива:
int func(void **mem1, void **mem2) {
*mem1 = malloc(SIZE);
*mem2 = malloc(SIZE);
if (!*mem1 || !*mem2) {
free(*mem2);
free(*mem1);
return 1;
}
return 0;
}
Ответ 5
Помогает ли вызывающий абонент делать что-либо полезное с блоками памяти, которые были правильно распределены до отказа? Если нет, вызывающий должен обработать освобождение.
Одной из возможностей эффективной очистки является использование do..while(0)
, что позволяет break
, где ваш пример return
s:
int func(void **mem1, void **mem2)
{
*mem1 = NULL;
*mem2 = NULL;
do
{
*mem1 = malloc(SIZE);
if(!*mem1) break;
*mem2 = malloc(SIZE);
if(!*mem2) break;
return 0;
} while(0);
// free is NULL-safe
free(*mem1);
free(*mem2);
return 1;
}
Если вы делаете много распределений, вы можете использовать свою функцию freeAll()
для очистки здесь.
Ответ 6
лично; У меня есть библиотека отслеживания ресурсов (в основном сбалансированное двоичное дерево), и у меня есть обертки для всех функций распределения.
Ресурсы (такие как память, сокеты, дескрипторы файлов, семафоры и т.д. - все, что вы выделяете и освобождаете) могут принадлежать набору.
У меня также есть библиотека обработки ошибок, в которой первый аргумент каждой функции является установленной ошибкой, и если что-то пойдет не так, функция, испытывающая ошибку, отправляет ошибку в набор ошибок.
Если установленная ошибка содержит ошибку, никакие функции не выполняются. (У меня есть макрос в верхней части каждой функции, которая заставляет его возвращаться).
Таким образом, несколько mallocs выглядят следующим образом:
mem[0] = malloc_wrapper( error_set, resource_set, 100 );
mem[1] = malloc_wrapper( error_set, resource_set, 50 );
mem[2] = malloc_wrapper( error_set, resource_set, 20 );
Нет необходимости проверять возвращаемое значение, потому что, если возникает ошибка, не выполняются следующие функции, например. следующие mallocs никогда не встречаются.
Итак, когда приходит время для освобождения ресурсов (например, в конце функции, где все ресурсы, используемые внутри этой функции, были помещены в набор), я освобождаю набор. Это всего лишь один вызов функции.
res_delete_set( resource_set );
Мне не нужно специально проверять наличие ошибок - в моей проверке кода возвращаются значения if() s, что делает его пригодным для обслуживания; Я нахожу, что разграничение проверки ошибок в строке разрушает читаемость и, следовательно, ремонтопригодность. У меня просто хороший список вызовов функций.
Это искусство, человек: -)
Ответ 7
Моя собственная склонность заключается в создании переменной функции аргументов, которая освобождает все указатели, отличные от NULL. Затем вызывающий может обрабатывать случай ошибки:
void *mem1 = NULL;
void *mem2 = NULL;
if (func(&mem1, &mem2)) {
freeAll(2, mem1, mem2);
return 1;
}
Ответ 8
Если приведенные выше утверждения goto вы по каким-то причинам ужасны, вы всегда можете сделать что-то вроде этого:
int func(void **mem1, void **mem2)
{
*mem1 = malloc(SIZE);
if (!*mem1) return 1;
*mem2 = malloc(SIZE);
if (!*mem2) {
/* Insert free statement here */
free(*mem1);
return 1;
}
return 0;
}
Я использую этот метод довольно регулярно, но только когда он очень четко понимает, что происходит.
Ответ 9
Я немного испугался всех рекомендаций для инструкции goto!
Я обнаружил, что использование goto приводит к запутыванию кода, который, скорее всего, приведет к ошибкам программиста. Теперь я предпочитаю избегать его использования, за исключением самых экстремальных ситуаций. Я почти никогда не использовал его. Не из-за академического перфекционизма, а потому, что год или более позже всегда кажется сложнее вспомнить общую логику, чем с альтернативой, которую я предлагаю.
Будучи тем, кто любит реорганизовывать вещи, чтобы свести к минимуму мой вариант забыть вещи (например, очистить указатель), я бы добавил несколько функций в первую очередь. Вероятно, я предположим, что я буду использовать их совсем немного в одной программе. Функция imalloc() выполнила операцию malloc с косвенным указателем; ifree() отменит это. cifree() освобождает память условно.
С моей стороны, моя версия кода (с третьим аргументом для демонстрации) будет выглядеть так:
// indirect pointer malloc
int imalloc(void **mem, size_t size)
{
return (*mem = malloc(size));
}
// indirect pointer free
void ifree(void **mem)
{
if(*mem)
{
free(*mem);
*mem = NULL;
}
}
// conditional indirect pointer free
void cifree(int cond, void **mem)
{
if(!cond)
{
ifree(mem);
}
}
int func(void **mem1, void **mem2, void **mem3)
{
int result = FALSE;
*mem1 = NULL;
*mem2 = NULL;
*mem3 = NULL;
if(imalloc(mem1, SIZE))
{
if(imalloc(mem2, SIZE))
{
if(imalloc(mem3, SIZE))
{
result = TRUE;
}
cifree(result, mem2);
}
cifree(result, mem1);
}
return result;
}
Я предпочитаю иметь только один возврат из функции, в конце. Выпрыгивание между ними происходит быстро (и, на мой взгляд, вроде грязно). Но, что более важно, вы можете легко обойти связанный код очистки непреднамеренно.