|
|
|
Programando en C++ Encapsulado II |
En esta lección: |
El encapsulado protege a los datos de daños accidentales y los constructores garantizan una adecuada inicialización, ambos previenen errores que todos estamos expuestos a cometer ya que al escribir una clase pensamos sólo en los aspectos internos de la misma, posteriormente, al utilizarla no necesitamos preocuparnos por los detalles de la clase, concentrandonos en resolver el problema que enfrentamos al momento. Como podrá adivinar, existen muchos más aspectos que debemos tratar respecto al uso y beneficios de las clases. El propósito de éste capítulo es ilustrar algunos de los aspectos de C ó C++ con las clases y objetos. En orden de un estudio sistemático, utilizaremos como punto de partida el último programa de ejemplo del capítulo anterior.
En el primer código de ejemplo de este capítulo analizaremos el concepto de un array de objetos, observe que hemos declarado un array de 4 cajas (grupo[4]) en la línea 39:
#include <iostream.h>
class caja
{
int longitud;
int ancho;
static int dato_extra; // Declaracion de dato_extra
public:
caja(void); // Constructor
void coloca(int nuevo_longitud, int nuevo_ancho);
int area(void);
int extra(void) {return dato_extra++;}
};
int caja::dato_extra; // Definicion de dato_extra
caja::caja(void) // Implementacion del constructor
{
longitud = 8;
ancho = 8;
dato_extra = 1;
}
// Este metodo coloca un tamaño de caja a los dos parametros de entrada
void caja::coloca(int nuevo_longitud, int nuevo_ancho)
{
longitud = nuevo_longitud;
ancho = nuevo_ancho;
}
// Este metodo calcula y retorna el area de una instancia de caja
int caja::area(void)
{
return (longitud * ancho);
}
int main()
{
caja chica, mediana, grande, grupo[4]; // Siete cajas para trabajar
chica.coloca(5, 7);
grande.coloca(15, 20);
for (int indice = 1 ; indice < 4 ; indice++) //grupo[0] usa el default
grupo[indice].coloca(indice + 10, 10);
cout << "El area de caja chica es " << chica.area() << "\n";
cout << "El area de caja mediana es " << mediana.area() << "\n";
cout << "El area de caja grande es " << grande.area() << "\n";
for (indice = 0 ; indice < 4 ; indice++)
cout << "El array area caja es " << grupo[indice].area() << "\n";
cout << "El valor de datos extra es " << chica.extra() << "\n";
cout << "El valor de datos extra es " << mediana.extra() << "\n";
cout << "El valor de datos extra es " << grande.extra() << "\n";
cout << "El valor de datos extra es " << grupo[0].extra() << "\n";
cout << "El valor de datos extra es " << grupo[3].extra() << "\n";
return 0;
}
Respecto al funcionamiento del constructor recordará que cada uno de los cuatro objetos caja serán inicializados a los valores definidos dentro del constructor ya que el constructor será ejecutado para cada objeto caja definido. Para definir un array de objetos debemos disponer de un constructor sin parámetros para dicho objeto. (Aún no hemos ilustrado un constructor con parámetros de inicialización pero lo haremos en el próximo ejemplo). Esto es una consideración de eficiencia ya que probablemente sería un error inicializar todos los elementos de un array de objetos al mismo valor.
En la línea 43 tenemos un bucle for que empieza con 1, dejando que el primer objeto llamado grupo[0], utilice el valor almacenado por default cuando se llamó al constructor. Observe que al enviar un mensaje a uno de los objetos se utiliza el mismo constructor como con cualquier objeto, el nombre del array seguido de su índice entre corchetes se utiliza para enviar un mensaje a uno de los objetos del array, como se demuestra en la línea 44. Otro método es llamado en los enunciados de salida en la línea 51 donde el area de las cuatro cajas en el array grupo son enlistadas en el monitor.
A manera de ilustración se ha incluido una variable adicional llamada dato_extra en la línea 7, como se ha utilizado la palabra clave static para modificar ésta variable, es por lo tanto una variable externa y solo existirá una copia de la misma. Los siete objetos de esta clase compartirán una sola copia de la variable la cual es global para los objetos definidos en la línea 39. La variable ha sido declarada lo que nos dice que existe en algún lugar, pero aún no ha sido definida. Una declaración nos indica la existencia de la variable y le asigna un nombre, pero la definición, valga la redundancia, define un lugar para almacenarla dentro del espacio de memoria de la computadora. Por definición, una variable estática puede ser declarada en el encabezado de una clase pero no puede ser definida ahí, por lo general se hace en el archivo de implementación. En este caso se ha definido en la línea 15 y por lo tanto puede utilizarse en toda la clase.
En el siguiente programa veremos el primer ejemplo de un objeto con una cadena incrustada, bueno, en realidad contiene un puntero incrustado, pero como ambos funcionan de manera similar, estudiar uno nos lleva a comprender al otro, el código es el siguiente:
#include <iostream.h>
class caja
{
int longitud;
int ancho;
char *linea_de_texto;
public:
caja(char *linea_de_entrada); // Constructor
void coloca(int nueva_longitud, int nuevo_ancho);
int obtiene_area(void);
};
caja::caja(char *linea_de_entrada) // Implementacion del constructor
{
longitud = 8;
ancho = 8;
linea_de_texto = linea_de_entrada;
}
// Este metodo determina el tamaño de la caja segun los parametros de entrada
void caja::coloca(int nueva_longitud, int nuevo_ancho)
{
longitud = nueva_longitud;
ancho = nuevo_ancho;
}
// Calcula y retorna el area de una instancia de caja
int caja::obtiene_area(void)
{
cout << linea_de_texto << "= ";
return (longitud * ancho);
}
int main()
{
caja chica("caja chica "), mediana("caja mediana "), grande("caja grande ");
chica.coloca(5, 7);
grande.coloca(15, 20);
cout << "El area de la ";
cout << chica.obtiene_area() << "\n";
cout << "El area de la ";
cout << mediana.obtiene_area() << "\n";
cout << "El area de la ";
cout << grande.obtiene_area() << "\n";
return 0;
}
La línea 7 contiene un puntero a un char llamado linea_de_texto. El constructor lleva un parámetro de entrada llamado linea_de_entrada el cual es un puntero a una cadena la cual será copiada a la cadena llamada linea_de_texto dentro del constructor. Usted puede observar que al momento de definir las tres cajas hemos añadido una cadena constante como parámetro en cada declaración la cual es utilizada por el constructor como los datos a los cuales señala el puntero. Cuando llamamos a obtiene_area( ) el las líneas 42 a 47, desplegamos el mensaje y el area es retornada. Es prudente poner estas operaciones en métodos diferentes ya que no hay una aparente conexión entre desplegar el mensaje y calcular el area, pero se ha escrito de ésta manera con propósito de ilustración. Lo que ésto significa es la posibilidad de tener un método con un efecto lateral, el mensaje desplegado en el monitor y el valor retornado, es decir, el area de la caja. Complile y ejecute éste programa para un mejor entendimiento de su funcionamiento.
Hagamos unas modificaciones a nuestro código para presentar un ejemplo de un puntero incrustado el cual será utilizado para asignar datos dinámicamente, el código es ahora:
#include <iostream.h>
class caja
{
int longitud;
int ancho;
int *puntero;
public:
caja(void); // Constructor
void coloca(int nueva_longitud, int nuevo_ancho, int valor_almacenado);
int obtiene_area(void) {return longitud * ancho;} // Implementacion en linea
int obtiene_valor(void) {return *puntero;} // Implementacion en linea
~caja(); // Destructor
};
caja::caja(void) // Implementacion del constructor
{
longitud = 8;
ancho = 8;
puntero = new int;
*puntero = 112;
}
// Este metodo determina el tamaño de la caja segun los parametros de entrada
void caja::coloca(int nueva_longitud, int nuevo_ancho, int valor_almacenado)
{
longitud = nueva_longitud;
ancho = nuevo_ancho;
*puntero = valor_almacenado;
}
caja::~caja(void) // Destructor
{
longitud = 0;
ancho = 0;
delete puntero;
}
int main()
{
caja chica, mediana, grande;
chica.coloca(5, 7, 177);
grande.coloca(15, 20, 999);
cout << "El area de la caja chica es " << chica.obtiene_area() << "\n";
cout << "El area de la caja mediana es " << mediana.obtiene_area() << "\n";
cout << "El area de la caja grande es " << grande.obtiene_area() << "\n";
cout << "El valor almacenado en caja chica es " << chica.obtiene_valor() << "\n";
cout << "El valor almacenado en caja mediana es " << mediana.obtiene_valor() << "\n";
cout << "El valor almacenado en caja grande es " << grande.obtiene_valor() << "\n";
return 0;
}
En la línea 7 declaramos un puntero a una variable de tipo int, pero sólo es un puntero, no hay espacio para almacenamiento asociado a él. El constructor por tanto ubica una variable de tipo entero en la pila (heap) para utilizarla con el puntero en la línea 20. debe quedarle claro que los tres objetos definidos en la línea 41 cada uno contiene un puntero el cual señala a tres diferentes ubicaciones en la pila (heap), cada objeto tiene su propia variable ubicada dinámicamente para su uso privado, más aún, cada una tiene almacenada el valor de 112 porque la línea 21 almacena ese valor en cada una de las tres ubicaciones, una por cada llamada al constructor. En un programa tan pequeño no hay manera de desbordar la pila así que no se requiere una prueba de disponibilidad de memoria.
El método llamado coloca ( ) tiene asociado tres parámetros siendo el tercero el utilizado para asignar el valor a la nueva variable dinámicamente ubicada. Las tres areas se despliegan seguidas por los valores almacenados en las variablea dinámicamente ubicadas. En éste programa es necesario el uso de un destructor ya que sin éste, al terminar el programa principal dejaríamos tres variables dinámicamente ubicadas en la pila sin ningún puntero que señale a ellas, serían inaccesibles y ocuparían espacio en memoria sin provecho, por esta razón utilizamos el destructor para delete (borrar) la variable a la que se refiere el puntero llamado puntero, en este caso, las líneas 34 y 35 asignan cero a las variables que serán automáticamente borradas, aunque no absolutamente necesarias, son enunciados permitidos en C++.
Nuevamente modificamos el código de ejemplo para mostrar un objeto ubicado dinámicamente, estudie el siguiente ejemplo:
#include <iostream.h>
class caja
{
int longitud;
int ancho;
public:
caja(void); // Constructor
void coloca(int nueva_longitud, int nuevo_ancho);
int obtiene_area(void);
};
caja::caja(void) // Implementacion del constructor
{
longitud = 8;
ancho = 8;
}
// Este metodo determina el tamaño de la caja segun los parametros de entrada
void caja::coloca(int nueva_longitud, int nuevo_ancho)
{
longitud = nueva_longitud;
ancho = nuevo_ancho;
}
// Este metodo calcula y retorna el area de una instancia de caja
int caja::obtiene_area(void)
{
return (longitud * ancho);
}
int main()
{
caja chica, mediana, grande;
caja *puntero;
chica.coloca(5, 7);
grande.coloca(15, 20);
puntero = new caja;
cout << "El area de la caja chica es " << chica.obtiene_area() << "\n";
cout << "El area de la caja mediana es " << mediana.obtiene_area() << "\n";
cout << "El area de la caja grande es " << grande.obtiene_area() << "\n";
cout << "La nueva area de caja es " << puntero->obtiene_area() << "\n";
puntero->coloca(12, 12);
cout << "La nueva area de caja es " << puntero->obtiene_area() << "\n";
delete puntero;
return 0;
}
En la línea 35 definimos un puntero a un objeto de tipo caja y como sólo es un puntero con nada a qué señalar, ubicamos dinámicamente un objeto para este puntero en la línea 40, con el objeto creado en la pila (heap) como cualquier variable ubicada dinámicamente. Cuando el objeto es creado en la línea 40, el constructor es llamado automáticamente para asignar valores a las dos variables de almacenamiento internas, observe que el constructor no es llamado durante la definición del puntero ya que no hay nada que inicializar, es llamado al ubicar el objeto.
La referencia a los componentes del objeto son manejados de una manera similar a como se hace en las estructuras, utilizando el operador de puntero como se ilustra en las líneas 45 a 47. Finalmente, el objeto es borrado en la línea 49 y el programa termina. A estas alturas habrá notado que el uso de objetos es similar a las estructuras.
La siguiente modificación a nuestro programa nos muestra un objeto con una referencia interna a otro objeto de su propia clase:
#include <iostream.h>
class caja
{
int longitud;
int ancho;
caja *otra_caja;
public:
caja(void); // Constructor
void coloca(int nueva_longitud, int nuevo_ancho);
int obtiene_area(void);
void apunta_al_siguiente(caja *donde_apuntar);
caja *obtiene_siguiente(void);
};
caja::caja(void) // Implementacion del constructor
{
longitud = 8;
ancho = 8;
otra_caja = NULL;
}
// Este metodo determina el tamaño de la caja segun los parametros de entrada
void caja::coloca(int nueva_longitud, int nuevo_ancho)
{
longitud = nueva_longitud;
ancho = nuevo_ancho;
}
// Este metodo calcula y retorna el area de una instancia de caja
int caja::obtiene_area(void)
{
return (longitud * ancho);
}
// Este metodo obliga al puntero a señalar al parametro de entrada
void caja::apunta_al_siguiente(caja *donde_apuntar)
{
otra_caja = donde_apuntar;
}
// Este metodo retorna la caja señalada
caja *caja::obtiene_siguiente(void)
{
return otra_caja;
}
int main()
{
caja chica, mediana, grande;
caja *caja_puntero;
chica.coloca(5, 7);
grande.coloca(15, 20);
cout << "El area de la caja chica es " << chica.obtiene_area() << "\n";
cout << "El area de la caja mediana es " << mediana.obtiene_area() << "\n";
cout << "El area de la caja grande es " << grande.obtiene_area() << "\n";
chica.apunta_al_siguiente(&mediana);
mediana.apunta_al_siguiente(&grande);
caja_puntero = &chica;
caja_puntero = caja_puntero->obtiene_siguiente();
cout << "La caja senalada tiene una area de " << caja_puntero->obtiene_area() << "\n";
return 0;
}
El constructor contiene el enunciado que asigna al puntero el valor de NULL para inicializarlo en la línea 20. Es buena idea inicializar los punteros de sus programas a algo. En las líneas 12 y 13 declaramos dos métodos adicionales, el de la línea 13 no se ha mencionado aún en este tutorial, éste método regresa un puntero a un objeto de la clase caja. Como Usted sabe, es posible regresar un puntero a una estructura en C y el método de la línea 13 es la construcción equivalente en C++. La implementación en las líneas 42 a la 45 regresa el puntero almacenado como una variable miembro dentro del objeto.
Un puntero adicional llamado caja_puntero está definido en el programa principal para usarse posteriormente y en la línea 59 hacemos el puntero incrustado dentro de caja chica señalando a caja mediana. En la línea 60 hacemos que el puntero incrustado en caja mediana señale a caja grande. Hemos generado efectivamente una lista enlazada con tres elementos. En la línea 61 hecemos que el puntero extra señale a la caja chica, continuando en la línea 62 lo utilizamos para referirnos a la caja chica y actualizarla al valor contenido en la caja chica el cual es la dirección de la caja mediana. De esta manera hemos atravesado de un elemento de la lista a otro enviando un mensaje a uno de los objetos.
La palabra this se defini para cualquier objeto como un puntero al objeto en el cual está contenido, concretamente se define como
nombre_de_la_clase *this;
y es inicializada para señalar al objeto para el cual la función miembro es invocada, esta instrucción es muy útil cuando se trabaja con punteros, especialmente con listas enlazadas cuando se requiere referenciar a un puntero a un objeto que Usted insertará en la lista. El uso de la palabra clave this aún no se ha ilustrado pero lo haremos posteriormente en este tutorial.
El siguiente programa de ejemplo es una lista enlazada escrita en notación orientada a objetos:
#include <iostream.h>
class caja
{
int longitud;
int ancho;
caja *otra_caja;
public:
caja(void); // Constructor
void coloca(int nueva_longitud, int nuevo_ancho);
int obtiene_area(void);
void apunta_al_siguiente(caja *donde_apunta);
caja *obtiene_siguiente(void);
};
caja::caja(void) // Implementacion del constructor
{
longitud = 8;
ancho = 8;
otra_caja = NULL;
}
// El tamaño de la caja de acuerdo a los parametros de entrada
void caja::coloca(int nueva_longitud, int nuevo_ancho)
{
longitud = nueva_longitud;
ancho = nuevo_ancho;
}
// Calcula y regresa el area de una instancia de caja
int caja::obtiene_area(void)
{
return (longitud * ancho);
}
// Obliga al puntero a señalar al parametro de entrada
void caja::apunta_al_siguiente(caja *donde_apunta)
{
otra_caja = donde_apunta;
}
// Regresa la caja señalada
caja *caja::obtiene_siguiente(void)
{
return otra_caja;
}
int main()
{
caja *inicio = NULL; // Siempre señala el inicio de la lista
caja *temp; // Puntero temporal
caja *caja_puntero; // Utilizado para la creacion de caja
// Se genera la lista
for (int indice=0; indice<10; indice++)
{
caja_puntero = new caja;
caja_puntero->coloca(indice + 1, indice + 3);
if (inicio == NULL)
{
inicio = caja_puntero; // Primer elemento en la lista
}
else
{
temp->apunta_al_siguiente(caja_puntero); // Elemento adicional
}
temp = caja_puntero;
}
// Despliega la lista
temp = inicio;
do
{
cout << "El area es " << temp->obtiene_area() << "\n";
temp = temp->obtiene_siguiente();
}
while (temp != NULL);
// Borra la lista
temp = inicio;
do
{
temp = temp->obtiene_siguiente();
delete inicio;
inicio = temp;
}
while (temp != NULL);
return 0;
}
Este programa es muy similar al anterior, de hecho es idéntico hasta que entramos en el programa principal main ( ). Recordará que en el programa anterior la única manera que teníamos para utilizar el puntero incrustado era a traves del uso de dos métodos llamados apunta_al_siguiente ( ) y obtiene_siguiente los cuales están enlistados en las líneas 37 a la 46 del presente programa, las utilizaremos para construir nuestra lista enlazada, después la recorreremos y desplegaremos los resultados, finalmente borraremos la lista completa (delete) para liberar el espacio de la pila (heap).
En las líneas 50 a la 52 definimos tres punteros para utilizarlos en el programa. El puntero llamado inicio siempre señalará al principio de la lista, pero temp se moverá a lo largo de la lista conforme la vamos creando. El puntero llamado caja_puntero será utilizado para la creación de cada objeto, executamos un bucle en las líneas 54 a la 67 para generar la lista donde en la línea 56 ubica dinámicamente un nuevo objeto de la clase caja y la línea 57 le asigna datos a manera de ilustración. Si este es el primer elemento en la lista, el puntero inicio se coloca para señalar a este elemento, pero si el elemento yá existe, el último elemento de la lista es asignado para señalar al nuevo elemento. En cualquier caso, el puntero temp es asignado para señalar al último elemento de la lista en preparación para agregar un nuevo elemento, si es que lo hay.
En la línea 69, el puntero llamado temp señala al primer elemento de la lista actualizandose en la línea 73 en su recorrido por la lista en cada pasada del bucle, cuando temp obtiene el valor NULL proveniente del último elemento de la lista, finaliza el recorrido por la misma. Por último, borramos la lista completa un elemento a la vez en cada pasada del bucle de las líneas 78 a la 84