CAPITULO 16 "MANEJO DE EXCEPCIONES"
16.1. INTRODUCCION.
Una excepción
es la indicación de un problema que ocurre durante la ejecución de un programa.
El nombre “excepción” implica que el problema ocurre con poca frecuencia; si la
“regla” es que una instrucción se ejecuta en forma correcta, entonces la
“excepción a la regla” es cuando ocurre un problema. El manejo de una excepción
permite que un programa continúe su ejecución como si no se hubiera encontrado
un problema. Un problema más grave podría evitar que un programa continuara su
ejecución normal, en vez de requerir al programa que notifique al usuario sobre
el problema antes de terminar de una manera controlada.
16.2. GENERALIDADES ACERCA DEL MANEJO DE EXCEPCIONES.
Con frecuencia, los
programas evalúan condiciones que determinan cómo debe proceder la ejecución
del programa. Considere el siguiente seudocódigo:
Realizar
una tarea
Si
la tarea anterior no se ejecutó correctamente
Realizar
el procesamiento de los errores
Realizar
la siguiente tarea
Si
la tarea anterior no se ejecutó correctamente
Realizar
el procesamiento de los errores
En este seudocódigo,
empezaremos por realizar una tarea; después evaluaremos si se ejecutó en forma
correcta. Si no lo hizo, realizamos el procesamiento de los errores. De otra
manera, continuamos con la siguiente tarea.
El manejo de excepciones
permite al programador remover el código para manejo de errores de la “línea
principal” de ejecución del programa, lo cual mejora la claridad y capacidad de
modificación del mismo. Esta flexibilidad reduce la probabilidad de que los
errores se pasen por alto y, por consecuencia, hace que los programas sean más
robustos.
16.3. EJEMPLO: MANEJO DE UN INTENTO DE DIVIDIR ENTRE
CERO.
El propósito de este
ejemplo es mostrar cómo evitar un problema aritmético común: la división entre
cero. En C++, la división entre cero mediante el uso de aritmética de enteros
por lo general hace que un programa termine en forma prematura. En la
aritmética de punto flotante, ciertas implementaciones de C++ permiten la
división entre cero, en cuyo caso el resultado de infinito positivo o negativo
se muestra como INF o –INF.
En este ejemplo
definimos una función llamada cociente, la cual recibe dos enteros introducidos
por el usuario, y divide su primer parámetro int entre su segundo
parámetro int. Antes de realizar la división, la función convierte el
valor del primer parámetro entero al tipo double. Después, el valor del
segundo parámetro int se promueve al tipo double para el cálculo. Así, la
función cociente en realidad realiza la división utilizando dos valores double
y devuelve un resultado double. Aunque la división entre cero está permitida
en la aritmética de punto flotante, para el propósito de este ejemplo
trataremos un intento de división entre cero como un error. Así, la función
cociente evalúa su segundo parámetro para asegurar que no sea cero antes de
permitir que continúe la división. Si el segundo parámetro es cero, la función
utiliza una excepción para indicar a la función que hizo la llamada que ocurrió
un problema. Así, la función que hizo la llamada puede procesar la excepción y
permitir que el usuario escriba dos nuevos valores, antes de llamar a la
función cociente de nuevo. De esta forma, el programa puede seguir ejecutándose
aun después de que se introduzca un valor inapropiado, con lo cual el programa
se vuelve más robusto.
El ejemplo consiste en
dos archivos. ExcepcionDivisionEntreCero.h define una clase de excepción
que representa el tipo del problema que podría ocurrir en el ejemplo, define la
función cociente y la función main que llama a la clase de excepción.
La función main contiene el código que demuestra el manejo de excepciones.
Definición de una clase de excepción para representar
el tipo del problema que podría ocurrir
Se define la clase ExcepcionDivisionEntreCero
como una clase derivada de la clase runtime_ error (definida en el
archivo de encabezado <stdexcept>).
La clase runtime_error,
una clase derivada de la clase exception de la Biblioteca estándar
(definida en el archivo de encabezado <exception>), es la clase base
estándar de C++ para representar errores en tiempo de ejecución. La clase exception
es la clase base estándar de C++ para todas las excepciones.
Una clase de excepción
típica que se deriva de la clase runtime_error define sólo un constructor
que pasa una cadena de mensaje de error al constructor de la clase
base runtime_error.
Ejemplo:
ExcepcionDivisionEntreCero()
Runtime_error(
“intento de dividir entre cero” ) {}
Cada clase de excepción
que se deriva en forma directa o indirecta de exception contiene la
función virtual what, la cual devuelve el mensaje de error de un objeto
excepción. No es obligatorio derivar una clase de excepción personalizada, esto
permite a los programadores utilizar la función virtual what para obtener un
mensaje de error apropiado.
Demostración del manejo de excepciones.
Se utiliza el manejo de
excepciones para envolver código que podría lanzar una excepción de “división
entre cero” y para manejar esa excepción, en caso de que ocurra una. La
aplicación permite al usuario introducir dos enteros, que se pasan como
argumentos a la función cociente. Esta función divide su primer parámetro
(numerador) entre su segundo parámetro (denominador). Suponiendo que el usuario
no especifica 0 como el denominador para la división, la función cociente
devuelve el resultado de la división. Sin embargo, si el usuario introduce un 0
para el denominador; la función cociente lanza una excepción. Cuando ocurre la
excepción, el programa informa al usuario del error y le pide que introduzca
dos nuevos enteros.
Encerrar código en un bloque try
El programa empieza
pidiendo al usuario que introduzca dos enteros. Estos enteros se introducen en
la condición del ciclo while.
while(cin
>> numero1 >>numero2)
El manejo de
excepciones está orientado a situaciones en las que la función que detecta un
error no puede manejarlo. C++
proporciona bloques try para permitir el manejo de excepciones. Un bloque
try consiste en la palabra clave try, seguida de llaves ({})
que definen un bloque de código en el que podrían ocurrir errores. El bloque
try encierra instrucciones que podrían ocasionar excepciones, e
instrucciones que se deberían omitir si ocurre una excepción.
1.
try{
2.
resultado
= cociente( numero1, numero2 );
3.
cout
<< “El cociente es: ” << resultado << endl;
4.
}
Definición de un manejador catch para procesar una
ExcepcionDivisionEntreCero
Las excepciones se
procesan mediante los manejadores catch (también conocidos como
manejadores de excepciones), que atrapan y manejan las excepciones. Por lo
menos debe haber un manejador catch inmediatamente después de cada
bloque
try. Cada manejador catch empieza con la palabra clave catch
y especifica entre paréntesis un parámetro de excepción que representa
el tipo de excepción que puede procesar el manejador catch (en este caso,
ExcepcionDivisionEntreCero).
Cuando ocurre una
excepción en un bloque try, el manejador catch que se ejecuta es aquél cuyo
tipo coincide con el tipo de la excepción que ocurrió (es decir, el tipo en el
bloque catch coincide exactamente con el tipo de excepción lanzada, o es una
clase base de la misma).
Si un parámetro de
excepción incluye un nombre de parámetro opcional, el manejador catch
puede usar ese nombre de parámetro para interactuar con la excepción atrapada
en el cuerpo del manejador catch, que está delimitado por
llaves ({ y }). Por lo general, un manejador catch reporta el error al
usuario, lo registra en un archivo, termina el programa sin que haya pérdida de
datos o intenta una estrategia alterna para realizar la tarea fallida.
1.
catch(
ExcepcionDivisionEntreCero ÷ByZeroException){
2.
cout
<< “Ocurrio una excepción: ” << divideByZeroException.what()
<< endl;
3.
}
Modelo de terminación del manejo de excepciones
Si ocurre una excepción
como resultado de una instrucción en un bloque try, este bloque expira (es
decir, termina de inmediato).
El programa busca el
primer manejador catch que pueda procesar el tipo de excepción que ocurrió. El
programa localiza el catch que coincida, comparando el
tipo de la excepción lanzada con el tipo del parámetro de excepción de cada catch,
hasta que el programa encuentra una coincidencia. Ocurre una coincidencia si
los tipos son idénticos, o si el tipo de la excepción lanzada es una clase
derivada del tipo del parámetro de excepción. Cuando ocurre una coincidencia,
se ejecuta el código contenido en el manejador catch que coincide.
Cuando un manejador catch
termina su procesamiento al llegar a su llave derecha de cierre (}),
se considera que la excepción se manejó y las variables locales definidas dentro
del manejador catch quedan fuera de alcance. El control del programa no
regresa al punto en el que ocurrió la excepción debido a que el bloque
try ha expirado. A esto se le conoce como modelo de terminación del
manejo de excepciones.
Si el bloque
try completa su ejecución con éxito, entonces el programa ignora los
manejadores catch y el control del programa continúa con la primera
instrucción después del último bloque catch que sigue de ese bloque
try. Si no ocurren excepciones en un bloque try, el programa
ignora el (los) manejador(es) catch para ese bloque. Si una
excepción que ocurre en un bloque try no tiene un manejador catch
que coincida, o si una excepción ocurre en una instrucción que no se encuentre
dentro de un bloque try, la función que contiene la instrucción termina de
inmediato y el programa intenta localizar un bloque try circundante en
la función que hizo la llamada. A este proceso se le conoce como limpieza
de la pila.
Flujo del control del programa cuando el usuario
introduce un denominador distinto de cero.
Considere el flujo de
control cuando el usuario introduce el numerador 100 y el denominador 7. En la
línea siguiente, la función cociente determina que el denominador no es igual a
cero:
if(
denominador == 0 )
Por lo que
acontinuación en la línea se realiza la división y se devuelve el resultado
(14.2857):
return static_cast< double > ( numerador ) /
denominador;
Como el bloque try se
completó con éxito y no lanzó una excepción, el programa no ejecuta las
instrucciones contenidas en el manejador catch, y el control continúa a la
próxima línea en donde se pide al usuario que introduzca dos enteros más:
cout
<< “\nEscriba dos enteros (fin de archivo para terminar ): “;
Flujo del control del programa cuando el usuario
escribe un denominador de cero.
Ahora vamos a
considerar un caso más interesante, en el que el usuario introduce el numerador
100 y el denominador 0. En las siguientes líneas de código, cociente determina
que el denominador es igual a cero, lo cual indica un intento de división entre
cero. Se lanza una excepción, que representamos como un objeto de la clase ExcepcionDivisionEntreCero.
Para lanzar una excepción, Se utiliza la palabra clave throw seguida de un
operando que representa el tipo de excepción a lanzar. Por lo general, una
instrucción throw especifica un operando.
1.
if( denominador == 0){
2.
throw
ExcepciondivisionEntreCero();
El operando de una
instrucción throw puede ser de cualquier tipo. Si el operando es un objeto,
lo llamamos objeto excepción. Sin embargo, un operando de throw también puede
asumir otros valores, como el valor de una expresión que no produce un objeto
(por ejemplo, throw x > 5) o el valor de un int ( por ejemplo, throw
5).
Como parte de lanzar
una excepción, se crea el operando throw y se utiliza para inicializar
el parámetro en el manejador catch. La instrucción throw crea un objeto de
la clase ExcepcionDivisionEntreCero. Esta es una característica central
del manejo de excepciones: una función debe lanzar una excepción antes de que
el error tenga la oportunidad de manifestarse. En general, cuando se lanza una
excepción dentro de un bloque try, la excepción se atrapa
mediante un manejador catch que especifica el tipo que
coincide con la excepción lanzada. El manejador catch especifica que
atrapa objetos ExcepcionDivisionEntreCero; este tipo coincide con el tipo del
objeto lanzado en la función cociente.
16.4 CUANDO UTILIZAR EL MANEJO DE EXCEPCIONES.
El manejo de
excepciones está diseñado para procesar errores síncronos, que ocurren cuando
se ejecuta una instrucción. Ejemplos comunes de estos errores son los
subíndices de arreglos fuera de rango, el desbordamiento aritmético (es decir,
un valor fuera del rango de valores representables), la división entre cero,
los parámetros de función inválidos y la asignación fallida de memoria (debido
a la falta de memoria). El manejo de excepciones no está diseñado para procesar
los errores asociados con los eventos síncronos (por ejemplo, completar la E/S
de disco, la llegada de mensajes de red, los clics del ratón y las pulsaciones
de tecla), los cuales ocurren en paralelo con, y de manera independiente a, el
flujo de control del programa.
El mecanismo de manejo
de errores también es útil para procesar problemas que ocurren cuando un
programa interactúa con los elementos de software, como las funciones miembro,
los constructores, los destructores y las clases. En vez de manejar los
problemas de manera interna, dichos elementos de software utilizan comúnmente
las excepciones para notificar a los programas cuando ocurren problemas.
Por lo general, las
aplicaciones complejas consisten en componentes predefinidos de software y
componentes específicos de cada aplicación que utilizan los componentes
predefinidos. Cuando un componente predefinido encuentra un problema, ese
componente necesita un mecanismo para comunicar el problema al componente
específico de la aplicación; el componente predefinido no puede saber de antemano
cómo procesa cada aplicación un problema que se presenta.
16.5. VOLVER A LANZAR UNA EXCEPCION.
Es posible que un
manejador de excepciones, al momento de recibir una excepción, decida que no
puede procesar esa excepción o que puede procesar la excepción sólo de manera
parcial. En tales casos, el manejador de excepciones puede diferir el manejo de
excepciones a otro manejador de excepciones. En cualquier caso, para lograr
esto hay que volver a lanzar la excepción mediante la siguiente instrucción:
throw;
Sin importar el que un
manejador pueda o no procesar una excepción, el manejador puede volver a lanzar
la excepción para seguirla procesando fuera de éste.
16.6. ESPECIFICACIONES DE EXCEPCIONES.
Una especificación de
excepciones opcional (también conocida como lista throw) enumera una
lista de excepciones que puede lanzar una función. Por ejemplo, considere la
siguiente declaración de una función:
1. int unaFuncion( double valor )
2.
throw
( ExcepcionA, ExcepcionB, ExcepcionC )
3.
{
4.
//
cuerpo de la función
5.
}
En esta definición, la
especificación de la función, que empieza con la palabra clave throw
justo después del paréntesis de cierre de la lista de parámetros de la función,
indica que la función unaFuncion puede lanzar excepciones de los tipos
ExcepcionA, ExcepcionB y ExcepcionC. Una función sólo puede lanzar excepciones
de los tipos indicados por la especificación, o excepciones de cualquier tipo
derivado de estos tipos. Si la función lanza (throw) una excepción que
no pertenezca a un tipo especificado, el mecanismo de manejo de excepciones
llama a la función unexpected, la cual termina el programa. Una función que no
proporciona una especificación de excepciones puede lanzar cualquier excepción.
Al colocar throw() (una especificación de excepciones vacía) después de la
lista de parámetros de una función, se establece que esa función no lanza
excepciones. Si la función intenta lanzar una excepción, se invoca la función unexpected.
16.7. PROCEDIMIENTO DE EXCEPCIONES INESPERADAS.
La función unexpected
llama a la función registrada con la función set_unexpected (definida
en el archivo de encabezado <exception>). Si no se ha
registrado una función de esta forma, se hace una llamada a la función terminate
de manera predeterminada. Los casos en los que se hace una llamada a la función
terminate
son:
1.
El
mecanismo de excepción no puede encontrar un catch que coincida para
una excepción que se haya lanzado.
2.
Un
destructor intenta lanzar una excepción durante la limpieza de la pila.
3.
Hay
un intento de volver a lanzar una excepción cuando no se esté manejando una
excepción en un momento dado.
4.
Una
llamada a la función unexpected realiza una llamada a la
función terminate de manera predeterminada.
La función set_terminate
puede especificar la función a invocar cuando se haga la llamada a terminate.
En caso contrario, terminate llama a abort, con lo cual se termina el
programa sin llamar a los destructores de cualquier objeto restante de una
clase de almacenamiento estático o automático.
Esto podría conllevar a
fugas de recursos cuando un programa termina en forma prematura. Las funciones set_terminate
y set_unexpected
devuelven un apuntador a la última función llamada por terminate y unexpected,
respectivamente. Las funciones set_terminate y set_unexpected reciben
como argumentos apuntadores a funciones con tipos de valores de retorno void
y sin argumentos. Si la última acción de una función de terminación definida por
el programador no es salir de un programa, se hará una llamada a la función abort
para terminar la ejecución del programa una vez que se ejecuten las demás
instrucciones de la función de terminación definida por el programador.
16.8. LIMPIEZA DE LA PILA.
Cuando se lanza una
excepción pero no se atrapa en un alcance específico, la pila de llamadas a
funciones se “limpia” y se hace un intento de atrapar (catch) la excepción en el
siguiente bloque try...catch exterior.
Limpiar la pila de llamadas a funciones significa
que la función en la que no se atrapó la excepción termina, todas las variables
locales en la función se destruyen y el control regresa a la instrucción que
invocó originalmente a esa función. Si un bloque try encierra esa instrucción,
se hace un intento de atrapar la excepción. Si un bloque try no encierra
esa instrucción, se vuelve a realizar la limpieza de la pila. Si no hay un
manejador catch que atrape a esta excepción, se hace una llamada a la
función terminate para terminar el programa. Recuerde que la función what
es una función virtual de la clase exception que una clase derivada
puede sobrescribir para que devuelva un mensaje de error apropiado.
16.9 CONSTRUCTORES, DESTRUCTORES Y MANEJO DE
EXCEPCIONES.
El constructor de un objeto
responde cuando new falla debido a que no pudo asignar la memoria requerida
para almacenar la representación interna de ese objeto. Como el constructor
no puede devolver un valor para indicar un error, debemos elegir un medio
alternativo de indicar que el objeto no se ha construido en forma apropiada. Un
esquema es establecer una variable fuera del constructor. La
alternativa preferida es requerir que el constructor lance una excepción que
contenga la información del error, con lo cual se ofrece una oportunidad para
que el programa maneje la falla.
Antes de que un constructor
lance una excepción, se hacen llamadas a los destructores para
cualquier objeto miembro que se construya como parte del objeto que se va a
construir. Se hacen llamadas a los destructores para todos los objetos
automáticos construidos en un bloque try antes de lanzar una
excepción. Se garantiza que la limpieza de la pila se habrá completado
en el momento en el que un manejador de excepciones se empiece a ejecutar.
Si un destructor
invocado como resultado de la limpieza de la pila lanza una
excepción, se hace una llamada a terminate. Si un objeto tiene
objetos miembro, y si se lanza una excepción antes de que el objeto exterior se
construya por completo, entonces se ejecutarán los destructores para los
objetos miembro que se hayan construido antes de la ocurrencia de la excepción.
Si se ha construido parcialmente un arreglo de objetos cuando ocurre una
excepción, sólo se llamará a los destructores para los objetos
construidos en el arreglo.
Una excepción podría
impedir la operación de código que por lo general liberaría un recurso, con lo
cual se produciría una fuga de recursos. Una técnica para resolver este
problema es inicializar un objeto local para adquirir el recurso. Cuando ocurra
una excepción, se invocará al destructor para ese objeto y se
podrá liberar el recurso.
16.10.
EXCEPCIONES Y HERENCIA.
Se pueden derivar
varias clases de excepciones de una clase base común, cuando creamos la clase ExcepcionDivisionEntreCero
como una clase derivada de la clase exception. Si un manejador catch
maneja un apuntador o referencia a un objeto excepción del tipo de una clase
base, también puede atrapar un apuntador o referencia a todos los
objetos de las clases que se deriven públicamente de esa clase base; esto permite
el procesamiento polimórfico de los errores relacionados.
16.11.
PROCEDIMIENTO DE LAS FALLAS DE NEW.
El estándar de C++
especifica que, cuando falla el operador new, lanza una excepción bad_alloc
(definida en el archivo de encabezado <new>).
Caso en el que new devuelve 0 al fallar
La figura 16.5 demuestra
cómo new
devuelve 0 al fallar en asignar la cantidad requerida de memoria. La
instrucción for en las líneas 13 a 24 debería iterar 50 veces y, en cada
pasada, asignar un arreglo de 50,000,000 valores double (es decir,
400,000,000 bytes, ya que un double es por lo general de 8
bytes). La instrucción if en la línea 17 evalúa el
resultado de cada operación de new para determinar si new
asignó la memoria con éxito. Si new falla y devuelve 0, en la línea
19 se imprime un mensaje de error y el ciclo termina. Los resultados muestran
que el programa sólo realizó tres iteraciones antes de que new fallara, y el
ciclo terminó. Los resultados del lector podrían diferir con base en la memoria
física, el espacio en disco disponible para la memoria virtual en su sistema y
el compilador que esté utilizando.
Caso en el que new lanza bad_alloc al fallar.
1.
#include
<iostream>
2.
#include
<new>
3.
using
namespace std;
4.
using
std::bac_alloc;
5.
int
main{
6.
doublé
*ptr[ 50 ];
7.
try{
8.
for
( int i = 0; i < 50; i++){
9.
ptr[
i ] = new double[ 50000000 ];
10. cout << “ptr[“ << i
<< “] apunta a 50,000,000 nuevos valores double\n”;
11. }
12. catch( bad_alloc
&ExcepcionAsignacionMemoria ){
13. cerr << “Ocurrio una
Excepcion: ” << ExcepcionAsignacionMemoria.what(); << endl;
14. }
15. }
En el código anterior
demuestra cómo new lanza bad_alloc al fallar en asignar la memoria solicitada.
La instrucción for, dentro del bloque try debe iterar 50 veces y en
cada pasada asigna un arreglo de 50,000,000 valores double. Si new
falla y lanza una excepción bad_alloc, el ciclo termina y el
programa continúa en la línea 12, en donde el manejador catch atrapa y procesa la
excepción. En las líneas 12 y 13 se imprime el mensaje "Ocurrio una
excepción:" seguido del mensaje devuelto de la versión de la función what
correspondiente a la clase base exception.
Los resultados muestran
que el programa sólo realizó tres iteraciones del ciclo antes de que fallara new
y se lanzara la excepción bad_alloc. Los resultados del lector
podrían diferir con base en la memoria física, espacio en disco disponible para
la memoria virtual en su sistema, y el compilador que esté utilizando.
Para este propósito, el
archivo de encabezado <new> define el objeto nothrow
(de tipo nothrow_t), que se utiliza de la siguiente manera:
double *ptr = new( nothrow ) double[ 50000000 ];
La instrucción anterior
utiliza la versión de new que no lanza excepciones bad_alloc
(es decir, nothrow) para asignar un arreglo de 50,000,000 valores double.
Manejo de las fallas de new mediante la función
set_new_handler
Una característica
adicional para manejar las fallas de new es la función set_new_handler (cuyo
prototipo se encuentra en el archivo de encabezado estándar <new>).
Esta función recibe como argumento un apuntador a una función que no recibe
argumentos y devuelve void. Este apuntador apunta a la
función que se llamará si new falla. Esto proporciona al
programador una metodología uniforme para manejar todas las fallas de new,
sin importar que ocurra una falla en el programa. Una vez que set_new_handler
registra un manejador de new en el programa, el operador new
no lanza bad_alloc en el futuro; en vez de ello, difiere el manejo de
errores a la función manejadora de new. Si new asigna memoria con éxito,
devuelve un apuntador a esa memoria. Si new falla en asignar memoria y set_new_handler
no registró una función manejadora de new, lanza una excepción bad_alloc.
Si new
falla al asignar memoria y se ha registrado una función manejadora de new,
se hace una llamada a esta función. El estándar de C++ especifica que la función
manejadora de new debe realizar una de las siguientes tareas:
1.
Hacer
más memoria disponible al eliminar otra parte de la memoria asignada en forma
dinámica (o indicando al usuario que cierre otras aplicaciones) y regresar al
operador new para tratar de asignar memoria otra vez.
2.
Lanzar
una excepción de tipo bad_alloc.
3.
Llamar
a la función abort o exit (ambas se encuentran en el
archivo de encabezado <cstdlib>) para terminar el
programa.
En el siguiente código
se demuestra el uso de set_new_handler. La función nuevoManejadorPersonalizado
imprime un mensaje de error (línea 9) y después termina el programa mediante
una llamada a abort (línea 10). Los resultados muestran que el programa sólo
realizó tres iteraciones del ciclo, debido a que new falló e invocó a la
función nuevoManejadorPersonalizado. Los resultados del lector podrían
diferir con base en la memoria física, el espacio disponible para la memoria
virtual en su sistema y el compilador que utilice para compilar el programa.
1.
#include
<iostream>
2.
#include
<new>
3.
#include
<cstdlib>
4.
using::std
cerr;
5.
using::std
cout;
6. using::std
set_new_handler;
7. using::std
abort;
8. void
nuevoMAnejoPerzonalizado(){
9.
cerr
<< “Se llamo a nuevoManejoPersonalizado”;
10. abort();
11. }
12. Int main(){
13. double *ptr[ 50 ];
14. set_new_handler( nuevoManejoPersonalizado);
15. for( int i = 0; i < 50; i++){
16. ptr[ i ] = new double[ 50000000 ];
17. cout << “ptr[“ << i
<< “] apunta a 50,000,000 nuevos valores\n”;
18. }
19. return 0;
20. }
16.12. LA CLASE auto_ptr Y LA ASIGNACIÓN DINÁMICA DE
MEMORIA.
Una práctica común de
programación es asignar la memoria dinámica, asignar la
dirección de la memoria a un apuntador, usar el apuntador para manipular la
memoria y desasignar la memoria con delete cuando ésta ya no sea
necesaria. Si ocurre una excepción después de una asignación exitosa de
memoria, pero antes de que se ejecute la instrucción delete, podría ocurrir
una fuga de memoria.
El estándar de C++
proporciona la plantilla de clase auto_ptr en el archivo de encabezado
<memory>
para lidiar con esta situación. Un objeto de la clase auto_ptr mantiene un
apuntador a la memoria que se asigna en forma dinámica. Cuando se hace una
llamada al destructor de un objeto auto_ptr, realiza una operación delete
con su miembro de datos apuntador. La plantilla de clase auto_ptr proporciona los
operadores sobrecargados * y ->, de manera que un objeto auto_ptr
se puede utilizar de la misma forma que una variable apuntador común.
1.
#include
<iostream>
2.
#include
<memory>
3.
#include
“Entero.h”
4.
using::std
cout;
5.
using::std
endl;
6.
using::
std auto_ptr;
7.
int
main() {
8.
cout
<< “Creacion de un objeto auto_ptr que apunta a un objeto Entero\n”;
9.
auto_ptr<
Entero> ptrAEntero( new Entero( 7 ) );
10. cout << “\nUso de auto_ptr
para manipular el objeto Entero\n”;
11. ptrAEntero -> setEntero( 99 );
12. cout << “Entero después de
setEntero: ” << ( *ptrAEntero).getEntero()
13. return 0;
14. }
En la línea 9 del
código anterior se crea el objeto ptrAEntero de auto_ptr, y se inicializa
con un apuntador a un objeto Entero asignado en forma dinámica, que contiene el
valor 7. En la línea 11 se utiliza el operador -> sobrecargado de auto_ptr
para invocar a la función setEntero en el objeto Entero que
maneja ptrAEntero. En la línea 12 se utiliza el operador * sobrecargado de auto_ptr
para desreferenciar a ptrAEntero, y después se utiliza el operador punto (.)
para invocar a la función getEntero en el objeto Entero. Al igual que un
apuntador regular, los operadores sobrecargados -> y * de un objeto auto_ptr
se pueden utilizar para acceder al objeto al que apunta el objeto auto_ptr.
Como ptrAEntero es una
variable automática local en main, se destruye cuando main
termina. El destructor de auto_ptr obliga a que se lleve a
cabo una operación delete del objeto Entero al que apunta ptrAEntero, que a su vez
llama al destructor de la clase Entero. La memoria que ocupa Entero se libera,
sin importar cómo sale el control del bloque. Sólo un objeto auto_ptr puede
poseer un objeto asignado en forma dinámica en un momento dado, y el objeto no
puede ser un arreglo.
Al usar su operador de
asignación sobrecargado o su constructor de copia, un objeto auto_ptr
puede transferir la propiedad de la memoria dinámica que maneja. El último
objeto auto_ptr que mantiene el apuntador a la memoria dinámica
eliminará la memoria. Esto hace de auto_ptr un mecanismo ideal para
devolver memoria asignada en forma dinámica al código cliente. Cuando el objeto
auto_ptr
queda fuera de alcance en el código cliente, el destructor de auto_ptr
elimina la memoria dinámica.
16.13 JERARQUÍA DE EXCEPCIONES DE LA BIBLIOTECA ESTÁNDAR.
La experiencia ha
demostrado que las excepciones se pueden clasificar en varias categorías. Esta
jerarquía está encabezada por:la clase base exception, la cual
contiene la función virtual what, que las clases derivadas
pueden sobrescribir para generar los mensajes de error apropiados. Las clases
derivadas inmediatas de la clase base exception incluyen a runtime_error
y logic_error,
cada una de las cuales tiene varias clases derivadas. De exception también se
derivan las excepciones lanzadas por los operadores de C++; por ejemplo, bad_alloc
es lanzada por new, bad_cast es lanzada por dynamic_cast
y bad_typeid
es lanzada por typeid. Incluir a bad_exception en la lista throw de una
función significa que, si ocurre una excepción inesperada, la función unexpected
puede lanzar a bad_exception en vez de terminar la ejecución del programa o
llamar a otra función especificada por set_unexpected.
La clase logic_error
es la clase base de varias clases de excepciones estándar que indican errores
en la lógica del programa. la clase invalid_argument indica que se pasó
un argumento inválido a una función. La clase length_error indica que
para ese objeto se utilizó una longitud mayor que el tamaño máximo permitido
para el objeto que se está manipulando. La clase out_of_range indica que
un valor, como un subíndice en un arreglo, excedió su rango permitido de
valores.
La clase runtime_error,
es la clase base de varias otras clases de excepciones estándar que indican
errores en tiempo de ejecución. La clase overflow_error describe un error de
desbordamiento aritmético y la clase underflow_error describe un error de
subdesbordamiento (es decir, el resultado de una operación aritmética es menor
que el número más pequeño que se puede almacenar en la computadora).
16.14. OTRAS TÉCNICAS PARA MANEJAR ERRORES.
En capítulos anteriores
hemos visto varias formas de tratar con situaciones excepcionales. A
continuación se sintetizan éstas y otras técnicas para manejar errores:
Ignorar
la excepción.
Si ocurre una excepción, el programa podría fallar como resultado de la
excepción que no se atrapó. Esto es devastador para los productos de software
comerciales y el software de misión crítica y propósito especial, pero para el
software desarrollado para nuestros propios fines, es común ignorar muchos tipos
de errores.
Abortar
el programa.
Sin duda, esto evita que un programa se ejecute hasta completarse y que
produzca resultados incorrectos. Para muchos tipos de errores esta técnica es
apropiada, en especial para los errores no fatales que permiten a un programa
ejecutarse hasta completarse. Esta estrategia es inapropiada para las
aplicaciones de misión crítica. Las cuestiones sobre los recursos también son
de importancia aquí; si un programa obtiene un recurso, debe liberarlo antes de
terminar.
Establecer
indicadores de errores.
El problema con esta metodología es que los programas tal vez no comprueben
estos indicadores de errores en todos los puntos en los que los errores podrían
resultar problemáticos. Otro problema es que el programa, después de procesar
el problema, tal vez no borre los indicadores de errores.
Probar
la condición de error, generar un mensaje de error y llamar a exit
en <cstdlib>
para pasar un código de error apropiado al entorno del programa.
Usar
las funciones setjump y longjump.
Estas funciones de la biblioteca <csetjump> permiten al
programador especificar un salto inmediato, desde una llamada a una función con
muchos niveles de anidamiento hasta un manejador de errores. Las funciones setjump
y longjump
son peligrosas, ya que limpian la pila sin llamar a los destructores
para los objetos automáticos. Esto puede provocar problemas graves.
CAPITULO 18 “LA CLASE STRING Y EL PROCESAMIENTO DE FLUJOS DE CADENA”.
18.1. INTRODUCCION
La plantilla de clase basic_string proporciona operaciones comunes de
manipulación de cadenas, las herramientas de soporte se definen de la siguiente
manera:
typedef basic_string<char> string;
Se proporciona una definición de typedef para el tipo wchar_t.
El tipo wchar_t almacena caracteres para soportar otros conjuntos de
caracteres, además se debe incluir el archivo de encabezado <string>.
Sintaxis constructora
string texto(“hola”);
string mes = “marzo”;
La clase string también proporciona un constructor predeterminado (que
crea una cadena vacía) y un constructor de copia. Una cadena vacía es un objeto
string
que no contiene caracteres. El operador = en la declaración no es una
asignación, sino una llamada implícita al constructor de la clase string,
que realiza la conversión. En la clase string no se proporcionan
conversiones de int o char 2 string en una definición string.
Los objetos string no necesariamente tienen terminación nula. La
longitud de un objeto string se puede obtener mediante la
función miembro length y mediante la función size. El operador
subíndice ([]), se utiliza con objetos string para acceder a caracteres
individuales y modificarlos; los objetos string tiene un primer subíndice 0 y
un último subíndice de lenght() -1. El operador de
extracción de flujo (>>) se sobre carga para soportar objetos string.
string objetos1;
cin >> objetos1;
Declaran un objeto string y leen un objeto string.
La función getline también se sobrecarga para objetos string. Asumiendo que
cadena1 es un objeto string.
getline( cin.cadena1 );
Lee un objeto string y lo coloca en la cadena1.
18.2. ASIGNACION Y
CONCATENACION DE OBJETOS STRING.
cadena3.assign( cadena1 );
Se utiliza la función miembro assign para copiar cadena1 en
cadena3, se hace una copia separada (cadena1 y cadena3 son objetos
independientes).
cadenaDestino.assign( cadenaOrigen, inicio,
numeroDeCaractes );
En donde cadenaOrigen es el objeto string que se va a copiar, inicio es
el subíndice inicial y numeroDeCaracteres es el número de caracteres a copiar.
for(
int i = 0; i < cadena3.length(); i++)
cout << cadena3.at( i );
La función miembro at proporciona un acceso comprobado
(o comprobación de rango); es decir, al ir más allá del final del objeto string
se lanza una aceptación out_of_range.
string.cadena4( cadena1 + “apulta”);
La cadena4 se declara y se inicializa con el resultado de la concatenación
de cadena1 y “apulta” mediante el uso
del operador + sobrecargado, que denota la concatenación.
cadena3 += “peta”;
También se utiliza el operador de asignación de suma (+=) para
concatenar cadena3 y “pet”.
cadena1.append( “acumba” );
Se utiliza la función miembro append para concatenar cadena1 y
“acumba”.
cadena5.append( cadena1, 4, cadena1.length() - 4);
En esta línea se adjunta la cadena “cumba” a la cadena vacía cadena5.
Esta función miembro recibe el objeto string (cadena1) de donde va a
obtener los caracteres, el subíndice inicial en el objeto string (4) y el número de
caracteres que se deben adjuntar (el valor devuelto por cadena1.length () - 4).
18.3. COMPARACION
DE OBJETOS STRING
if( cadena1 == cadena4 )
La condición de la línea anterior compara la igualdad entre cadena1 y
cadena4, usando el operador de igualdad sobrecargo. Si la condición es true,
se imprime “cadena1 == cadena4”. Si la condición es false, se evalúa la
condición if (cadena1 > cadena4). Todas las funciones operador
sobrecargo de la clase string, devuelven valores bool.
int resultado = cadena1.compare( cadena2 );
Se utiliza la función miembro compare de string para comparar
cadena1 y cadena2. A la variable resultado se le asigna 0 si los objetos string
son equivalentes. Un léxico es un diccionario. Cuando decimos que un objeto string
es léxicográficamente
menor que otro, indicamos que el método compare utiliza los valores
numéricos de los caracteres en cada objeto string para determinar que el primer
objeto string es menor que el segundo.
resultado = cadena1.compare (1, 4, cadena3, 1, 4 );
Se utiliza la función miembro compare para comparar las porciones
de cadena1 y cadena3. Los primeros argumentos (1, 4) especifican el subíndice
inicial y la longitud de la porción de la cadena1 (“prueba”) que se va a
comparar con cadena3. El 3er argumento es un objeto string de comparación.
Los 2 últimos argumentos (1 y 4) son el subíndice inicial de y la longitud de
la porción del objeto string de comparación que se va a
comparar.
resultado = cadena4.compare (0, cadena2.lenght (),
cadena2);
En la línea anterior se utiliza compare para comparar cadena4 y
cadena2. Los primeros dos argumentos son los mismos: el subíndice inicial y la
longitud. El último argumento es el objeto string de comparación. Como las 2
piezas de objetos string que se están comparando aquí son idénticos, a resultado
se le asigna 0.
resultado = cadena2.compare (0, 3, cadena4);
En esta línea se hace una llamada a la función compare para comparar los
primeros 3 caracteres en cadena2 con cadena4.
18.4. SUBCADENAS
La clase string proporciona la función miembro substr para obtener una
subcadena de un objeto string. El resultado es un nuevo
objeto string que se copia del objeto string de origen.
cout << cadena1.substr( 7, 5) << endl;
La línea anterior utiliza la función miembro substr para obtener una
subcadena de cadena1. El primer argumento especifica el subíndice inicial de la
subcadena deseada, el segundo argumento especifica la longitud de la subcadena.
18.5. INTERCAMBIO
DE OBJETOS STRING
La clase string proporciona la función miembro swap para intercambiar
objetos string.
primero.swap ( segundo );
Se utiliza la función swap para intercambiar los valores
de primero y segundo. Los objetos string se imprimen de nuevo para
confirmar que se hayan cambiado. La función miembro swap de string
es útil para implementar programas que ordenan cadenas.
18.6.
CARACTERISTICAS DE LOS OBJETOS STRING
La clase string proporciona funciones miembro para recopilar información
acerca del tamaño de un objeto string, su longitud máxima y otras
características.
El tamaño (función miembro size) o la longitud
(función miembro length) de un objeto string es el número de caracteres
actualmente almacenados en el objeto string.
La capacidad (usando la función miembro capacity)
de un objeto string es el número de caracteres que se pueden almacenar en el
objeto string sin necesidad de asignar más memoria.
El tamaño máximo (usando la función miembro max_size)
es el mayor tamaño posible que puede tener un objeto string. Si se excede este
valor, se lanza una excepción length_error.
Si el objeto está o no vacío se utiliza la función
miembro empty.
La función miembro resize se utiliza para
incrementar la longitud de cadena1 por 10 caracteres. Los elementos adicionales
se establecen con caracteres nulos.
18.7. BUSQUEDA DE
SUBCADENAS Y CARACTERES EN UN OBJETO STRING.
La clase string proporciona funciones miembro const para buscar
subcadenas y caracteres en un objeto string.
<< ” \n\n(find )\”es\” se encontro en: “
<< cadena1.find( “es” )
Esta línea trata de buscar “es” en cadena1, usando la función find.
<<”\n(rfind) \ “es\” se encontro en: ” << cadena1.rfind ( “es” );
En esta línea se utiliza la función miembro rfind para realizar una
búsqueda inversa en cadena1(es decir de derecha a izquierda).
ubicación = cadena1.find_first_of( “liesop” );
Con la función find_first_of
podemos localizar la primera ocurrencia
en cadena1 de cualquier carácter de “liesop”. A búsqueda se realiza
desde el inicio de la cadena1.
ubicación = cadena1.find_last_of( “liesop” );
Utilizamos la función
miembro find_last_of para buscar la última ocurrencia en cadena1 de
cualquier carácter de “liesop”. La búsqueda se realiza desde el final de
caden1.
ubicacion = cadena1.find_first_not_of( “medop ias” );
Utilizamos la función find_first_not_of
para buscar el primer carácter en cadena1 que no esté contenida en “medop ias”.
La búsqueda se realiza desde el inicio de la cadena1.
18.8. REEMPLAZO DE CARACTERES EN UN OBJETO STRING.
cadena1.erase(
72 );
Esta línea de código
utiliza la función miembro erase de string para borrar todo,
desde el carácter en la posición 62 hasta el final de la cadena1.
Int
posicion = cadena1.find( “ ” );
while(
posicion != string::npos ){
cadena1.replace(
posición, 1, ”.”);
posicion
= cadena1( “ ”, posicion + 1); }
En las líneas de código
anterior se utiliza la función miembro find para localizar cada ocurrencia
el carácter de espacio. Después, cada espacio se reemplaza con un mediante una
llamada a la función miembro replace de string. La función replace
recibe tres argumentos: el subíndice del carácter en el objeto string
en el que se desea ser reemplazado, el número de caracteres a reemplazar y la
cadena de reemplazo.
18.9. INSERCIÓN DE CARACTERES EN UN OBJETO STRING.
La clase string
proporciona funciones miembro para insertar caracteres en un objeto string.
cadena1.insert(
10, cadena2 );
En esta línea de código
se utiliza la función miembro insert de string para insertar el
contenido de cadena2 antes del elemento 10 de cadena1.
cadena3.insert(
3, cadena4, 0, string::npos );
En la línea anterior se
utiliza insert para insertar cadena4 antes del elemento 3 de la
cadena3. Los últimos 2 argumentos especifican los elementos inicial y final de
cadena4 que se deben insertar. Al utilizar string::npos, se inserta todo el
objeto string.
18.10. CONVERSIÓN
A CADENAS ESTILO C.
La clase string
proporciona funciones miembro para convertir objetos de la clase string en
cadenas basadas en apuntador estilo C.
cadena1.copy(
ptr2, longitud, 0);
Se utiliza a función
miembro copy de string para copiar el objeto cadena1
en el arreglo char al que apunta ptr2.
<<
cadena1.c_str() << “\nptr1 es ”;
La función c_str
sirve para obtener un apuntador const char* que apunte a una cadena
estilo C con terminación nula.
18.11. ITERADORES
La clase string
proporciona iteradores para realizar recorridos hacia adelante y hacia atrás
de objetos string. Los iteradores proporcionan acceso a los caracteres
individuales con una sintaxis similar a las operaciones de los apuntadores.
string::const_iterador
iterador1 = cadena1.begin();
Un const_iterador es un iterador
que no puede modificar el objeto string. Existen dos versiones de begin:
una devuelve un iterador para iterar a través de un objeto string no const y una
versión const que devuelve un const_iterador para iterar con un
objeto const_string.
1.
while
( iterador1 != cadena1.end() ){
2.
cout
<< *iterador1;
3.
iterador1++;
4.
}
En las líneas
anteriores se utiliza iterador iterador1 para “recorrer”
cadena1. La función miembro end de la clase string devuelve un
objeto iterador o const_iterador para la posición que está
más allá del último elemento de cadena1.
El iterador
se avanza una posición mediante el operador ++.
La clase string
proporciona las funciones miembro rend y rbegin para acceder a los
caracteres individuales de un objeto string en forma inversa, desde el
final hasta su inicio. Las funciones rend y rbegin devuelven objetos
reverse_iterador
o const_reverse_iterador.
18.12. PROCESAMIENTO DE FLUJOS DE CADENA.
Además de la E/S
de flujos estándar y la E/S
de flujos de archivo, en C++ la E/S de flujos incluye herramientas para
recibir y enviar datos de objetos string en la memoria. A estas
herramientas se le conoce como E/S en memoria o procesamiento de flujos de
cadena.
La entrada desde un
objeto string esta soportada por la clase istringstream. La salida
hacia un objeto string esta soportada por la clase ostringstream. Los
nombres son definidos por typedef:
Typedef basic_istringstream< char >
istringstream;
Typedef basic_ostringstream< char >
ostringstream;
Estas plantillas
proporcionan la misma funcionalidad que
las clases istream y ostream. Los programas que utilizan
el formato en memoria deben incluir los archivos de encabezado <sstream>
y <iostream>. Un programa puede leer toda una línea a la
vez del flujo de entrada y colocarla en un objeto string. Una rutina de
validación puede escudriñar el contenido del objeto string y corregir los
datos, si es necesario. El programa puede continuar recibiendo datos de
entrada, sabiendo que los datos de entrada se encuentran en el formato
apropiado.
Un objeto ostringstream
utiliza un objeto string para almacenar los datos de salida. La función miembro str
de la clase ostringstream devuelve una copia de ese objeto string.
ostringstream
cadenaSalida;
Con la línea de código anterior se crea el objeto ostringstream
y utiliza el operador de inserción de flujo
para enviar una serie de objetos string y valores numéricos al
objeto.
cadenaSalida
<< cadena1 << cadena2 << cadena3 << doble1 <<
cadena4 << entero << cadena5 << &entero;
Con esta línea se
envían los objetos string y la dirección de int entero; todos al objeto
cadenaSalida en memoria.
cout
<< “cadenaSalida contiene:\n” << cadenaSalida.str();
En esta línea se
utiliza el operador de inserción de flujo y la llamada cadenaSalida.str() para
mostrar una copia del objeto string creado.
cadenaSalida
<< “\nse agregaron mas caracteres”;
Con esta línea de
código se demuestra que se pueden adjuntar
más datos al objeto string en memoria, con solo enviar
otra operación de inserción de flujo a cadenaSalida.
cout
<< “\n\ndespues de las inserciones de flujo adicionales,\n” <<
“cadenaSalida contiene: \n” << cadenaSalida.str() << endl;
En esta línea se
muestra el objeto string cadenaSalida después de adjuntar caracteres adicionales.
Un objeto istringstream introduce datos desde un objeto string
en memoria a las variables del programa. Los datos son almacenados en un objeto
istringstream
como caracteres. Las operaciones de entrada desde el objeto istringstream
funcionan de manera idéntica a las operaciones de entrada desde
cualquier archivo. El objeto istringstream interpreta el final
del objeto string como el fin del archivo.
String
entrada( “Entrada prueba 123 4.7 A” );
Istringstream
cadena Entrada( entrada );
Con la primera línea de
código se crea el objeto string que contiene los datos, y la
segunda que se construye para contener los datos en el objeto string
entrada. Este objeto contiene los datos que cuando se leen como entrada para el
programa, consisten en dos cadenas (“Entrada” y “prueba”), un int
(123), un double (4.7) y un char (‘A’).
if(
cadenaEntrada.good() ){
La condición if
en la línea anterior utiliza la función good para probar si quedan datos. Si
no quedan datos, la función devuelve false y se ejecuta la siguiente
parte de la instrucción if la cual sería else.
No hay comentarios.:
Publicar un comentario