¿Cómo evitar una cadena larga de free (o borrados) después de cada verificación de error en C?

Supongamos que escribo mi código de manera defensiva y siempre verifico los tipos de retorno de todas las funciones a las que llamo.

Así que voy como

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

Fíjate en lo rápido que free() cosas se salen de control. Si alguna de las funciones falla, tengo que liberar todas las asignaciones antes. El código rápidamente se vuelve feo y todo lo que hago es copiar y pegar todo. Me convierto en un progtwigdor de copiar / pegar, aún peor, si alguien agrega una statement en algún punto intermedio, tiene que modificar todo el código a continuación para llamar al free() para su adición.

¿Cómo resuelven este problema los progtwigdores en C experimentados? No puedo resolver nada.

Gracias, Boda Cydo.

Podría definir una nueva etiqueta que liberaría los recursos y luego podría GOTO cada vez que su código falla.

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

La gestión de errores con GOTO ya se trató aquí: ¿ Uso válido de goto para la gestión de errores en C?

Sé que me lincharán por esto, pero tuve un amigo que dijo que usaron goto para eso.

Luego me dijo que no era suficiente en la mayoría de los casos y ahora usaba setjmp() / longjmp() . Básicamente reinventó las excepciones de C ++ pero con mucho menos elegancia.

Dicho esto, dado que goto podría funcionar, podría refactorizarlo en algo que no use goto , pero la sangría se saldrá de las manos rápidamente:

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

Por cierto, la dispersión de las declaraciones de variables locales alrededor del bloque como que no es estándar C.

Ahora, si te das cuenta de eso free(NULL); Está definido por el estándar C como un “no hacer nada”, puede simplificar el anidamiento de algunos:

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

Aunque admiro tu enfoque de encoding defensiva y eso es algo bueno. Y cada progtwigdor de C debería tener esa mentalidad, también puede aplicarse a otros idiomas …

Tengo que decir que esto es lo único útil de GOTO, a pesar de que los puristas dirán lo contrario, eso sería un equivalente a un bloque finalmente, pero hay un particular que puedo ver allí …

El código de karlphillip está casi completo, pero … suponga que la función se realizó de esta manera

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

¡¡¡Ten cuidado!!! Esto dependería del diseño y el propósito de la función que considere conveniente, que se dejaría de lado … si regresara de la función de esa manera, podría terminar cayendo a través de la etiqueta release_resources … lo que podría inducir una error sutil, todas las referencias a los punteros en el montón han desaparecido y podrían terminar devolviendo basura … así que asegúrese de tener la memoria asignada y devuélvala, use la palabra clave return inmediatamente antes de la etiqueta, de lo contrario la memoria podría desaparecer … o crear una pérdida de memoria ….

También puede tomar el enfoque opuesto y verificar el éxito:

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

Si sus estructuras de datos son complicadas / anidadas, un solo goto podría no ser suficiente, en cuyo caso sugiero algo como:

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

Un ejemplo del mundo real sería más complicado y podría incluir múltiples niveles de anidamiento de estructuras, listas vinculadas, etc. Tenga en cuenta que free(mystruct->data); no se puede llamar (porque la desreferenciación de un elemento de mystruct no es válida) si el primer malloc falló.

Si quieres hacerlo sin goto , aquí hay un enfoque que se adapta bien:

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