Encapsulado III


100% EN ESPAÑOL


Programando en C++
Encapsulado III
En esta lección:

Objetos anidados

El siguiente programa muestra un ejemplo de clases anidadas lo que resulta en objetos anidados. Un objeto anidado puede ilustrarse utilizando su computadora como analogía, la computadora en sí está compuesta de varios elementos que trabajan juntos pero cada uno desempeña una labor diferente, como el teclado, el disco duro y la fuente de alimentación. La computadora está compuesta de éstos y otros elementos muy diferentes entre sí al grado que es deseable discutir por separado lo concerniente al teclado y al disco duro. Una clase computadora puede estar compuesta de varios objetos diferentes anidando las clases diferentes dentro de la clase computadora

Por otra parte, si deseamos discutir respecto a las unidades de disco, podríamos desear examinar las características de las unidades de disco en general, después los detalles del disco duro y los diferentes discos flexibles. Esto involucraría el uso de la herencia porque la mayoría de los datos acerca de ámbos tipos de discos pueden ser caracterizados y aplicados a la unidad de disco genérica y utilizar la información para explicar las características en común. La herencia la estudiaremos en las siguientes lecciones, por lo pronto nos concentraremos en la clase anidada ó incrustada:


#include <iostream.h>

class correo
{
    int remitente;
    int franqueo;
    public:
    void coloca(int clase_interna, int franqueo_interno)
        {remitente = clase_interna; franqueo = franqueo_interno;}
    int obtiene_franqueo(void) {return franqueo;}
};

class caja
{
    int longitud;
    int ancho;
    correo etiqueta;
    public:
    void coloca(int l, int w, int s, int p)
        {longitud = l; ancho = w; etiqueta.coloca(s, p);}
    int obtiene_area(void) {return longitud * ancho;}
};

int main()
{
    caja chica, mediana, grande;

    chica.coloca(2, 4, 1, 35);
    mediana.coloca(5, 6, 2, 72);
    grande.coloca(8, 10, 4, 98);

    cout << "El area es " << chica.obtiene_area() << "\n";
    cout << "El area es " << mediana.obtiene_area() << "\n";
    cout << "El area es " << grande.obtiene_area() << "\n";

    return 0;
}

Este ejemplo contiene una clase llamada caja que a su vez contiene un objeto incrustado de otra clase en la línea 17, la clase correo. Este objeto está disponible para utilizarse sólo dentro de la implementación de caja por definición. El programa principal main ( ) tiene objetos definidos de la clase caja pero ningún objeto de correo por lo que la clase correo no puede ser referenciada en el programa principal. En éste caso los objetos de la clase correo están destinados a utilizarse dentro de la clase caja y un ejemplo está dado en la línea 20 donde se envía un mensaje al método etiqueta.coloca ( ) para inicializar las variables.

De primordial importancia es el hecho de que no hay un solo objeto de la clase correo declarado directamente en el programa principal, éstos son inherentemente declarados cuando los objetos de la clase caja son declarados. Por supuesto que se pueden declarar objetos de la clase correo en el programa principal si es necesario, pero no en el programa de ejemplo. Para estar completa, la clase caja debe tener uno o más métodos para usar la información almacenada en el objeto de la clase correo.

Volver al principio


Sobrecarga de operadores

El siguiente código presenta un ejemplo de sobrecarga de operadores, esto le permite definir una clase de objetos y redefinir el uso de los operadores normales, el resultado final es que los objetos de la nueva clase pueden utilizarse de una manera tan natural como con los tipos predefinidos, de hecho, parecen ser parte del lenguaje en lugar de una adición suya.


#include <iostream.h>

class caja
{
    int longitud;
    int ancho;
    public:
    void coloca(int l, int w) {longitud = l; ancho = w;}
    int obtiene_area(void) {return longitud * ancho;}
    friend caja operator+(caja a, caja b);  // Suma dos cajas
    friend caja operator+(int a, caja b);   // Suma una constante a una caja
    friend caja operator*(int a, caja b);   // Multiplica caja por una constante
};

caja operator+(caja a, caja b)  // Suma el ancho de dos cajas
{
    caja temp;
    temp.longitud = a.longitud;
    temp.ancho = a.ancho + b.ancho;
    return temp;
}

