Manejo de Excepciones en C++: Guía Completa

Crear programas es una tarea que demanda mucho esfuerzo mental. Los programadores no solo necesitan un extenso período de estudio y entrenamiento del pensamiento, sino que también deben ser extremadamente cuidadosos con cada detalle y límite. Incluso así, los errores en los programas son inevitables.

Los errores en un programa se pueden dividir en tres categorías principales: errores de sintaxis, errores lógicos y errores en tiempo de ejecución:

  1. Los errores de sintaxis se detectan durante la compilación y el enlace. Solo el código que cumple completamente con las reglas de sintaxis puede generar un ejecutable. Este tipo de errores son los más fáciles de encontrar, localizar y corregir; son los que menos deben preocupar a los programadores.
  2. Los errores lógicos ocurren cuando el código escrito no logra el objetivo deseado. Estos errores se pueden resolver mediante depuración.
  3. Los errores en tiempo de ejecución son aquellos que suceden durante la ejecución del programa, como división por cero, falla en la asignación de memoria, acceso indebido a índices de arreglos, archivos inexistentes, entre otros. El mecanismo de excepciones de C++ fue diseñado precisamente para manejar este tipo de errores.

Si los errores en tiempo de ejecución no se manejan, el sistema ejecutará la acción predeterminada: terminar el programa, lo que comúnmente conocemos como un "crash" o colapso del programa. C++ proporciona un mecanismo de excepciones que nos permite capturar estos errores y dar al programa una oportunidad de "recoverarse", o al menos informar al usuario qué ocurrió antes de terminar la ejecución.

Ejemplo 1: Un programa que genera un error en tiempo de ejecución:

#include <iostream>
#include <string>
using namespace std;

int main(){
    string mensaje = "Bienvenido al tutorial de C++";
    char caracter1 = mensaje[50]; //Índice fuera de rango, valor inesperado
    cout << caracter1 << endl;
    char caracter2 = mensaje.at(50); //Índice fuera de rango, lanza excepción
    cout << caracter2 << endl;
    return 0;
}

Al ejecutar el código, después de mostrar el valor de caracter1 el programa colapsa. Analicemos la razón.

La función at() es un método de la clase string que retorna un carácter según el índice proporcionado. A diferencia del operador [], at() verifica si el índice está dentro de los límites; si no lo está, lanza una excepción. El operador [] no realiza ninguna verificación y accede directamente sin importar si el índice es válido.

Lanzar una excepción significa报告 un error en tiempo de ejecución, lo cual permite al programador tomar decisiones basadas en esa información.

En el código anterior, el índice 50 claramente excede la longitud del string mensaje. La línea 6 no verifica el índice, por lo que aunque existe un error lógico, el programa continúa. Sin embargo, la línea 8 es diferente: la función at() detecta que el índice está fuera de rango y lanza una excepción. Como no la manejamos en nuestro código, el sistema ejecuta la acción predeterminada, que es terminar el programa.

Captura de Excepciones

Podemos utilizar el mecanismo de excepciones de C++ para capturar la excepción mencionada y evitar que el programa colapse. La sintaxis para capturar excepciones es:

try{
    // Sentencias que pueden lanzar excepciones
}catch(tipoExcepcion variable){
    // Sentencias para manejar la excepción
}

Las palabras clave try y catch van seguidas de un bloque de código, y los corchetes {} son obligatorios. El bloque try contiene las sentencias que podrían lanzar una excepción; si se detecta alguna, será capturada por el catch correspondiente. Como sugiere su nombre, try simplemente "detecta" si hay excepciones en el bloque; si no ocurre ninguna, no "ve" nada. Catch significa "atrapar", y se utiliza para capturar y procesar las excepciones detectadas por try. Si no se lanza ninguna excepción en el bloque try, las sentencias en catch no se ejecutarán.

Es como si catch le dijera a try: "Ve a verificar si hay errores en el programa; si los hay, dímelo y yo me encargaré de manejarlos; si no los hay, no me molestes".

La sintaxis tipoExcepcion variable después de catch especifica qué tipo de excepciones puede manejar el bloque catch y proporciona información específica sobre el error. Más adelante explicaremos los tipos de excepciones en detalle. Por ahora, demostraremos cómo usar try-catch para dar al lector una comprensión general.

Ejemplo 2: Modificar el código anterior para incluir captura de excepciones:

#include <iostream>
#include <string>
#include <exception>
using namespace std;

int main(){
    string mensaje = "Bienvenido al tutorial de C++";

    try{
        char caracter1 = mensaje[50];
        cout << caracter1 << endl;
    }catch(exception& e){
        cout << "[1]¡Índice fuera de rango!" << endl;
    }

    try{
        char caracter2 = mensaje.at(50);
        cout << caracter2 << endl;
    }catch(exception& e){
        cout << "[2]¡Índice fuera de rango!" << endl;
    }

    return 0;
}

