Ingeniero en Informática


Manejo de excepciones


MANEJO DE EXCEPCIONES

El manejo de excepciones se basa en que si una función detecta un problema que no puede resolver se lanza (throw) un mecanismo de tratamiento del problema. En general, se llama a una función que atrapará (catch) y se hará cargo de la excepción.

Ejemplo : determinación de los intervalos de un vector

class Vector {

int* p;

int tam;

public:

class intervalo{}; //clase creada para la excepción

int& operator[](int i);

// .....

};

Los objetos de la clase intervalo se emplean para las excepciones y se lanzan de la manera siguiente:

int& Vector::operator[](int i)

{

if ( 0<=i && i<tam ) return p[i];

throw intervalo();

}

Las funciones que deseen detectar si un índice se encuentra fuera del intervalo debe encerrar el código del programa donde desea detectar el intervalo en un enunciado try y luego un manejador de excepciones

void f(Vector& v)

{

//...

try {

hacer_algo(v);

}

catch (Vector::Intervalo) {

// se ejcuta esta sección del programa solo si

// la llamada hacer_algo() llama a

// Vector::operator[]() con un índice erróneo

}

//...... por aquí sigue si no existe error

}

La construcción catch(/*......*/) { // .....} es la que denominamos manejador de excepción. Se puede emplear inmediatamente después de un bloque que comienza con la palabra clave try ó después de otro manejador catch.

Los paréntesis que posee la declaración catch contienen una declaración similar a la declaración de un argumento de función y especifica el tipo de objeto con los cuales se puede entrar al manejador y, excepcionalmente, nombra el argumento.

Si hacer_algo() ó cualquier otra función a la que llama hacer_algo() provoca un error de intervalo en algún Vector el manejador atrapa la excepción y se ejecuta el código de manejo de excepciones. Por ejemplo en el siguiente caso se entraría en el manejo de excepciones :

void hacer_algo(Vector& v)

{

// ...

caida(v);

// ...

}

void caida(Vector& v)

{

v[v.tamaño()+10]; // provoca el error de intervalo

}

El proceso de lanzar y hacerse cargo de una excepción implica examinar la cadena de llamadas que se tiene en la pila hasta encontrar el llamador (throw), en este proceso además se trata de desarrollar y buscar en la pila hasta la función que atrapa (catch). Una vez que la función atrapa la excepción, ésta es procesada, dejando de lado los demas manejadores que puedan existir. Esto significa que solo se llama al manejador activo por el que pasó más recientemente el hilo de control.

Ejemplo en el que no se atrapará a Vector::Intervalo :

int ff(Vector& v)

{

try {

f(v); // f() atrapa Vector::Intervalo

}

catch (Vector::Intervalo) { // el manejo de la

// excepción no llega aquí

// ...

}

}

Si quisieramos manejar varios errores para una misma clase, por ejemplo para la clase Vector podríamos tener lo siguiente :

class Vector {

int* p;

int tam;

public:

enum { max = 32000 };

class intervalo{}; // excepción para el intervalo

class Tamaño {}; // excepción para el tamaño

int& operator[](int i);

// .....

};

Para lanzar una excepción por el tamaño tendríamos :

Vector::Vector(int tam)

{

if (tam<0 || max < tam) throw Tamaño ();

// ...

}

Un usuario de la clase Vector podría utilizar las dos excepciones invocando ambas dentro de un bloque try :

void f()

{

try {

usar_vectores();

}

catch (Vector::Intervalo) {

// ajustar el intervalo de los vectores

// intentarlo de nuevo

}

catch (Vector::Tamaño) {

cerr << “error en el tamaño del vector”;

exit(99);

}

// por aquí sigue normalmente

}

Los manejadores de excepciones podrían estar anidados :

try {

// ...

}

catch (xxii) {

try {

// algo complicado

}

catch (xxiii) {

// falló algo complicado

}

}

Cuando se lanza una excepción se lanza un objeto, si se necesita alguna información adicional cuando se lanza la excepción, se podría hacer colocando datos en ese objeto :

class Vector {

int* p;

int tam;

public:

enum { max = 32000 };

class intervalo{

public :

int índice;

Intervalo(int i) : índice(i) {}

} ; // excepción para el intervalo

class Tamaño {}; //excepción creada para el tamaño

int& operator[](int i);

// .....

};

Para conocer el índice erróneo el manejador debe asignar un nombre al objeto de excepción:

void f (Vector& v)

{

// ...

try {

hacer_algo(v);

}

catch (Vector::Intervalo r) {

cerr<< “índice incorrecto”<< r.índice ,, `\n' ;

// ...

}

}

Muchas veces las excepciones pertenecen a una familia, por ejemplo podemos tener un conjunto de manejo de excepciones para manejar errores matemáticos, que incluyan desborde, insuficiencia, división por cero, etc. ,.

Enum Errmatem { Desborde, Insuficiencia, Diventrecero, /*...*/};

try { ... }

catch (Errmatem m) {

switch (m) {

case Desborde:

// ...

case Insuficiencia:

// ...

case Diventrecero:

}

}

Otra manera de implementar esto es por medio de la utilización de clases y herencia :

class Errmatem { };

class Desborde: public Errmatem { };

class Insuficiencia: public Errmatrm { };

class Diventrecero: public Errmatem{ };

// ...

De esta forma uno podría manejar cualquier Errmatem sin preocuparse del error que se trate :

try { ...}

catch(Desborde) { // manejar Desborde o cualquier

// cosa derivada de Desborde

}

catch (Errmatem {

// manejar cualquier Errmatem que no sea Desborde

}

Adquisición de recursos

Con frecuencia cuando se adquiere un recurso como por ejemplo cuando se abre un archivo, se asigna memoria al almacenamiento disponible, se establece un bloqueo para controlar un acceso, etc., es de vital importancia para el futuro de la ejecución de un programa que ese recurso se libere correctamente, ejemplo :

void usar_archivo(const char* fn)

{

FILE * f = fopen (fn,”r”);

// Hacer uso del archivo f

fclose(f);

}

si sucede algo entre el fopen() y el fclose() una excepción puede hacer que salga de la función usar_archivo sin llamar a fclose(). Una primera solución que se nos podría ocurrir es la siguiente :

void usar_archivo(const char* fn)

{

FILE * f = fopen (fn,”r”);

try {

// Hacer uso del archivo f

}

catch (...) { // aquí se atrapa la excepción se

fclose(f); // cierra el archivo y se

throw; // vuelve a lanzar la excepción

}

fclose(f);

}

El problema del manejo de excepción anterior es que lleva mucho código y, por lo tanto, es costosa. La manera general de tratar este problema es la siguiente :

void adquirir ()

{

// adquirir recurso 1

// ...

// adquirir recurso n

// utilizar recursos

// liberar recurso n

// ...

// liberar recurso 1

}

Es importante, como se podrá observar, liberar los recursos en el orden inverso al que se adquirieron, de modo parecido a la creación y destrucción de objetos. Por lo tanto se podrían manejar los problemas de adquisición y liberación de recursos empleando objetos de clases con constructores y destructores.

Class ApuntArch {

FILE* p;

public:

ApuntArch(const char* n, const char* a)

{ p = fopen(n,a); }

ApuntArch(FILE* pp) { p = pp; }

~ApuntArch() { fclose(); }

operator FILE*() { return p; }

}

Podemos construir un objeto ApuntArch dado un File* ó por medio de los argumentos requeridos para un fopen(), el destructor actuará cuando se llegue al final del alcance de ApuntArch. El programa ahora se reduce :

void usar_archivo(const char* fn)

{

ApuntArch f(fn,”r”);

// Hacer uso del archivo f

}

En esta implementación el archivo se cerrará independientemente de si la función sale normalmente o por el lanzamiento de una excepción .

Otro problema que requiere un frecuente tratamiento es cuando fracasa el intento de adquirir recursos, esto puede pasar cuando excedemos la capacidad de abrir archivos o cuando solicitamos memoria del almacenamiento disponible y esta está agotada, ante este tipo de problemas tenemos dos posibilidades :

  • continuar : en este caso “alguien” debe resolver el problema

  • terminar : pedir mas recursos y si no se encuentra ninguno lanzar una excepción

El primer caso de continuar se resuelve por medio de la programación de nuevas funciones que resuelvan el problema, en el segundo caso se resuelve le problema por medio del manejo de excepciones. Este último caso es más sencillo de manejar y resolver.

# include <stdlib.h>

extern void* _last_allocation

extern void* operator new(size_t size) // tamaño

{

void* p;

while ( (p=malloc(size)) ==0 ) {

if (_new_handler) // resuelve le problema de new

(*_new_handler)(); // llamando a esta función

else

return 0;

}

return _last_allocation=p;

}

Si operator new() no puede asignar memoria llama a _new_handler(), si ésta función puede resolver el problema todo esta bien , sino esta rutina no puede volver a llamar a operator new() sin causar un loop infinito. Para este caso se puede optar por llamar a una excepción y dejar que este resuelva el conflicto :

void mi_manejador_new()

{

tratar_de_hallar_memoria();

if(se_halló_memoria()) return;

throw memoria_agotada(); // darse por vencido

}

En algún otro lugar debemos tener :

try { // ...}

catch (memoria_agotada) {

// ...

}

Universidad Tecnológica Nacional - Santa Fe - Departamento Sistemas -

Curso : Desarrollos de Programación en C++




Descargar
Enviado por:María
Idioma: castellano
País: España

Te va a interesar