caja operator+(int a, caja b)   // Suma una constante a caja
{
    caja temp;
    temp.longitud = b.longitud;
    temp.ancho = a + b.ancho;
    return temp;
}

caja operator*(int a, caja b)   // Multiplica caja por una constante
{
    caja temp;
    temp.longitud = a * b.longitud;
    temp.ancho = a * b.ancho;
    return temp;
}

int main()
{
    caja chica, mediana, grande;
    caja temp;

    chica.coloca(2, 4);
    mediana.coloca(5, 6);
    grande.coloca(8, 10);

    cout << "El area es " << chica.obtiene_area() << "\n";
    cout << "El area es " << mediana.obtiene_area() << "\n";
    cout << "El area es " << grande.obtiene_area() << "\n";

    temp = chica + mediana;
    cout << "La nueva area es " << temp.obtiene_area() << "\n";
    temp = 10 + chica;
    cout << "La nueva area es " << temp.obtiene_area() << "\n";
    temp = 4 * grande;
    cout << "La nueva area es " << temp.obtiene_area() << "\n";

    return 0;
}

En este caso sobrecargamos los operadores de + y * con las declaraciones en las líneas 10 a la 12, y en las definiciones en las líneas 15 a la 37. Los métodos están declarados como funciones friend por lo que podemos utilizar la función de doble parámetro como está indicado. Si no utilizamos la construcción friend la función sería parte de uno de los objetos y tal objeto sería el receptor del mensaje. Incluyendo la construcción friend nos permite separar éste método del objeto y llamar al método con una notación fija, utilizando ésta técnica puede escribirse como objeto1 + objeto2 en lugar de objeto1.operador+(objeto2). Además sin la construcción friend no podríamos utilizar una sobrecarga con una variable de tipo int para el primer parámetro porque no podemos enviar un mensaje a una variable de tipoint como en int.operador+(objeto). Dos de los tres operadores sobrecargados usan un int para el primer parámetro por lo que es necesario declararlos como funciones friend. No existe un límite superior en el número de sobrecargas para un operador dado, cualquier número de sobrecargas puede utilizarse siempre y cuando los parámetros sean diferentes para cada sobrecarga particular.

El encabezado en la línea 15 ilustra la primera sobrecarga donde el operador + es sobrecargado dando el tipo retornado seguido por la palabra clave operator y el operador que deseamos sobregargar. Los dos parámetros formales y sus respectivos tipos son listados en paréntesis y las operaciones normales son dadas en la implementación de la función en las líneas 17 a la 20. El estudiante observador notará que la implementación de las funciones friend no son parte de la clase porque el nombre de la clase no está antes del nombre del método en la línea 15.
La mayor diferencia la encontramos en la línea 52 donde éste método es llamado utilizando la notación infija en lugar del formato usual de enviar mensajes. Como las variables chica y mediana son objetos de la clase caja, el sistema buscará una manera de utilizar el operador + en los dos objetos de la clase caja y la encontrará en el operador sobrecargado operator+ que ya hemos discutido. En la línea 54 pedimos al sistema que sume un int a un objeto de la clase caja por lo que el sistema encontrará la otra sobrecarga del operador + que empieza en la línea 23 para ejecutar la operación solicitada. Además en la línea 56 le pedimos al sistema que utilice el operador * con una constante de tipo int y un objeto de la clase caja lo que queda satisfecho en las líneas 31 a 37. Observe que sería ilegal tratar de hacer la operación inversa, grande*4 ya que no definimos para utilizar los métodos de ésta manera. Observe además que cuando usamos la sobrecarga de operadores estamos a la vez utilizando sobrecarga de nombres de función ya que algunos de los nombres son los mismos. El resultado de la ejecución del programa es éste:

Sobrecarga de operadores

Volver al principio


Sobrecargando funciones en una clase

El programa siguiente es un ejemplo del nombre de una función sobrecargado dentro de una clase, en éste programa el constructor también es sobrecargado en uno de los métodos para ilustrar que ésto es posible, estudie el siguiente código:


#include <iostream.h>

class muchos_nombres
{
    int longitud;
    int ancho;
    public:
    muchos_nombres(void);// Constructors
    muchos_nombres(int lon);
    muchos_nombres(int lon, int anc);
    void despliega(void);// Display functions
    void despliega(int uno);
    void despliega(int uno, int dos);
    void despliega(float numero);
};

