Las excepciones son situaciones anómalas que
requieren un tratamiento especial.
¡¡No tienen por qué ser errores!!
Si se consigue dominar su programación, la calidad
de las aplicaciones que se desarrollen aumentará
considerablemente.
El funcionamiento general del mecanismo de lanzamiento y tratamiento de excepciones es el siguiente:
La correcta programación de excepciones significa diseñar los algoritmos pensando únicamente en la forma habitual en la que deben ejecutarse, manejando las situaciones extraordinarias a parte. De esta manera se consigue un diseño mucho más estructurado, legible, robusto y fácil de mantener.
Ejemplo.
Cuando solicitamos memoria al sistema, lo habitual es que exista suficiente memoria disponible y no haya ningún problema. Pero si queremos realizar una aplicación robusta deberemos de tener en cuenta la eventualidad de que dicha memoria no se conceda, lo cual complica enormemente un sencillo algoritmo.
Veamos una función escrita en C que simplemente intenta asignar memoria dinámica para tres enteros:
void SinExcepciones (void) { int *p1, *p2, *p3; p1 = (int*) malloc(sizeof(int)); if (p1 == NULL) { printf("No hay suficiente memoria"); abort(); } p2 = (int*) malloc(sizeof(int)); if (p2 == NULL) { printf("No hay suficiente memoria"); abort(); } p3 = (int*) malloc(sizeof(int)); if (p3 == NULL) { printf("No hay suficiente memoria"); abort(); } }
Si programamos en C++ y hacemos uso de las excepciones:
void ConExcepciones (void) { int *p1, *p2, *p3; try { p1 = new int; // Comportamiento normal. p2 = new int; p3 = new int; } catch(...){ // Comportamiento excepcional. printf("No hay suficiente memoria"); abort(); } }
El ANSI C++ especifica que si una instrucción new falla (no hay memoria disponible) debe lanzar la excepción bad_alloc (definida en el fichero de cabecera new). Las instrucciones que pueden provocar el lanzamiento de una excepción (la "zona crítica") se encierran en un bloque try. Si alguna de las instrucciones del bloque try provocara una excepción el flujo del programa se dirige al final de ese bloque, buscando un bloque catch que capture la excepción (si no lo encontrara, el programa terminaría -ver sección 6.4-). En este caso, la instrucción catch(...) captura cualquier excepción, y en particular, la excepción bad_alloc. El tratamiento que se efectúa es, en este caso, mostrar un mensaje de error y terminar la ejecución del programa.
Ejemplo.
El siguiente programa (proyecto Division) lee dos valores y calcula el cociente entre ambos. Si el divisor es cero, se intenta realizar una división por cero y el programa lanza una excepción de división por cero (EZeroDivide).
//---------------------------------------------------------------- #include <vcl.h> #pragma hdrstop //---------------------------------------------------------------- WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { AnsiString Valor; float Dividendo, Divisor, Cociente; // Leer datos de entrada: dividendo y divisor Valor = InputBox ("División", "Dividendo", ""); // Leer dividendo y Dividendo = StrToFloat (Valor); // convertirlo. Valor = InputBox ("División", "Divisor", ""); // Leer divisor y Divisor = StrToFloat (Valor); // convertirlo. // Calcular cociente. Zona critica: peligro si Divisor == 0 Cociente = Dividendo / Divisor; // Mostrar el resultado ShowMessage ("Cociente = " + AnsiString(Cociente)); return (0); } //----------------------------------------------------------------
Cuando se produce la excepción, al no estar tratada por el programa, el entorno se hace cargo de ella y muestra la siguiente ventana:
Para proteger la instrucción crítica, recoger y tratar la excepción, introducir la siguiente modificación:
try { Cociente = Dividendo / Divisor; ShowMessage ("Cociente = " + AnsiString(Cociente)); } catch (...) { ShowMessage ("El divisor no puede ser cero."); }
Para ceder el control de la gestión de excepciones a nuestro programa debemos desabilitar la opción por la que el entorno integrado gestiona las interrupciones e indicar que se van a gestionar por el programa: seleccionaremos Tools | Debuggber Options y abriremos la carpeta OS Exceptions para dejarla como se indica en la figura 6.2.
Con esto indicamos al depurador de C++ Builder que nuestro programa se encargará de manejar las excepciones C++ que ocurran.
Ahora, al ejecutar el programa e intoducir el valor 0 como divisor se muestra la ventana mostrada en la figura 6.3
6.1. Lanzamiento de excepciones
El lanzamiento de una excepción se realiza llamando a la función throw(). Cuando se lanza una excepción, en realidad lo que se hace es crear un objeto de la clase que se le indique a throw(), y precisamente será dicho objeto la excepción en sí.
Suele ser muy útil crear clases de excepciones propias para controlar las situaciones anómalas de nuestra aplicación. Por ejemplo,
En ObjGraf.h:
//*************************************************/ // Definicion de la clase EFueraRango // Clase de excepcion por entrar en "fuera de rango": // salirse de los limites del PaintBox al calcular // las nuevas coordenadas de dibujo de la pelota. //*************************************************/ class EFueraRango {};
Con lo que podríamos lanzar una excepción de la siguiente manera:
throw EfueraRango();
Nota: Aunque las excepciones sean clases, en C++ Builder existe la convención de que su nombre empiece por la letra E, y no por T como el resto de las clases.
Aunque en el ejemplo anterior la clase creada para la
excepción, no tiene ningún miembro (propiedades
o métodos), se le pueden añadir.
Éstos servirán para poder incorporar
información en el objeto excepción,
acerca de la situación en la que se produjo
la excepción, que podrá ser utilizada
por la sección de código que lo trate.
Por ejemplo, si tuvieramos un método que lanzara
una excepción por falta de memoria, podría
ser interesante que incorporara una propiedad que indicara
cual era el máximo de memoria disponible cuando
se lanzó la excepción.
6.2. Especificación de excepciones
C++ cuenta con una característica denominada especificación de excepciones, que sirve para enumerar, en una declaración, las excepciones que puede lanzar un método. Si queremos que un método pueda lanzar un determinado tipo de excepción deberemos especificarlo de esta manera.
En ObjGraf.h, en la clase TObjGraf:
//*************************************************/ // Definicion de la clase base TObjGraf //*************************************************/ class TObjGraf { private: int FX; int FY; void SetX (int _X) throw (EFueraRango); // Si hay problemas,crean un void SetY (int _Y) throw (EFueraRango); // objeto de clase EFueraRango virtual int GetAncho (void) = 0; // Metodo virtual puro virtual int GetAlto (void) = 0; // Metodo virtual puro ...
En ObjGraf.cpp:
// Funciones de escritura de las propiedades virtuales X e Y void TObjGraf :: SetX (int _X) throw (EFueraRango) { if (_X < 0) { // Coordenada negativa FX = 0; // Ajustar al margen izquierdo throw EFueraRango(); // Lanzar excepcion } else if (_X > (PaintBox->Width - Ancho)) { // Demasiado alta FX = PaintBox->Width - Ancho; // Ajustar al margen derecho throw EFueraRango(); // Lanzar excepcion } else FX = _X; // Correcto: escribir sin modificar } void TObjGraf :: SetY (int _Y) throw (EFueraRango) { if (_Y < 0) { // Coordenada negativa FY = 0; // Ajustar al margen superior throw EFueraRango(); // Lanzar excepcion } else if (_Y > (PaintBox->Height - Alto)) { // Demasiado alta FY = PaintBox->Height - Alto; // Ajustar al margen inferior throw EFueraRango(); // Lanzar excepcion } else FY = _Y; // Correcto: escribir sin modificar }
6.3. Captura de excepciones
El bloque susceptible de producir alguna excepción ("zona crítica") se encierra en un bloque try, la captura (discriminación de la excepción que se ha producido) se efectúa en una instrucción catch y su procesamiento se realiza a continuación, en el bloque catch.
try {Podemos especificar tantos bloques catch para un bloque try como deseemos, en el momento que ocurra una excepción se ejecutará el bloque catch cuya clase concuerde con la de la excepción. Si se especifica catch (...) se capturará cualquier excepción.
La excepción no tiene porqué lanzarse explícitamente en el bloque try sino que puede ser consecuencia de la ejecución de una función que se ha llamado dentro de ese bloque.
En ObjGraf.cpp:
void TPelota :: Mover (void) { Borrar (); try { X += FDirX * Velocidad; } catch (EFueraRango) { FDirX = -FDirX; }; try { Y += FDirY * Velocidad; } catch (EFueraRango) { FDirY = -FDirY; }; Mostrar (); }
En este ejemplo, la instrucción "crítica":
X += FDirX * Velocidad;se traduce a:
X = X + FDirX * Velocidad;y como X es una propiedad virtual, en realidad se trata de hacer:
SetX (FX + FDirX * Velocidad);donde se observa que es el método SetX() el que puede provocar el lanzamiento de la excepción.
En Ppal.cpp:
TPelota *Pelota;
//---------------------------------------------------------- void __fastcall TPpalFrm::FormCreate(TObject *Sender) { Pelota = new TPelota (PaintBox, clYellow, 120, 70, 25); } //---------------------------------------------------------- void __fastcall TPpalFrm::FormDestroy(TObject *Sender) { delete Pelota; }
//---------------------------------------------------------- void __fastcall TPpalFrm::Timer1Timer(TObject *Sender) { Pelota->Mover(); } //----------------------------------------------------------
NOTA: El entorno debe estar configurado para que el programa gestione las excepciones, no el entorno. Si no fuera sí, seleccionar Tools | Debuggber Options, abrir la carpeta OS Exceptions y dejarla como se indica en la figura 6.2.
En este punto, el proyecto debe estar como se indica en el proyecto Ejemplo3.
El resultado es el mostrado en la figura 6.4:
![]() |
![]() |
6.4. Excepciones no tratadas
Si una excepción que ha sido lanzada no encuentra un manejador apropiado, se llamará a la función terminate(), la cual por defecto realiza un abort(). Pero se puede definir nuestra propia función terminate() con la orden set_terminate().
Algo muy parecido ocurre si un método lanza una excepción de un tipo que no tiene listado en su especificación de excepciones, entonces se llama a la función unexpected() la cual por defecto llama a terminate(). Pero de igual manera que con terminate() podemos definir nuestra propia función unexpected() con set_unexpected().
6.5. Excepciones de la VCL
Si se utilizan componentes de la VCL en las aplicaciones, es necesario comprender el mecanismo de tratamiento de excepciones de la VCL. El motivo es que las excepciones están integradas en numerosas clases y se lanzan automáticamente cuando se presenta un situación no esperada. Si no se realiza un tratamiento de la excepción, la VCL llevará a cabo un tratamiento por defecto. Normalmente, aparece un mensaje que describe el tipo de error que se ha producido.
C++ Builder incorpora un extenso conjunto de clases de excepción integradas que permiten tratar automáticamente errores de división por cero, de E/S de archivos, de conversiones de tipos y otras muchas situaciones anómalas. Todas las clases excepción de VCL se derivan de un objeto raíz denominado Exception, que encapsula las propiedades y métodos fundamentales de todas las excepciones por parte de las aplicaciones.
Las excepciones de tipo VCL, como todas las clases de la VCL deben de localizarse en el heap (creación dinámica), por lo que se deberá tener en cuenta lo siguiente:
Las excepciones pueden pasarse a un bloque catch que toma un parámetro de tipo Exception. Emplear la siguiente sintaxis para capturar las excepciones de la VCL:
catch (const exception_class & exception_variable)
Se especifica la clase de excepción que se desea capturar y se proporciona una variable por medio de la cual hacer referencia a la excepción.
Por ejemplo,
//-------------------------------------------------------- #include <vcl.h> #pragma hdrstop //-------------------------------------------------------- WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { try { throw Exception("Mi excepción VCL"); } catch (const Exception & E) { ShowMessage("Clase de la excepción capturada: " + AnsiString(E.ClassName()) + "\nMensaje: " + E.Message); } return (0); } //--------------------------------------------------------
La sentencia throw del ejemplo anterior crea una instancia de la clase Exception y llama a su constructor. Todas las excepciones que descienden de Exception cuentan con un mensaje que puede presentarse en pantalla, pasarse a los constructores y recuperarse mediante la propiedad Message. El resultado de la ejecución del programa anterior es el mostrado en la figura 6.5
Aunque las excepciones no tienen porque indicar un error en la ejecución de una aplicación, las excepciones de la VCL si suelen ser errores. Veamos una selección de las clases de excepción más utilizadas en C++ Builder.
Clase |
Descripción |
EAccessViolation |
Error de acceso a memoria. |
EDatabaseError |
Especifica un error de acceso a base de datos. |
EDBEditError |
Datos incompatibles con una máscara especificada. |
EDivByZero |
Captura errores de división por cero (para división entera y real, respectivamente). |
EInOutError |
Representa un error de E/S de archivo. |
EInvalidPointer |
Operaciones no válidas con punteros. |
EPrinterError |
Indica un error de impresión. |
Aunque todas estas clases funcionen de la misma manera, cada una de ellas tiene particularidades derivadas de la excepción concreta que indican.
Ejemplo.
El siguiente programa (proyecto Division2) es una ampliación del proyecto Division. En éste se van a discriminar distintas excepciones, más que capturar cualquier excepción.
//--------------------------------------------------------------------- #include <vcl.h> #pragma hdrstop //--------------------------------------------------------------------- WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { AnsiString Valor; float Dividendo, Divisor, Cociente; float sigo = true; while (sigo) { try { // Leer dividendo y convertirlo Valor =InputBox ("Division", "Dividendo", ""); Dividendo = StrToFloat (Valor); if (Dividendo == 0) sigo = false; else { // Leer divisor y convertirlo Valor =InputBox ("Division", "Divisor", ""); Divisor = StrToFloat (Valor); Cociente = (Dividendo / Divisor); ShowMessage ("Cociente = " + AnsiString(Cociente)); } } // try // Se captura si se ha realizado una división por cero. catch (EZeroDivide & ) { ShowMessage ("El divisor no puede ser cero."); } // Más general: se alcanza si no se captura la anterior. catch (Exception & e) { ShowMessage ("Se produjo una excepción: "+AnsiString(e.ClassName())+ "\n " + e.Message); } } // while sigo return (0); } //---------------------------------------------------------------------
Si se intenta realizar una division por cero el resultado es el que se indica en la figura 6.6:
Si se introdujera un dato incorrecto, la función StrToFloat() lanzaría una excepción EConvertError que se captura con el último bloque catch (ver figura 6.7).
Es más, si se produjera algún tipo de desbordamiento también podría controlarse con el último bloque catch (ver figura 6.8).
Si quitáramos el primer bloque catch podría capturarse la interrupción EZeroDivide (ver figura 6.9).
Para finalizar, si añadimos el siguiente bloque catch entre los dos anteriores:
catch (EMathError & e) { ShowMessage ("Operación no válida: " + e.Message); }y consideramos (una parte de) la jerarquía de clases de excepciones:
TObject-->Exception-->EExternal-->EMathError-->(EZeroDivide, EOverflow, ...)
el alumno deberá interpretar el resultado de la figura 6.10:
![]() |
EJERCICIO: Reproductor de sonidos. |