Resultado: ( [2]¡Índice fuera de rango!

El primer try no capturó ninguna excepción y mostró un carácter sin sentido (valor de basura). Como [] no verifica índices fuera de rango, no lanza excepciones, por lo que try no detecta ningún problema. En otras palabras, para que try pueda detectar una excepción, esta debe ser explícitamente lanzada. Si no se lanza, aunque exista un problema, try no la detectará.

El segundo try sí detectó la excepción y la envió a catch para su procesamiento. Es importante señalar que una vez que se lanza una excepción, se detecta inmediatamente por try y las sentencias posteriores al punto donde ocurrió la excepción ya no se ejecutna. En este ejemplo, el punto donde se lanza la excepción es la función at() en la línea 17, y la siguiente instrucción cout no se ejecutará.

En términos sencillos, al detectra una excepción, el flujo de ejecución salta desde el punto donde ocurrió hasta la ubicación del catch. Las sentencias posteriores al punto de excepción dentro del bloque try no se ejecutarán. Incluso si catch maneja exitosamente el error, el flujo no regresa al punto de excepción, por lo que esas sentencias nunca tendrán oportunidad de ejecutarse. En este ejemplo, la línea 18 es la sentencia que se salta.

Después de ejecutar el código dentro del bloque catch, el programa continúa con las sentencias que siguen a catch,恢复了 el flujo normal de ejecución.

Para demostrar que "si no se lanza una excepción explícitamente, no se puede detectar", puede cambiar la línea 10 a char caracter1 = mensaje[100000000];. Acceder al índice 50 quizás no cause problemas, pero acceder al índice 100 millones definitivamente sí, resultando en un error de acceso a memoria. Al ejecutar el programa modificado, la línea 10 generó una excepción que provocó el colapso del programa, demostrando que try-catch no capturó esa excepción.

Sobre "cómo lanzar excepciones", lo explicaremos en la siguiente sección. Lo importante aquí es comprender el flujo de manejo de excepciones:

Lanzar (Throw) → Detectar (Try) → Capturar (Catch)

Ubicación donde Ocurren las Excepciones

Las excepciones pueden ocurrir dentro del bloque try actual, o en alguna función llamada desde el bloque try, o incluso en funciones llamadas por esas funciones. Todas estas excepciones pueden ser detectadas por try.

  1. El siguiente ejemplo muestra una excepción que ocurre directamente en el bloque try:
#include <iostream>
#include <string>
#include <exception>
using namespace std;

int main(){
    try{
        throw "Excepción Desconocida";
        cout << "Esta sentencia no se ejecutará." << endl;
    }catch(const char* &e){
        cout << e << endl;
    }

    return 0;
}

Resultado: Excepción Desconocida

La palabra clave throw se utiliza para lanzar una excepción, que será detectada por try y posteriormente capturada por catch. Sobre el uso de throw, profundizaremos en la siguiente sección. Por ahora, lo importante es saber que las excepciones lanzadas directamente en el bloque try serán detectadas por try.

  1. El siguiente ejemplo muestra una excepción que ocurre en una función llamada desde el bloque try:
#include <iostream>
#include <string>
#include <exception>
using namespace std;

void funcion(){
    throw "Excepción Desconocida";
    cout << "[1]Esta sentencia no se ejecutará." << endl;
}

int main(){
    try{
        funcion();
        cout << "[2]Esta sentencia no se ejecutará." << endl;
    }catch(const char* &e){
        cout << e << endl;
    }

    return 0;
}

Resultado: Excepción Desconocida

La función funcion() es llamada dentro del bloque try, y la excepción que lanza será detectada por try y capturada por catch. Como se puede ver en el resultado, ni el cout dentro de funcion() ni el cout dentro de try se ejecutaron.

  1. El bloque try llama a una función, que a su vez llama a otra función, y esta última lanza una excepción:
#include <iostream>
#include <string>
#include <exception>
using namespace std;

void funcionInterna(){
    throw "Excepción Desconocida";
    cout << "[1]Esta sentencia no se ejecutará." << endl;
}

void funcionExterna(){
    funcionInterna();
    cout << "[2]Esta sentencia no se ejecutará." << endl;
}

int main(){
    try{
        funcionExterna();
        cout << "[3]Esta sentencia no se ejecutará." << endl;
    }catch(const char* &e){
        cout << e << endl;
    }

    return 0;
}

Resultado: Excepción Desconocida

Cuando ocurre una excepción, el flujo de ejecución retrocede a lo largo de la cadena de llamadas hasta encontrar un bloque try. Durante este retroceso, el código restante en la cadena de llamadas (todas las sentencias no ejecutadas en las funciones) se saltará y no tendrá oportunidad de ejecutarse.

Etiquetas: C++ excepciones manejo-de-errores programación templates

Publicado el 9-8 16:25