Manejo de Excepciones en C/C++. En programación los errores suceden: intentos de dividir por cero, acceso a archivos inexistentes, operaciones sobre estructuras vacías, etc. Ignorar estos errores o no gestionarlos correctamente puede provocar que una aplicación se bloquee o arroje resultados incorrectos.
Para gestionar estos casos de forma segura y hacer los programas sean robustos y carentes de errores, C++ proporciona un mecanismo conocido como manejo de excepciones.
Este sistema permite a los programas:
- Detectar errores en tiempo de ejecución.
- Capturarlos de forma controlada.
- Actuar en consecuencia sin interrumpir bruscamente la ejecución del resto del programa.
En este capítulo exploraremos:
- La sintaxis básica del manejo de excepciones (
try
,catch
,throw
). - Cómo capturar excepciones específicas y tratar diferentes tipos de errores de manera diferenciada.
- Cómo crear nuestras propias excepciones personalizadas.
Empezamos!!!!
12.1 try
, catch
y throw
en C++
¿Qué son?
El manejo de excepciones en C++ se basa en tres palabras clave fundamentales:
try
: Indica el bloque de código que puede producir un error (una excepción).throw
: Utilizado para lanzar una excepción (error) cuando ocurre un error.catch
: Define el bloque que captura y maneja la excepción lanzada (acción).
Estructura básica
try {
// Código que puede lanzar una excepción
throw valor_o_objeto; // Se lanza una excepción
}
catch (tipo valor) {
// Código para manejar la excepción
}
Cuando se lanza una excepción con throw
, el flujo de ejecución salta automáticamente al bloque catch
correspondiente, y el resto del bloque try
ya no se ejecuta.
Veamoslo mejor con un ejemplo.
#include <iostream>
using namespace std;
int main() {
try {
int divisor = 0;
if (divisor == 0)
throw "Error: división por cero";
int resultado = 10 / divisor;
cout << "Resultado: " << resultado << endl;
}
catch (const char* mensaje) {
cout << "Se ha capturado una excepción: " << mensaje << endl;
}
return 0;
}
¿Qué tenemos aquí?
- Dentro del bloque
try
, se detecta quedivisor == 0
. - En lugar de hacer la división y provocar un error en tiempo de ejecución, se lanza una excepción con
throw "Error: división por cero"
. - El control pasa automáticamente al bloque
catch
, que recibe unconst char*
(una cadena de texto). - Se imprime el mensaje de error sin que el programa se cierre abruptamente.
Lanzar diferentes tipos de excepciones
C++ nos da la posibilidad de lanzar distintos tipos de valores según la situación:
throw 404; // Entero
throw 3.14; // Número decimal
throw "Error"; // Cadena de texto
throw runtime_error("Archivo no encontrado"); // Objeto de clase
Y, para manejarlos, debes usar un catch
que coincida:
catch (int codigo) { ... }
catch (double valor) { ... }
catch (const char* mensaje) { ... }
catch (const exception& e) { ... }
catch genérico
Cuando no sabemos qué tipo de excepción puede lanzarse, podemos usar un bloque genérico:
catch (...) {
cout << "Se ha producido una excepción desconocida." << endl;
}
Ejemplo
#include <iostream>
#include <stdexcept>
using namespace std;
void dividir(int a, int b) {
if (b == 0)
throw runtime_error("No se puede dividir por cero");
cout << "Resultado: " << a / b << endl;
}
int main() {
try {
dividir(10, 0);
}
catch (const runtime_error& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}
- Se usa una función que puede lanzar una excepción.
runtime_error
es una clase de excepción estándar de C++.- La función
e.what()
devuelve el mensaje de error que se pasó en elthrow
.
Recuerda
Es importante utilizar el manejo de execpciones con try
, catch
, throw
ya que
- Evita que el programa se bloquee inesperadamente.
- Separa claramente el código normal del código de gestión de errores.
- Mejora la robustez y mantenibilidad del software.
12.2 Captura de Excepciones Específicas
En C++, cuando se lanza una excepción mediante throw
, podemos capturarla en bloques catch
que coincidan con su tipo exacto. Esto nos permite reaccionar de manera diferente ante distintos tipos de errores, escribiendo código controlado.
¿Por qué capturar tipos específicos?
La captura de tipos específicos nos permite:
- Identificar el origen y naturaleza del error con mayor exactitud.
- Tomar decisiones diferentes según el tipo de excepción lanzada.
- Mantener el programa más robusto y organizado.
Sintaxis
try {
// Código que puede lanzar diferentes tipos de excepciones
}
catch (int errorCode) {
// Manejo si se lanza un int
}
catch (const char* mensaje) {
// Manejo si se lanza un const char*
}
catch (const exception& e) {
// Manejo para errores estándar de C++
}
Veamos un ejemplo
#include <iostream>
#include <stdexcept>
using namespace std;
void lanzarError(int tipo) {
if (tipo == 1)
throw 404;
else if (tipo == 2)
throw "Error de tipo cadena";
else if (tipo == 3)
throw runtime_error("Excepción estándar");
}
int main() {
try {
lanzarError(3); // Prueba con 1, 2 o 3
}
catch (int codigo) {
cout << "Error numérico capturado: " << codigo << endl;
}
catch (const char* mensaje) {
cout << "Error de texto capturado: " << mensaje << endl;
}
catch (const exception& e) {
cout << "Excepción estándar capturada: " << e.what() << endl;
}
return 0;
}
En este código:
lanzarError
lanza un tipo distinto de excepción según el parámetro.- El bloque
main
contiene varioscatch
, cada uno preparado para un tipo específico:int
: para códigos numéricos.const char*
: para mensajes de texto.const exception&
: para excepciones estándar comoruntime_error
,logic_error
, etc.
- Se ejecutará únicamente el bloque
catch
que coincida con el tipo lanzado.
¿Qué pasa si no hay coincidencia?
Si se lanza una excepción y no existe un bloque catch
que coincida con su tipo, el programa termina abruptamente (lanza una excepción no manejada).
Por eso es recomendable, en ciertos casos, tener un bloque genérico al final:
catch (...) {
cout << "Excepción no identificada capturada." << endl;
}
Este actúa como un «filtro de seguridad» para cualquier excepción no prevista.
@Tip
- Ordena los
catch
desde el más específico al más general. - Si usas clases de excepción derivadas, captura primero las hijas antes que las clases base.
- Usa
const &
en la captura para evitar copias innecesarias de objetos grandes.
Truco
También es posible re-lanzar una excepción si un catch
no puede manejarla del todo:
catch (const exception& e) {
cout << "Capturado: " << e.what() << endl;
throw; // Re-lanza la misma excepción
}
12.3 Excepciones Personalizadas
En C++, además de capturar excepciones predefinidas como std::runtime_error
o std::out_of_range
, también podemos definir nuestras propias clases de excepción. Esto es útil cuando queremos representar errores específicos de nuestra aplicación o sistema con información particular y más concreta.
Casosn en los que crear excepciones personalizadas:
- Para representar errores concretos de nuestro dominio (por ejemplo,
ArchivoNoEncontrado
,SaldoInsuficiente
, etc.). - Para añadir información adicional al error (como código de error, contexto, fecha…).
- Para hacer el código más claro y autoexplicativo.
Cómo crear una excepción personalizada
Para crear una excepción propia:
- Creamos una clase que herede de
std::exception
(u otra clase de excepción). - Sobrescribimos el método
what()
para dar un mensaje descriptivo.
Veamos como hacerlo.
#include <iostream>
#include <exception>
using namespace std;
class MiExcepcion : public exception {
public:
const char* what() const noexcept override {
return "¡Se produjo un error personalizado!";
}
};
int main() {
try {
throw MiExcepcion();
}
catch (const MiExcepcion& e) {
cout << "Excepción capturada: " << e.what() << endl;
}
return 0;
}
MiExcepcion
hereda destd::exception
.- Sobrescribimos
what()
para devolver un mensaje propio. - En
main
, lanzamos la excepción conthrow MiExcepcion()
. - Se captura en un
catch
que reconoce el tipo exacto.
Para añadir información adicional lo haríamos así:
#include <iostream>
#include <exception>
#include <string>
using namespace std;
class ArchivoNoEncontrado : public exception {
private:
string mensaje;
public:
ArchivoNoEncontrado(const string& nombreArchivo) {
mensaje = "Archivo no encontrado: " + nombreArchivo;
}
const char* what() const noexcept override {
return mensaje.c_str();
}
};
int main() {
try {
throw ArchivoNoEncontrado("datos.txt");
}
catch (const ArchivoNoEncontrado& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}
Si te fijas, con este enfoque
- Podemos crear excepciones con nombres significativos y comportamiento personalizado.
- Podemos pasar parámetros (nombre del archivo, código, etc.).
- Se integran perfectamente en el sistema de manejo de excepciones con
try/catch
.
Como recomendaciones te sugirero que.
- Hereda de
std::exception
o una de sus derivadas. - Siempre sobrescribe
what()
y marca la función comonoexcept
. - Puedes crear una jerarquía de excepciones propias si tu programa es grande.
Combinación con excepciones estándar
También puedes lanzar excepciones estándar personalizadas usando las clases de la STL:
hrow std::invalid_argument("Parámetro no válido");
throw std::out_of_range("Índice fuera de rango");
Estas ya están preparadas para uso general y tienen sus propios mensajes.
Conclusión
Hasta aquí este breve capítulo donde hemos aprendido a gestionar errores en tiempo de ejecución utilizando el sistema de excepciones de C++. Vimos cómo:
- Usar los bloques
try
,catch
ythrow
para capturar y manejar errores. - Capturar excepciones específicas, como aquellas lanzadas por la biblioteca estándar (
std::runtime_error
,std::invalid_argument
, etc.). - Crear excepciones personalizadas mediante clases propias que heredan de
std::exception
, permitiéndonos generar errores más descriptivos y adaptados a nuestras aplicaciones.
Vamos ahora a practicar.
Ejercicio 1: División segura
Enunciado:
Crea una función que reciba dos enteros y devuelva el resultado de dividir el primero entre el segundo. Si el divisor es cero, lanza una excepción y captúrala en la función main
, mostrando un mensaje adecuado al usuario.
Enunciado:
Crea una función que reciba dos enteros y devuelva el resultado de dividir el primero entre el segundo. Si el divisor es cero, lanza una excepción y captúrala en la función
main
, mostrando un mensaje adecuado al usuario.Código:
#include <iostream>
#include <stdexcept>
double dividir(int a, int b) {
if (b == 0)
throw std::runtime_error("Error: división por cero no permitida.");
return static_cast<double>(a) / b;
}
int main() {
int x = 10;
int y = 0;
try {
double resultado = dividir(x, y);
std::cout << "Resultado: " << resultado << std::endl;
} catch (const std::runtime_error& e) {
std::cerr << "Excepción capturada: " << e.what() << std::endl;
}
return 0;
}
Explicación:
La función dividir
lanza una excepción std::runtime_error
si el divisor es cero. En main
, se captura y muestra el mensaje de error. Se evita que el programa se cierre abruptamente.
Ejercicio 2: Acceso seguro a un vector
Enunciado:
Crea un programa que almacene 5 enteros en un std::vector
y permita al usuario acceder a una posición concreta. Si el usuario introduce un índice fuera de rango, lanza y captura la excepción apropiada.
Enunciado:
Crea un programa que almacene 5 enteros en un
std::vector
y permita al usuario acceder a una posición concreta. Si el usuario introduce un índice fuera de rango, lanza y captura la excepción apropiada.Código:
cppCopyEdit#include <iostream>
#include <vector>
#include <stdexcept>
int main() {
std::vector<int> numeros = {1, 2, 3, 4, 5};
int indice;
std::cout << "Introduce un índice entre 0 y 4: ";
std::cin >> indice;
try {
std::cout << "Número en la posición " << indice << ": " << numeros.at(indice) << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << "Excepción capturada: " << e.what() << std::endl;
}
return 0;
}
Explicación:
El método at()
del vector lanza automáticamente una excepción std::out_of_range
si el índice es inválido. Al capturarla, evitamos que el programa se bloquee y mostramos una advertencia.
Ejercicio 3: Excepción personalizada para edad no válida
Enunciado:
Crea una clase EdadInvalidaException
que se use para lanzar un error cuando se intente registrar a una persona con una edad negativa o mayor a 130 años.
Enunciado:
Crea una clase
EdadInvalidaException
que se use para lanzar un error cuando se intente registrar a una persona con una edad negativa o mayor a 130 años.SCódigo:
cppCopyEdit#include <iostream>
#include <exception>
#include <string>
class EdadInvalidaException : public std::exception {
std::string mensaje;
public:
EdadInvalidaException(int edad) {
mensaje = "Edad inválida: " + std::to_string(edad);
}
const char* what() const noexcept override {
return mensaje.c_str();
}
};
void registrarPersona(int edad) {
if (edad < 0 || edad > 130)
throw EdadInvalidaException(edad);
std::cout << "Persona registrada con edad: " << edad << std::endl;
}
int main() {
try {
registrarPersona(150);
} catch (const EdadInvalidaException& e) {
std::cerr << "Excepción personalizada capturada: " << e.what() << std::endl;
}
return 0;
}
Explicación:
Se define una clase de excepción personalizada que hereda de std::exception
. Cuando se detecta una edad inválida, se lanza esta excepción con un mensaje detallado. La función main
la captura y muestra el error.
Bibliografía del tutorial de C/C++.
- C/C++. Curso de programación. Autor: Miguel Angel Acera (Editorial: Anaya Multimedia)
- C/C++. Curso de programación. Autor: Francisco José Ceballos (Editorial: RA-MA)
- Un recorrido por C++. Autor Bjarne Stroustrup (Editorial: Anaya Multimedia)
- 115 Ejercicios resueltos de programación C++. Autor Jorge Fernando Betancourt e Inma Yolanda Polanco (Editorial: RA-MA)