Curso de C++ Builder


Tratamiento de excepciones



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:

Figura 6.1. Ventana de error que surge debido a una excepción EDivByZero no tratada.

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.

Figura 6.2. Ventana Debugger Options

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

Figura 6.3. Ventana resultante de la gestión de la excepción.


 

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 {
       <bloque de instrucciones críticas>
}
catch (<tipo excepción1> <variable1>) {
       <manejador 1>
}
catch (<tipo excepción2> <variable2>) {
       ...
}

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:

  1. Dejar únicamente la siguiente declaración global:
       TPelota *Pelota;
    
  2. Modificar las funciones FormCreate() y FormDestroy() para que queden:

    //----------------------------------------------------------
    
    void __fastcall TPpalFrm::FormCreate(TObject *Sender)
    {
       Pelota = new TPelota (PaintBox, clYellow, 120,  70, 25);
    }
    //----------------------------------------------------------
    
    void __fastcall TPpalFrm::FormDestroy(TObject *Sender)
    {
       delete Pelota;
    }
    
  3. Eliminar el manejador del evento OnPaint del componente PaintBox (borrando su cuerpo solamente).

  4. Añadir a PpalFrm un TTimer (Carpeta System) y fijar Interval=100. En el evento OnTimer poner:

    //----------------------------------------------------------
    
    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:

Figura 6.4. Dos instantes del movimiento de la pelota.

   

 

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.

  1. Clases de Excepciones.
  2. 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.

  3. Tratamiento de Excepciones de la VCL
  4. 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

    Figura 6.5. Ejemplo de lanzamiento y captura de excepción VCL.

  5. Clases de excepción típicas
  6. 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
    EZeroDivide

    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:

Figura 6.6. Ejemplo de división por cero.

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

Figura 6.7. Ejemplo de entrada incorrecta.

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

Figura 6.8. Ejemplos de desbordamiento: A) En la entrada de datos y B) En el cálculo del cociente.

Si quitáramos el primer bloque catch podría capturarse la interrupción EZeroDivide (ver figura 6.9).

Figura 6.9. Otra manera de capturar la división por cero.

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:

Figura 6.10. Otra manera de capturar desbordamientos: A) En la entrada de datos y B) En el cálculo del cociente.


 


EJERCICIO: Reproductor de sonidos.

 



Página principal