Miembros estáticos: datos y funciones
Igual que con las clases normales, es posible declarar datos miembro o funciones estáticas dentro de una plantilla. En este caso existirá una copia de cada uno de ellos para cada tipo de instancia que se cree.
Por ejemplo, si añadimos un miembro static en nuestra declaración de "Tabla", se creará una única instancia de miembro estático para todas las instancias de Tabla<int>, otro para las instancias de Tabla<Cadena>, etc.
Hay un punto importante a tener en cuenta. Tanto si trabajamos con clases normales como con plantillas, debemos reservar un espacio físico para las variables estáticas, de otro modo el compilador no les asignará memoria, y se producirá un error si intentamos acceder a esos miembros.
Nota: Tanto una plantilla como una clase pueden estar definidas en un fichero de cabecera (.h), aunque es más frecuente con las plantillas, ya que toda su definición debe estar en el fichero de cabecera; bien dentro de la declaración, en forma de funciones inline o bien en el mismo fichero de cabecera, a continuación de la declaración de la plantilla. Estos ficheros de cabecera pueden estar incluidos en varios módulos diferentes de la misma aplicación, de modo que el espacio físico de los miembros estáticos debe crearse sólo una vez en la memoria global de la aplicación, y por lo tanto, no debe hacerse dentro de los ficheros de cabecera. |
Veamos un ejemplo:
// Fichero de cabecera: prueba.h #ifndef T_PRUEBA #define T_PRUEBA template <class T>; class Ejemplo { public: Ejemplo(T obj) {objeto = obj; estatico++;} ~Ejemplo() {estatico--;} static int LeeEstatico() {return estatico;} private: static int estatico; // (1) T objeto; // Justificamos el uso de la plantilla :-) }; |
Ahora probemos los miembros estáticos de nuestra clase:
// Fichero de prueba: prueba.cpp #include <iostream> #include "prueba.h" using namespace std; // Esto es necesario para que exista // una instancia de la variable: template <class T> int Ejemplo<T>::estatico; // (2) int main() { Ejemplo<int> EjemploInt1(10); cout << "Ejemplo<int>: " << EjemploInt1.LeeEstatico() << endl; // (3) Ejemplo<char> EjemploChar1('g'); cout << "Ejemplo<char>: " << EjemploChar1.LeeEstatico() << endl; // (4) Ejemplo<int> EjemploInt2(20); cout << "Ejemplo<int>: " << EjemploInt1.LeeEstatico() << endl; // (5) Ejemplo<float> EjemploFloat1(32.12); cout << "Ejemplo<float>: " << Ejemplo<float>::LeeEstatico() << endl; // (6) Ejemplo<int> EjemploInt3(30); cout << "Ejemplo<int>: " << EjemploInt1.LeeEstatico() << endl; // (7) return 0; } |
La salida de este programa debería ser algo así:
Ejemplo<int>: 1 Ejemplo<char>: 1 Ejemplo<int>: 2 Ejemplo<float>: 1 Ejemplo<int>: 3 |
Vamos a ver si explicamos algunos detalles de este programa.
Para empezar, en (1) vemos la declaración de la variable estática de la plantilla y en (2) la definición. En realidad se trata de una plantilla de declaración, en cierto modo, ya que adjudica memoria para cada miembro estático de cada tipo de instancia de la plantilla. Prueba a ver qué pasa si no incluyes esta línea.
El resto de los puntos muestra que realmente se crea un miembro estático para cada tipo de instancia. En (2) se crea una instancia de tipo Ejemplo<int>, en (3) una de tipo Ejemplo<char>, en (4) una segunda de tipo Ejemplo<int>, etc. Eso de ve también en los valores de salida.
Ejemplo de implementación de una plantilla para una pila
Considero que el tema es lo bastante interesante como para incluir algún ejemplo ilustrativo. Vamos a crear una plantilla para declarar pilas de cualquier tipo de objeto, y de ese modo demostraremos parte de la potencia de las plantillas.
Para empezar, vamos a ver la declaración de la plantilla, como siempre, incluida en un fichero de cabecera para ella sola:
// Pila.h: definición de plantilla para pilas // Mayo 2015 Tutoriales En Linea #ifndef TPILA #define TPILA // Plantilla para pilas genéricas: template <class T> class Pila { // Plantilla para nodos de pila, definición // local, sólo accesible para Pila: template <class Tn> class Nodo { public: Nodo(const Tn& t, Nodo<Tn> *ant) : anterior(ant) { pT = new Tn(t); } Nodo(Nodo<Tn> &n) { // Constructor copia // Invocamos al constructor copia de la clase de Tn pT = new Tn(*n.pT); anterior = n.anterior; } ~Nodo() { delete pT; } Tn *pT; Nodo<Tn> *anterior; }; ///// Fin de declaración de plantilla de nodo ///// // Declaraciones de Pila: public: Pila() : inicio(NULL) {} // Constructor ~Pila() { while(inicio) Pop(); } void Push(const T &t) { Nodo<T> *aux = new Nodo<T>(t, inicio); inicio = aux; } T Pop() { T temp(*inicio->pT); Nodo<T> *aux = inicio; inicio = aux->anterior; delete aux; return temp; } bool Vacia() { return inicio == NULL; } private: Nodo<T> *inicio; }; |
Aquí hemos añadido una pequeña complicación: hemos definido una plantilla para Nodo dentro de la plantilla de Pila. De este modo definiremos instancias de Nodo locales para cada instancia de Pila. Aunque no sea necesario, ya que podríamos haber creado dos plantillas independientes, hacerlo de este modo nos permite declarar ambas clases sin necesidad de establecer relaciones de amistad entre ellas. De todos modos, el nodo que hemos creado para la estructura Pila no tiene uso para otras clases.
En cuanto a la definición de la Pila, sólo hemos declarado cinco funciones: el constructor, el destructor, las funciones Push, para almacenar objetos en la pila y Pop para recuperarlos y Vacia para comprobar si la pila está vacía.
En cuanto al constructor, sencillamente construye una pila vacía.
El destructor recupera todos los objetos almacenados en la pila. Recuerda que en las pilas, leer un valor implica borrarlo o retirarlo de ella.
La función Push coloca un objeto en la pila. Para ello crea un nuevo nodo que contiene una copia de ese objeto y hace que su puntero "anterior" apunte al nodo que hasta ahora era el último. Después actualiza el inicio de la Pila para que apunte a ese nuevo nodo.
La función Pop recupera un objeto de la pila. Para ello creamos una copia temporal del objeto almacenado en el último nodo de la pila, esto es necesario porque lo siguiente que hacemos es actualizar el nodo de inicio (que será el anterior al último) y después borrar el último nodo. Finalmente devolvemos el objeto temporal.
Ahora vamos a probar nuestra pila, para ello haremos un pequeño programa:
// Pru_Pila.cpp: Prueba de plantilla pila // Mayo 2015 Tutoriales En Linea #include <iostream> #include "pila.h" #include "CCadena.h" using namespace std; // Ejemplo de plantilla de función: template <class T> void Intercambia(T &x, T &y) { Pila<T> pila; pila.Push(x); pila.Push(y); x = pila.Pop(); y = pila.Pop(); } int main() { Pila<int> PilaInt; Pila<Cadena> PilaCad; int x=13, y=21; Cadena cadx("Cadena X"); Cadena cady("Cadena Y ----"); cout << "x=" << x << endl; cout << "y=" << y << endl; Intercambia(x, y); cout << "x=" << x << endl; cout << "y=" << y << endl; cout << "cadx=" << cadx << endl; cout << "cady=" << cady << endl; Intercambia(cadx, cady); cout << "cadx=" << cadx << endl; cout << "cady=" << cady << endl; PilaInt.Push(32); PilaInt.Push(4); PilaInt.Push(23); PilaInt.Push(12); PilaInt.Push(64); PilaInt.Push(31); PilaCad.Push("uno"); PilaCad.Push("dos"); PilaCad.Push("tres"); PilaCad.Push("cuatro"); cout << PilaInt.Pop() << endl; cout << PilaInt.Pop() << endl; cout << PilaInt.Pop() << endl; cout << PilaInt.Pop() << endl; cout << PilaInt.Pop() << endl; cout << PilaInt.Pop() << endl; cout << PilaCad.Pop() << endl; cout << PilaCad.Pop() << endl; cout << PilaCad.Pop() << endl; cout << PilaCad.Pop() << endl; return 0; } |
Ejecutar este código en codepad.
La salida demuestra que todo funciona:
x=13 y=21 x=21 y=13 cadx=Cadena X cady=Cadena Y ---- cadx=Cadena Y ---- cady=Cadena X 31 64 12 23 4 32 cuatro tres dos uno |
Hemos aprovechado para crear una plantilla de función para intercambiar valores de objetos, que a su vez se basa en la plantilla de pila, y aunque esto no sería necesario, reconocerás que queda bonito :-).
Bibliotecas de plantillas
El ANSI de C++ define ciertas bibliotecas de plantillas conocidas como STL (Standard Template Library), que contiene muchas definiciones de plantillas para crear estructuras como listas, colas, pilas, árboles, tablas HASH, mapas, etc.
Está fuera del objetivo de este curso explicar estas bibliotecas, pero es conveniente saber que existen y que por su puesto, se suelen incluir con los compiladores de C++.
Aunque se trata de bibliotecas estándar, lo cierto es que existen varias implementaciones, que, al menos en teoría, deberían coincidir en cuanto a sintaxis y comportamiento.
Palabras reservadas usadas en este capítulo
template, typename.
Palabra typename
template <typename T> class Plantilla; |
Es equivalente usar typename y class como parte de la declaración de T, en ambos casos T puede ser una clase o un tipo fundamental, como int, char o float. Sin embargo, usar typename puede ser mucho más claro como nombre genérico que class.
Capitulo 41 | Ir al Principio
Comentarios