Каков наилучший способ освободить память после возврата от ошибки?

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

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;
}

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