muchos_nombres::muchos_nombres(void)
{
    longitud = 8;
    ancho = 8;
}

muchos_nombres::muchos_nombres(int lon)
{
    longitud = lon;
    ancho = 8;
}

muchos_nombres::muchos_nombres(int lon, int anc)
{
    longitud = lon;
    ancho = anc;
}

void muchos_nombres::despliega(void)
{
    cout << "De la funcion despliega(void) , area = " << longitud * ancho << "\n";
}

void muchos_nombres::despliega(int uno)
{
    cout << "De la funcion despliega(int uno) area = " << longitud * ancho << "\n";
}

void muchos_nombres::despliega(int uno, int dos)
{
    cout << "De la funcion de dos int, area = " << longitud * ancho << "\n";
}

void muchos_nombres::despliega(float numero)
{
    cout << "De la funcion despliega(float), area = " << longitud * ancho << "\n";
}

int main()
{
    muchos_nombres chica, mediana(10), grande(12, 15);
    int neto = 144;
    float pi = 3.1415, nomina = 12.50;

    chica.despliega();
    chica.despliega(100);
    chica.despliega(neto,100);
    chica.despliega(nomina);

    mediana.despliega();
    grande.despliega(pi);

    return 0;
}

Tenemos tres constructores, el que es llamado en determinado momento es seleccionado por el número y tipos de los parámetros en la definición. En la línea 57 del programa principal son declarados los tres objetos cada uno con diferente número de parámetros y una inspección de los resultados indicará que el correcto constructor fué llamado basado en el número de parámetros.
Respecto al comentario que sigue puede pensar que es un poco tonto, pero en realidad se trata de un asunto importante, a lo largo de éste tutorial hemos estado utilizando un operador sobrecargado el cual no le ha causado confusión alguna, se trata del operador << que forma parte de la clase cout el cual opera como una función sobrecargada ya que la manera en que despliega los datos está en función del tipo de variable de entrada o del campo que deseamos desplegar.

Algunos métodos por defecto

Aún en el caso en que Usted no incluya constructores u operadores sobrecargados, Usted cuenta con algunos definidos automáticamente por el compilador, esto lo ilustramos en el siguiente código de ejemplo y también mostramos el por qué algunas veces es más conveniente escribir sus propios métodos para hacer el trabajo que se supone deben hacer los métodos dados por el compilador, antes debemos indicar una lista de reglas que los programadores deben seguir para una correcta implementación en C++:

Cualquier clase declarada y utilizada en C++ debe tener una manera de crear un objeto porque el compilador, por definición, debe llamar a un constructor cuando definimos un objeto. Si no proveemos un constructor , el compilador generará uno que pueda llamar durante la creación de un objeto, éste es el constructor por defecto y lo hemos utilizado inconcientemente en una buena cantidad de programas en éste tutorial. El constructor por defecto no inicializa ninguna de las variables miembro, pero prepara todas las referencias internas necesarias y llama al constructor base, si existe (Cuando estudiemos la herencia sabremos qué es un constructor base). El código de nuestro nuevo ejemplo es éste:


#include <iostream.h>
#include <string.h>
#include <stdlib.h>

class defectos
{
    int tamano;     // Un simple valor almacenado
    char *cadena;   // Un nombre para el dato almacenado
    public:
    // Esto sobrecarga el constructor por defecto
    defectos(void);
    // Esto sobrecarga el constructor copia por defecto
    defectos(defectos &en_objeto);
    // Esto sobrecarga el operador de asignacion por defecto
    defectos &operator=(defectos &en_objeto);
    // Este destructor es necesario en caso de ubicacion dinamica
    ~defectos(void);
    // Finalmente, un par de metodos
    void coloca_dato(int en_tamano, char *en_cadena);
    void obtiene_dato(char *fuera_cadena);
};

defectos::defectos(void)
{
    tamano = 0;
    cadena = new char[2];
    strcpy(cadena, "");
}

defectos::defectos(defectos &en_objeto)
{
    tamano = en_objeto.tamano;
    cadena = new char[strlen(en_objeto.cadena) + 1];
    strcpy(cadena, en_objeto.cadena);
}

