Как избежать длинной цепочки свободных (или удалений) после каждой проверки ошибок в 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
не удалось.