Как избежать длинной цепочки свободных (или удалений) после каждой проверки ошибок в C?

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

Итак, я люблю:

char* function() {
    char* mem = get_memory(100);  // first allocation
    if (!mem) return NULL;

    struct binder* b = get_binder('regular binder');  // second allocation
    if (!b) {
        free(mem);
        return NULL;
    }

    struct file* f = mk_file();  // third allocation
    if (!f) {
        free(mem);
        free_binder(b);
        return NULL;
    }

    // ...
}

Обратите внимание, как быстро free() все выходит из-под контроля. Если какая-то из функций не работает, я должен освободить каждое выделение раньше. Код быстро становится уродливым, и все, что я делаю, это копировать все. Я становлюсь программистом для копирования/вставки, что еще хуже, если кто-то добавляет оператор где-то посередине, он должен изменить весь код ниже, чтобы вызвать free() для его добавления.

Как опытные программисты C справляются с этой проблемой? Я ничего не могу понять.

Спасибо, Бода Сидо.

Ответы

Ответ 1

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

char* function()
{
    char* retval = NULL;
    char* mem = get_memory(100);  // first allocation
    if (!mem)
        goto out_error;

    struct binder* b = get_binder('regular binder');  // second allocation
    if (!b)
        goto out_error_mem;

    struct file* f = mk_file();  // third allocation
    if (!f)
        goto out_error_b;

    /* ... Normal code path ... */
    retval = good_value;

  out_error_b:
    free_binder(b);
  out_error_mem:
    free(mem);
  out_error:
    return retval;
}

Управление ошибками с помощью GOTO уже обсуждалось здесь: Допустимое использование goto для управления ошибками в C?

Ответ 2

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

Затем он сказал мне, что этого было недостаточно в большинстве случаев, и теперь он использовал setjmp()/longjmp(). В основном он изобретал исключения С++, но с гораздо меньшей элегантностью.

Тем не менее, поскольку goto может работать, вы можете реорганизовать его во что-то, что не использует goto, но отступ быстро выйдет из строя:

char* function() {
    char* result = NULL;
    char* mem = get_memory(100);
    if(mem) {
        struct binder* b = get_binder('regular binder');
        if(b) {
            struct file* f = mk_file();
            if (f) {
                // ...
            }
            free(b);
        }
        free(mem);
    }
    return result;
}

BTW, рассеивая локальные объявления переменных вокруг блока, как это не является стандартным C.

Теперь, если вы понимаете, что free(NULL); определяется стандартом C как ничто, вы можете упростить вложение:

char* function() {
    char* result = NULL;

    char* mem = get_memory(100);
    struct binder* b = get_binder('regular binder');
    struct file* f = mk_file();

    if (mem && b && f) {
        // ...
    }

    free(f);
    free(b);
    free(mem);

    return result;
}

Ответ 3

Пока я восхищаюсь вашим подходом к защитному кодированию, и это хорошо. И каждый программист C должен иметь этот менталитет, он может применяться и к другим языкам...

Я должен сказать, что это одно полезное в GOTO, несмотря на то, что пуристы скажут иначе, что будет эквивалентом окончательного блока, но есть одна конкретная проблема, которую я вижу там...

karlphillip, код почти завершен, но.... предположим, что функция была выполнена следующим образом

    char* function() {
      struct file* f = mk_file();  // third allocation
      if (!f) goto release_resources;
      // DO WHATEVER YOU HAVE TO DO.... 
      return some_ptr;
   release_resources:
      free(mem);
      free_binder(b);
      return NULL;
    }

Будьте осторожны! Это будет зависеть от дизайна и цели функции, которую вы сочтете нужным, отложите в сторону. Если бы вы вернулись с такой функции, вы могли бы упасть через метку release_resources.... что могло бы вызывают тонкую ошибку, все ссылки на указатели на кучу исчезнут и могут в конечном итоге вернуть мусор... поэтому убедитесь, что если у вас выделена память и верните ее обратно, используйте ключевое слово return непосредственно перед меткой в ​​противном случае память может исчезнуть... или создать утечку памяти....

Ответ 4

Вы также можете использовать противоположный подход и проверить успех:

struct binder* b = get_binder('regular binder');  // second allocation
if(b) {
   struct ... *c = ...
   if(c) {
      ...
   }
   free(b);
}

Ответ 5

Если вы хотите сделать это без goto, вот подход, который хорошо масштабируется:

char *function(char *param)
{
    int status = 0;   // valid is 0, invalid is 1
    char *result = NULL;
    char *mem = NULL:
    struct binder* b = NULL;
    struct file* f = NULL:

    // check function parameter(s) for validity
    if (param == NULL)
    {
        status = 1;
    }

    if (status == 0)
    {
        mem = get_memory(100);  // first allocation

        if (!mem)
        {
            status = 1;
        }
    }

    if (status == 0)
    {
        b = get_binder('regular binder');  // second allocation

        if (!b)
        {
             status = 1;
        }
    }

    if (status == 0)
    {
        f = mk_file();  // third allocation

        if (!f)
        {
             status = 1;
        }
    }

    if (status == 0)
    {
        // do some useful work
        // assign value to result
    }

    // cleanup in reverse order
    free(f);
    free(b);
    free(mem);

    return result;
}

Ответ 6

Если ваши структуры данных сложны/вложены, единственного goto может не хватить, и в этом случае я предлагаю что-то вроде:

mystruct = malloc(sizeof *mystruct);
if (!mystruct) goto fail1;
mystruct->data = malloc(100);
if (!mystruct->data) goto fail2;
foo = malloc(sizeof *foo);
if (!foo) goto fail2;
...
return mystruct;
fail2:
free(mystruct->data);
fail1:
free(mystruct);

Пример в реальном мире будет более сложным и может включать несколько уровней вложенности структуры, связанных списков и т.д. Обратите внимание, что free(mystruct->data); нельзя вызвать (поскольку разыменование элемента mystruct является недопустимым), если первый malloc не удалось.