defectos &defectos::operator=(defectos &en_objeto)
{
    delete [] cadena;
    tamano = en_objeto.tamano;
    cadena = new char[strlen(en_objeto.cadena) + 1];
    strcpy(cadena, en_objeto.cadena);
    return *this;
}

defectos::~defectos(void)
{
    delete [] cadena;
}

void defectos::coloca_dato(int en_tamano, char *en_cadena)
{
    tamano = en_tamano;
    delete [] cadena;
    cadena = new char[strlen(en_cadena) + 1];
    strcpy(cadena, en_cadena);
}

void defectos::obtiene_dato(char *fuera_cadena)
{
    char temp[10];
    strcpy(fuera_cadena, cadena);
    strcat(fuera_cadena, " = ");
    _itoa(tamano, temp, 10);
    strcat(fuera_cadena, temp);
}

int main()
{
    char buffer[80];
    defectos mi_dato;

    mi_dato.coloca_dato(8, "tamano de sombrero");
    mi_dato.obtiene_dato(buffer);
    cout << buffer << "\n\n";

    defectos mas_dato(mi_dato);
    mas_dato.coloca_dato(12, "tamano de zapato");
    mi_dato.obtiene_dato(buffer);
    cout << buffer << "\n";
    mas_dato.obtiene_dato(buffer);
    cout << buffer << "\n";

    mi_dato = mas_dato;
    mi_dato.obtiene_dato(buffer);
    cout << buffer << "\n";
    mas_dato.obtiene_dato(buffer);
    cout << buffer << "\n";

    return 0;
}

La línea 11 declara un constructor por defecto el cual es llamado cuando Usted define un objeto sin parámetros, en éste caso el constructor es necesario porque tenemos una cadena incrustada en la clase que requiere una ubicación dinámica y una inicialización de la cadena a nulo. Debemos pensar un poco para comprender que nuestro constructor es mejor que el aportado por el compilador el cual nos dejaría con un puntero no inicializado. El constructor por defecto se utiliza en la línea 71 del programa.

El constructor copia

El constructor copia es generado automáticamente por Usted por el compilador en caso de que no haya sido definido, se utiliza para copiar el contenido de un objeto a un nuevo objeto durante la construcción del susodicho. Si el compilador lo genera simplemente copiará el contenido del original en un nuevo objeto como en una copia byte por byte, lo que puede ser no deseable. Para clases sencillas sin punteros esto por lo general es suficiente, pero en el presente ejemplo tenemos un puntero como miembro de clase así que copiaría el puntero byte por byte dando como resultado dos punteros señalando al mismo sitio. Para este programa declaramos nuestro propio constructor copia en la línea 13 y se implementa en la líneas 30 a la 35. Un estudio cuidadoso de la implementación revelará que la nueva clase es idéntica a la original pero la nueva clase cuenta con su propia cadena. Como ambos constructores contienen ubicación dinámica, debemos asegurarnos que el dato ubicado sea destruido cuando terminemos de utilizar los objetos, por lo que es necesario un destructor cuya implementación la vemos en las línea 46 a la 49. El constructor copia es utilizado en la línea 77.

El operador de asignación

Aunque no es demasiado obvio, pero éste programa requiere de un operador de asignación porque el provisto por el compilador simplemente copia el objeto fuente al objeto de destino byte por byte, esto conduciría a un problema similar al discutido con el constructor copia. El operador de asignación es declarado en la línea 15 y definido en las líneas 37 a la 44 donde desubicamos la vieja cadena en el objeto existente antes de ubicar espacio para el nuevo texto y copiar el texto del objeto fuente en el nuevo objeto, el operador de asignación se utiliza en la línea 84. Debe quedar claro para el estudiante que cuando en una clase es definida para incluir cualquier cantidad de ubicación dinámica, los tres métodos mostrados en el ejemplo deben utilizarse junto con el correspondiente destructor, de omitirlos el programa tendrá un comportamiento errático e impredecible. Compile y ejecute el programa, el resultado es éste:

Métodos por defecto

Volver al principio


| Indice | ¿Comentarios ó dudas? | Leccion anterior | Siguiente lección |

Esta página es patrocinada por:  GeoCities Construya su propia página gratis

© 1998, Virgilio Gómez Negrete

.