Manejo de excepciones
Las excepciones son en realidad errores durante la ejecución. Si uno de esos errores se produce y no implementamos el manejo de excepciones, el programa sencillamente terminará abruptamente. Es muy probable que si hay ficheros abiertos no se guarde el contenido de los buffers, ni se cierren, además ciertos objetos no serán destruidos, y se producirán fugas de memoria.
En programas pequeños podemos prever las situaciones en que se pueden producir excepciones y evitarlos. Las excepciones más habituales son las de peticiones de memoria fallidas.
Veamos este ejemplo, en el que intentamos crear un array de cien millones de enteros:
#include <iostream> using namespace std; int main() { int *x = NULL; int y = 100000000; x = new int[y]; x[10] = 0; cout << "Puntero: " << (void *) x << endl; delete[] x; return 0; } |
El sistema operativo se quejará, y el programa terminará en el momento que intentamos asignar un valor a un elemento del array.
Podemos intentar evitar este error, comprobando el valor del puntero después del new:
#include <iostream> using namespace std; int main() { int *x = 0; int y = 100000000; x = new int[y]; if(x) { x[10] = 0; cout << "Puntero: " << (void *) x << endl; delete[] x; } else { cout << "Memoria insuficiente." << endl; } return 0; } |
Pero esto tampoco funcionará, ya que es al procesar la sentencia que contiene el operador new cuando se produce la excepción. Sólo nos queda evitar peticiones de memoria que puedan fallar, pero eso no es previsible.
Sin embargo, C++ proporciona un mecanismo más potente para detectar errores de ejecución: las excepciones. Para ello disponemos de tres palabras reservadas extra: try, catch y throw, veamos un ejemplo:
#include <iostream> using namespace std; int main() { int *x; int y = 100000000; try { x = new int[y]; x[0] = 10; cout << "Puntero: " << (void *) x << endl; delete[] x; } catch(std::bad_alloc&) { cout << "Memoria insuficiente" << endl; } return 0; } |
La manipulación de excepciones consiste en transferir la ejecución del programa desde el punto donde se produce la excepción a un manipulador que coincida con el motivo de la excepción.
Como vemos en este ejemplo, un manipulador consiste en un bloque try, donde se incluye el código que puede producir la excepción.
A continuación encontraremos uno o varios manipuladores asociados al bloque try, cada uno de esos manipuladores empiezan con la palabra catch, y entre paréntesis una referencia o un objeto.
En nuestro ejemplo se trata de una referencia a un objeto bad_alloc, que es el asociado a excepciones consecuencia de aplicar el operador new.
También debe existir una expresión throw, dentro del bloque try. En nuestro caso es implícita, ya que se trata de una excepción estándar, pero podría haber un throw explícito, por ejemplo:
#include <iostream> using namespace std; int main() { try { throw 'x'; // valor de tipo char } catch(char c) { cout << "El valor de c es: " << c << endl; } catch(int n) { cout << "El valor de n es: " << n << endl; } return 0; } |
El throw se comporta como un return. Lo que sucede es lo siguiente: el valor devuelto por el throw se asigna al objeto del catch adecuado. En este ejemplo, al tratarse de un carácter, se asigna a la variable 'c', en el catch que contiene un parámetro de tipo char.
En el caso del operador new, si se produce una excepción, se hace un throw de un objeto de la clase std::bad_alloc, y como no estamos interesados en ese objeto, sólo usamos el tipo, sin nombre.
El manipulador puede ser invocado por un throw que se encuentre dentro del bloque try asociado, o en una de las funciones llamadas desde él.
Cuando se produce una excepción se busca un manipulador apropiado en el rango del try actual. Si no se encuentra se retrocede al anterior, de modo recursivo, hasta encontrarlo. Cuando se encuentra se destruyen todos los objetos locales en el nivel donde se ha localizado el manipulador, y en todos los niveles por los que hemos pasado.
#include <iostream> using namespace std; int main() { try { try { try { throw 'x'; // valor de tipo char } catch(int i) {} catch(float k) {} } catch(unsigned int x) {} } catch(char c) { cout << "El valor de c es: " << c << endl; } return 0; } |
En este ejemplo podemos comprobar que a pesar de haber hecho el throw en el tercer nivel del try, el catch que lo procesa es el del primer nivel.
Los tipos de la expresión del throw y el especificado en el catch deben coincidir, o bien, el tipo del catch debe ser una clase base de la expresión del throw. La concordancia de tipos es muy estricta, por ejemplo, no se considera como el mismo tipo int que unsigned int.
Si no se encontrase ningún catch adecuado, se abandona el programa, del mismo modo que si se produce una excepción y no hemos hecho ningún tipo de manipulación de excepciones. Los objetos locales no se destruyen, etc.
Para evitar eso existe un catch general, que captura cualquier throw para el que no exista un catch concreto:
#include <iostream> using namespace std; int main() { try { throw 'x'; // } catch(int c) { cout << "El valor de c es: " << c << endl; } catch(...) { cout << "Excepción imprevista" << endl; } return 0; } |
Comentarios