Algoritmos y Estructuras de Datos Ingeniería en Informática, Curso 2º SEMINARIO DE C++ Sesión 3 Contenidos: 1. Funciones y clases genéricas 2. Excepciones 3. Asertos 4. El puntero this 5. Redefinición de operadores Ejemplo 1. Funciones y clases genéricas * Genericidad (parametrización de tipo): el significado de una función o de un tipos de datos está definido en base a unos parámetros de tipo que pueden variar. o Ejemplo: funciones genéricas. En lugar de: OrdenaInt(int array[]), OrdenaCharP(char *array[]), OrdenaFloat(float array[]), ..., tenemos Ordena(T array[]). o Ejemplo: clases genéricas. El lugar de las clases PilaInt, PilaChar, PilaFloat, ..., tenemos una clase genérica Pila. * En C++, las funciones y clases genéricas se definen mediante sentencias template (plantilla). La sintaxis es: template < parámetros genéricos > función o clase genérica - parámetros genéricos: Lista con uno o varios tipos genéricos (clases). Por ejemplo: template ..., template ... - función o clase genérica: Declaración normal de una función o una clase, pudiendo usar los tipos de la lista de parámetros genéricos. * Funciones genéricas. o Definición. template < parámetros genéricos > tipoDevuelto nombreFunción ( parámetros de la función ) { cuerpo de la función } o Utilización. Llamar directamente a la función. El compilador, según los tipos de los parámetros de entrada, instanciará la versión adecuada. * Ejemplo. Función genérica ordena(T array[], int tam), que ordena un array de elementos de cualquier tipo T. #include // Contiene la clase string, alternativa // a los (char *) de C #include using namespace std; template void ordena (T array[], int tam) { for (int i= 0; i void escribe (T array[], int tam) { for (int i= 0; i class nombreClase { definición de miembros de la clase }; o Implementación de los métodos. Para cada método de la clase tenemos: template < parámetros genéricos > tipoDevuelto nombreclase::nombreFunción ( parámetros ) { cuerpo de la función } o Utilización. La clase genérica se debe instanciar a un tipo concreto antes de usarla. Instanciación: nombreclase. * Ejemplo. Clase genérica pila, de las pilas de cualquier tipo T. #include #define MAXIMO 100 using namespace std; template // Declaración de la clase class pila { private: int ultimo; T datos[MAXIMO]; public: pila () {ultimo= 0;} // Constructor void push (T v); void pop (); T tope(); }; // Implementación de los métodos template void pila::push (T v) { if (ultimo void pila::pop () { if (ultimo>0) ultimo--; } template T pila::tope () { return datos[ultimo-1]; } // Programa principal // Ejemplo de uso de la clase genérica main () { pila p1; // p1 es una pila de enteros p1.push(4); p1.push(9); p1.pop(); cout << p1.tope() << '\n'; pila p2; // p2 es una pila de cadenas p2.push("Ultimo"); p2.push("en"); p2.push("entrar"); p2.push("primero"); p2.push("en"); p2.push("salir"); for (int i= 0; i<6; i++, p2.pop()) cout << p2.tope() << '\n'; pila *p3; // p3 es una pila de float que p3= new pila; // se crea dinámicamente p3->push(1.3); p3->push(2.19); p3->push(3.14); cout << p3->tope() << '\n'; delete p3; } * Probar: 1. Probar pilas de otros tipos: pila, pila >, etc. 2. Implementar una operación igual, que compare dos pilas y devuelva true si son iguales. 3. Probar la función igual. * Existe una "pequeña pega" relacionada con las clases genéricas y la compilación separada: el compilador sólo genera código cuando se instancia la clase genérica. Problema: si separamos la definición de una clase genérica en fichero de cabecera (pila.hpp) y de implementación (pila.cpp), la compilación (c++ -c pila.cpp) no genera código objeto. Si otro módulo usa la case genérica (#include "pila.hpp"), el compilador dirá que no encuentra la implementación de la clase. Solución: para las clases genéricas, tanto la definición como la implementación de la clase deben ir en el mismo fichero, el hpp. 2. Excepciones * ¿Qué hacer si se produce un error irreversible dentro de un procedimiento? Por ejemplo, hacer una división por cero, aplicar tope() sobre una pila vacía, imposibilidad de reservar más memoria o de leer un fichero, etc. 1. Mostrar un mensaje de error por pantalla, o por la salida de error estándar: cerr << "Error: la pila está vacía"; 2. Devolver al procedimiento que hizo la llamada un valor especial que signifique "se ha producido un error". 3. Interrumpir la ejecución del programa: exit(1) 4. No hacer nada y continuar con la ejecución del programa como mejor se pueda. 5. Provocar o lanzar una excepción. * Significado de las excepciones: 1. El procedimiento que lanza la excepción (lo llamamos A), interrumpe su ejecución en ese punto. 2. El procedimiento que ha llamado al A (lo llamamos B), debe tener un código para manejar excepciones. 3. Si el procedimiento B no maneja la excepción, la excepción pasa al que ha llamado al B, y así sucesivamente hasta que alguien trate la excepción (propagación de la excepción). 4. Si la excepción se propaga hasta el main() y este no maneja la excepción, se interrumpe la ejecución del programa y se muestra un mensaje de error. * Conceptualmente, una excepción es un objeto. class ErrorPilaVacia {}; class DivisionPorCero {}; * Provocar una excepción. Sentencia: throw nombreClase(); double divide (int a, int b) { if (b==0) throw DivisionPorCero(); return double(a)/double(b); } * Manejar una excepción. Bloque: try { sentencias1 } catch (nombreClase) { sentencias2 } 1. Ejecutar las sentencias de sentencias1 normalmente. 2. Si se produce una excepción del tipo nombreClase, ejecutar sentencias2. Estas sentencias contienen el código de manejo de excepciones. try { double d1, d2, d3; d1= divide(3, 4); d2= divide(5, 0); d3= divide(1, 2); } catch (DivisionPorCero) { cerr << "División por cero\n"; }; * Manejar excepciones de cualquier tipo: try { sentencias1 } catch (...) { sentencias2 } * Ejemplo. #include using namespace std; class DivisionPorCero {}; double divide (int a, int b) { if (b==0) throw DivisionPorCero(); return double(a)/double(b); } main() { try { cout << divide(3, 4) << '\n'; cout << divide(5, 0) << '\n'; cout << divide(1, 2) << '\n'; } catch (DivisionPorCero) { cerr << "División por cero\n"; } cout << "Acaba\n"; } * Probar: 1. Ejecutar la división por cero fuera del bloque try ... catch ... 2. Cambiar catch (DivisionPorCero) por catch (...) 3. Ejecutar una sentencia throw dentro del main() 3. Asertos * Asertos: verificar una condición. Si se cumple, seguir normalmente. Si no, se muestra la línea de código donde falló el programa y se interrumpe la ejecución. assert ( condición booleana ); * La función assert() está definida en la librería * Los asertos se pueden utilizar para incluir precondiciones y postcondiciones. funcion (parametros) { assert (precondición); ... // Cuerpo de la función ... assert (postcondición); } * Ejemplo. En el ejemplo de la página anterior sustituir la implementación de divide() por la siguiente. No olvidar el #include double divide (int a, int b) { assert(b!=0); return double(a)/double(b); } * Si se define la macro NDEBUG, se desactiva la comprobación de todos los asertos definidos. Probar en el ejemplo anterior. * Ojo: Los asertos provocan la interrupción del programa. * Otra posibilidad: comprobación de precondición y postcondición generando excepciones. Definirse uno mismo las funciones... class FalloPrecondicion {}; class FalloPostcondicion {}; void precondicion (bool condicion) { #ifndef NDEBUG if (!condicion) throw FalloPrecondicion(); #endif } void postcondicion (bool condicion) { #ifndef NDEBUG if (!condicion) throw FalloPostcondicion(); #endif } 4. El puntero this * Dentro de cualquier método de una clase T existe una variable especial: this * El tipo de this es un puntero a T. El valor es un puntero al objeto receptor del mensaje. pila p1, p2; p1.push(5); // Aquí dentro this apunta a p1 p2.pop(); // Y aquí this apunta a p2 * Ejemplo. #include using namespace std; class Simple { char c; public: Simple *asigna(char valor); Simple *escribe(); }; Simple *Simple::asigna(char valor) { this->c= valor; // Equivalente a: c= valor; return this; } Simple *Simple::escribe() { cout << this->c; // Equivalente a: cout << c; return this; } main (void) { Simple *s= new Simple; s->asigna('a')->escribe()->asigna('b')->escribe(); s->asigna('c')->escribe()->asigna('\n')->escribe(); delete s; } * Ojo: en las sentencias this->c= valor; cout << this->c; el uso de this es completamente innecesario. this->XXX se puede sustituir siempre por XXX. Usar this en esos casos sólo hace el código menos legible, por lo que debe evitarse su uso. 5. Redefinición de operadores * Los operadores son las funciones básicas del lenguaje: +, -, *, =, ==, <<, >>, &&, ||, etc. * En C++ es posible redefinir los operadores dentro de una clase. * Redefinición de operadores. Definir e implementar funciones con el nombre: operator+, operator-, operator*, operator=, operator==, etc. * Igual que una función normal, pero con esos nombres especiales. * Ejemplo. Dentro de la definición de la clase pila incluir un operador de comparación, ==. Redefinir los operadores << y >> para que signifiquen apilar y tope, respectivamente. template class pila { ... bool operator==(pila &otra); void operator<<(T valor); void operator>>(T &valor); ... } ... // Implementación template bool pila::operator==(pila &otra) { if (ultimo != otra.ultimo) return false; for (int i= 0; i void pila::operator<<(T valor) { if (ultimo p1, p2; p1 << 4; p2 << 6; .... if (p1==p2) { // Ojo: si no redefinimos el operador ==, esta ... // comparación de aquí no funcionaría } Ejemplo * Vamos a definir un tipo genérico pila, con uso de memoria dinámica, asertos, el puntero this y redefinición de operadores. #include #include #include using namespace std; //Declaración de la clase generica pila template class pila { private: int ultimo; int maximo; T *datos; public: pila (int max= 10); //Constructor, max es el tamaño ~pila (); //Destructor bool operator==(pila &otra); //Comparación de igualdad void operator=(pila &otra); //Asignación entre pilas void push (T v); pila &operator<<(T valor); //Como push, devolviendo this void pop (); T tope(); pila &operator>>(T &valor); //Como tope+pop, devolv. this void escribe (char *nombre= NULL, ostream &co= cout); }; //Operador de salida, para poder hacer: cout << pila; template ostream& operator<<(ostream& co, pila &p) { p.escribe(NULL, co); return co; } //Implementacion de los metodos de la clase pila template pila::pila (int max) { assert(max>0); //Precondicion datos= new T[max]; maximo= max; ultimo= 0; assert(datos!=NULL); //Postcondicion } template pila::~pila () { assert(datos!=NULL); //Precondicion delete[] datos; } template bool pila::operator==(pila &otra) { if (ultimo != otra.ultimo) return false; for (int i= 0; i void pila::operator=(pila &otra) { delete[] datos; ultimo= otra.ultimo; maximo= otra.maximo; datos= new T[maximo]; for (int i= 0; i void pila::push (T v) { if (ultimo pila &pila::operator<<(T valor) { push(valor); return *this; } template void pila::pop () { assert(ultimo>0); //Precondicion ultimo--; } template T pila::tope () { assert(ultimo>0); //Precondicion return datos[ultimo-1]; } template pila &pila::operator>>(T &valor) { valor= tope(); pop(); return *this; } template void pila::escribe (char *nombre, ostream &co) { if (nombre) co << nombre << "= "; co << "["; for (int i= 0; i *p1= new pila(5); *p1 << 4 << 9 << 6 << 14 << 65 << 11; p1->escribe("p1"); pila *p2= new pila(10); *p2= *p1; *p2 << 99 << 76; cout << "p2= " << *p2; cout << "¿Iguales p1 y p2? " << (*p1 == *p2) << "\n"; int tmp1, tmp2; *p2 >> tmp1 >> tmp2; cout << *p2; cout << "¿Iguales p1 y p2? " << (*p1 == *p2) << "\n"; delete p1; delete p2; pila *p3= new pila; for (int i= 0; i > *p4= new pila< pila >; p4->push(*(new pila)<<2<<4); p4->push(*(new pila(10))<<8<<9<<10); p4->push(*(new pila(1))<<1<<2<<3<<4); cout << *p4; delete p4; pila *p5; p5= new pila; char aux1, aux2; try { *p5 << 6; *p5 >> aux1 >> aux2; *p5 << 7; delete p5; } catch (...) { cerr << "Error, pila vacia.\n"; delete p5; } p1->escribe(); } Algoritmos y Estructuras de Datos 12/12 Seminario de C++ – Sesión 3