You are on page 1of 23

INSTITUTO TECNOLOGICO

de la laguna
Programación Orientada a Objetos en C++

4.- Objetos de tamaño arbitrario.

Hagamos una revisión de lo aprendido acerca de los beneficios que trae consigo
el uso de asignación dinámica de memoria.

1.- Los objetos pueden ser creados de manera dinámica, cuando no se sabe
cuantos objetos se iran a necesitar hasta el momento en que esta corriendo el
programa.

2.- Los objetos pueden ser creados de manera dinámica cuando no se conoce el
tiempo de vida de un objeto.

3.- El constructor para un objeto puede usar asignación dinámica de memoria,


cuando no se conoce cuanta memoria va a requerir el objeto, lo cual se conoce
hasta en tiempo de ejecución.
Como ejemplo de lo anterior podemos mencionar:
* En una clase matriz, por ejemplo, su tamaño puede ser conocido hasta el
momento de su creación.
* Cuando se manejan listas enlazadas, éstas pueden irse expandiendo.
* Una clase que crea texto basado en ventanas y que guarda su contenido en
memoria cuando otra ventana es sobrepuesta, la cantidad de memoria asignada
va a depender del tamaño de la ventana.

4.a) Un arreglo de tamaño dinámico.

Como un ejemplo de un objeto de tamaño arbitrario, presentamos un arreglo de


objetos de tamaño dinámico. El Arredin contiene un arreglo de punteros a objetos;
éstos son punteros del tipo void de tal manera que puedan apuntar a cualquier tipo de
objetos.
El Arredin puede contener cualquier número de apuntadores y no es necesario
restringir el arreglo a un tamaño definido. Llamando a la función suma() con un puntero,
se pueden agregar elementos al arreglo, el Arredin es como un barril sin fondo, ya que
teoricamente puede contener cualquier número de elementos, lo que nos brinda la
misma funcionalidad que una lista enlazada.

Un tipo de objeto, cuyo propósito es mantener otros objetos, se le llama con


frecuencia una clase contenedor y es una parte importante de la programación
orientada a objetos.

Un Arredin nos dá algunas ventajas sobre las listas enlazadas, uno puede
moverse a la parte superior del Arredin y buscar un elemento mediante una función,
llamada por ejemplo sig(), como se haría en una lista enlazada, en este caso también
podemos seleccionar objetos directamente, usando el operador de corchetes y un
índice. La función suma() nos regresa el índice del elemento que fué agregado. Es

Jove - 261 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

posible también encontrar un índice llamando a la función indice(), la cual nos regresa
el valor del indice del elemento actual.

El Arredin mantiene la posición actual, mediante un entero privado llamado


cursor, también sabe el tamaño del arreglo, mediante la variable tam. El lugar donde
se almacenan los datos se representa por medio de un puntero a void **arreglo. Debido
a que un puntero puede ser usado como si se tratase de un nombre de un arreglo, una
vez asignada memoria para arreglo, se le puede tratar como void *arreglo[tam].
Cuando se asigna memoria con el operador new, se debe indicar que clase de objeto
se esta creando y el número de elementos del arreglo.

Sintaxis de new y delete para manejar arreglos.

Sintaxis de new

puntero = new nom_tipo [tamaño]

Donde:

puntero Almacena la dirección inicial del arreglo.


nom_tipo Es el tipo del ojeto para el cual se quiere reservar n localidades de
memoria, también puede ser un tipo predefinido.
En el caso de ser un tipo definido por el ususario, va a llamar al
constructor para cada elemento del arreglo. Note que el constructor
no puede tener argumentos.
tamaño Número n de elementos para los cuales se quiere reservar
memoria.

Para arreglos multidimensionales:

puntero = new nom_tipo[a][b]...[z]

Sintaxis de delete.

delete []puntero

Note que para asegurar la operación adecuada del operador delete, se omite el
número de elementos a eliminar. Delete automaticamente elimina todo el espacio
reservado al arreglo y también, en el caso de tipos definidos por el usuario, llama al
destructor para cada elemento del arreglo.

Por ejemplo, para un arreglo de punteros de tamaño, tam:

arreglo = new void *[tam];

A continuación se muestra la interface de la clase Arredin.

Jove - 262 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

La clase Arredin esta construida como sigue:

Arredin
enum {fuera = 10};
+arreglo : void
tam : int
cursor : int

error()()
Arredin()()
~Arredin()
suma()()
remueve(int)()
remueve(void *)()
reinicia()()
sig()()
previo()()
indice()()
actual()()
tope()()
operator[]()()
cuenta()()

// Interface de: Un arreglo de tamaño dinámico


// PLCP65.H
#ifndef DINAARRE_H_
#define DINAARRE_H_

class Arredin {
enum { fuera = 10 }; // Cuanto incrementar el arreglo cuando
void **arreglo; // corre fuera del espacio asignado.
int tam;
int cursor;
void error(char *msg = "");
public:
Arredin();
~Arredin();

Jove - 263 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

int suma(void *);


int remueve(int);
int remueve(void *);
void reinicia();
void *sig();
void *previo();
int indice() { return cursor; }
void *actual() { return arreglo[cursor]; }
void *tope() { reinicia(); return actual(); }
void *operator[](int); // Selecciona el elemento.
int cuenta(); // Número de elementos vivos en el arreglo.
};

#endif // DINARRE_H_

Cuando un elemento de un arreglo es inicializado y limpiado, se hace un cast (0)


a un puntero a void (void *)0. Esto nos dice que estamos trabajando de manera explícita
con un puntero y no con un entero.

Para inicializar el arreglo a CERO se usa la función de la librería memset(), la


cual tiene la siguiente sintaxis:

memset

Descripción: Inicializa n bytes con el caracter c.

Sintaxis #include <mem.h>


void *memset(void *s, int c, size_t n)

Prototipo en: string.h, mem.h

V. regresado Un puntero a s.

La función suma(), inicia en la parte superior de la lista buscando el primer


espacio vacio de arreglo[], (esta función automáticamente recicla los espacios cuando
se remueven elementos). Cuando encuentra uno, pone un puntero a void en el espacio
y regresa el número de índice correspondiente al espacio, cuando no encuentra
espacios vacios, y llega al final del arreglo, deberá extender el arreglo.

El arreglo se extiende incrementando el valor de la variable fuera, la cual es una


constante que puede ser cambiada de tal manera de incrementar la eficiencia. Para
extender el arreglo, se define un vector temporal temp al cual se le asigna un tamaño
de [tam + fuera], los elementos anteriores se copian a este arreglo temporal, el tamaño

Jove - 264 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

adicional se inicializa a (void *) y los argumentos de suma() se insertan al inicio de la


adición de la variable fuera.

En este esquema, los espacios nunca son removidos de Arredin. Generalmente


un Arredin se va a ir extendiendo al tamaño necesario, y allí va a permanecer (ya que
se pueden reciclar los elementos no usados). Esto es adecuado para la mayoria de las
situaciones, sin embargo, si se tiene una aplicación donde el Arredin necesita ser
reducido, se puede crear una función que condence todos los espacios vacios y los
saque del arreglo, los vaya copiando en un espacio de memoria y luego borre toda esta
memoria. Esta función tiene la desventaja de mezclar todos los valores de los indices.

El algoritmo de busqueda, usado en este arreglo, trabaja bien para un pequeño


arreglo, pero si se tiene un arreglo grande y pocos espacios vacios, se vuelve
ineficiente. En esta circunstancia, tal vez, podemos agregar un segundo arreglo privado
que contenga los numeros de índice de los espacios vacios del primer arreglo.

La función sig() mueve el cursor al siguiente elemento no vacio de arreglo y


regresa su puntero, si se llega al final del arreglo, y no se encuentra una celda vacía,
regresa un puntero a (void *)0. Se puede obtener la dirección del elemento apuntado
por medio de la función actual(), también se puede obtener el indice a ese elemento,
mediante la función indice(). Podemos remover elementos del arreglo, mediante el uso
de la función sobrecargada, remueve(), el elemento a remover puede ser señalado, ya
sea por su indice o por el puntero a ser removido.

La función reinicia() mueve el cursor al inicio del arreglo, no precisamente al


primer elemento del arreglo, sino a la primer celda no vacía del arreglo. Con ello se
consigue poder utilizar primero la función reinicia() y luego la función actual(), y se
estaria apuntando al primer elemento válido del arreglo.

A continuación se presentan los metodos de la clase Arredin, explicados con


anterioridad.

// Los metodos de Arredin


// Las ventajas de un arreglo con la flexibilidad
// de las listas enlazadas.
// Programa PLCP65.CPP
#include "plcp65.h"
#include <iostream.h>
#include <stdlib.h>
#include <string.h>

void Arredin::error(char *msg)


{
cerr << "Error en Arredin: " << msg << endl;
exit(1);
}

Jove - 265 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

Arredin::Arredin()
{
tam = fuera;
cursor = 0;
arreglo = new void *[tam];
memset(arreglo, 0, sizeof(void *)*tam);
}

Arredin::~Arredin()
{
delete []arreglo;
}

int Arredin::suma(void *nuevo_elem)


{
for(int i = 0; i < tam; i++)
if(arreglo[i] == 0) {
arreglo[i] = nuevo_elem;
return i;
}
// En este punto ya no se encontro espacio.
// Es necesario agregar más espacio.
int temp_tam = tam + fuera;
void **temp = new void *[temp_tam];
memset(temp, 0, sizeof(void *)*temp_tam);
// Copia en temp el arreglo anterior;
for(i = 0; i < tam; i++)
temp[i] = arreglo[i];
temp[i = tam] = nuevo_elem;
delete arreglo;
arreglo = temp;
tam = temp_tam;
return i;
}

int Arredin::remueve(void *rp)


{
for(int i = 0; i < tam; i++)
if(arreglo[i] == rp) {
arreglo[i] = 0;
return 1;
}
return 0; // No encontrado
}

int Arredin::remueve(int ri)


{
if(ri < 0 || ri >= tam)

Jove - 266 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

error("Indice fuera de Rango");


// Verifica si hay un elemento en esta posición
if(arreglo[ri]) {
arreglo[ri] = 0;
return 1;
}
return 0; // No encontrado
}

void Arredin::reinicia()
{
cursor = 0;
while( (arreglo[cursor] == 0) && (cursor < tam) )
cursor++; // Encuentra el primer elemento NO CERO
}

void *Arredin::sig()
{
if(cursor == tam - 1) // Ultimo elemento.
return 0;
// No es el último elemento, incrementa hasta que encuentra
// una localidad no vacia, o el final.
while(arreglo[++cursor] == 0)
if( cursor == tam - 1) // No más elementos en la lista
return 0;
return arreglo[cursor];
}

void *Arredin::previo()
{
if(cursor == 0)
return actual();
while(arreglo[--cursor] == 0)
if(cursor == 0) // Tope de la lista
return 0;
return arreglo[cursor];
}

void *Arredin::operator[](int x)
{
if(x < 0 || x >= tam)
error("operator[] - Indice fuera de Rango");
return arreglo[x]; // Incluso si esta vacio.
}

Jove - 267 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

int Arredin::cuenta()
{
int cnt = 0;
for(int x = 0; x < tam; x++)
if(arreglo[x])
cnt++;
return cnt;
}

Para mostrar la aplicación de la clase Arredin se presenta el siguiente programa,


En donde se define una clase simple llamada cadena, la cual almacena un arreglo de
caracteres, el arreglo Arredin se va llenando con las direcciones de constante de
cadenas, lo cual simplifica la aplicación. La clase cadena sobrecarga el operador (<<)
para una salida más sencilla.

A continuación presentamos la aplicación.

// Una Aplicación de la clase Arredin


// Programa PLCP65A.CPP
#include "plcp65.h"
#include <string.h>
#include <stdlib.h>
#include <iostream.h>

// Una clase para guardar una cadena e imprimirla

class cadena {
char *cad;
public:
cadena(char *msg = "") { cad = msg; }
char *cp() { return cad; }
friend ostream& operator<<(ostream& s, cadena *sp);
};

ostream& operator<<(ostream& s, cadena *sp)


{
return s << sp -> cad;
}

void main()
{
Arredin da;

da.suma( new cadena("Esta es una "));


da.suma( new cadena("prueba de la "));
da.suma( new cadena("clase Arredin\n"));

Jove - 268 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

da.suma( new cadena("Tecleamos un error (OOPS)!\n"));


da.suma( new cadena("Vamos a ver si "));
da.suma( new cadena("va a crecer de "));
da.suma( new cadena("manera automática\n"));
da.suma( new cadena("cuando revase el "));
da.suma( new cadena("Rango fijado por tam\n"));
da.suma( new cadena("Seguimos agregando "));
da.suma( new cadena("más cadenas "));
da.suma( new cadena("debemos tener aun más\n"));
da.suma( new cadena("Debe se una "));
da.suma( new cadena("lista grande de cadenas\n"));
da.suma( new cadena("Con esto debe "));
da.suma( new cadena(" de ser suficiente\n"));

// Primero, imprime la lista


da.reinicia();
do
cout << (cadena *)da.actual();
while( da.sig() );
cout << endl << endl;

// Ahora encuentra el elemento con "OOPS"


da.reinicia();
while( strstr(((cadena *)da.actual()) -> cp(), "OOPS") == 0)
if(da.sig() == 0) {
cerr << "OOPS: no encontrado" << endl;
exit(1);
}
int rm = da.indice(); // Número de elemento a remover
cout << "removiendo " << (cadena *)da[rm] << endl;
delete (cadena *)da[rm]; // Borra la cadena objeto
if(da.remueve(rm))
cout << "Fué removido satisfactoriamente." << endl;
// Imprime de nuevo la lista.
// Mientras borra las cadenas
da.reinicia();
do {
cout << (cadena *)da.actual();
delete (cadena *)da.actual();
da.remueve(da.actual());
} while( da.sig() );
}

OBSERVACIONES.

Jove - 269 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

Este programa crea un arreglo de cadenas, procuramos que sean más cadenas
que el valor de fuera, de tal forma de poder probar la parte dinámica del Arredin, para
hacer ésto declaramos un objeto del tipo Arredin llamado da.

Cuando se declara el objeto da, del tipo Arredein se llama a su constructor, el


cual pide memoria al heap e inicializa sus elementos a (void *)0.

Usando la función suma(), vamos agregando los punteros de cada cadena a


arreglo, cuando se llegue a 10 cadenas, la misma función suma() debe hacer crecer de
manera dinámica el arreglo, para poder seguir sumando los punteros de las cadenas
que siguen.

Para imprimir la lista tal y cual fué almacenada en la clase Arredin, primero
ponemos el puntero a arreglo en su inicio, y luego llamamos a la función actual(), la
cual nos regresa el puntero a donde apunta indice. Al hacer el cast a cadena se llama a
la función operattor<<() la cual nos imprime la cadena correspondiente de acuerdo a la
dirección almacenada en arreglo. Mediante el do-while, imprimimos todas las cadenas,
tal y como fueron almacenadas.
A continuación, vamos a buscar la subcadena "00PS", en todas las cadenas
cuyas direcciones son almacenadas en arreglo. Para ello utilizamos la función strstr().
La función strstr() es una función de la librería del paquete, la cual definimos a
continuación:

strstr

Descripción: Busca en una cadena (s1), una subcadena (s2).

Sintaxis: #include <string.h>


char *strstr(const char *s1, const char *s2)

Prototipo en: string.h

V. regresado: Regresa un puntero al elemento s1, donde s2 inicia (apunta a s2 en


s1), Si no encuentra s2, en s1, regresa un NULL.

En la función strstr(), usamos la subcadena como una constante de cadena, y la


dirección de cad, la obtenemos haciendo referencia a da.actual() -> cp(), en donde la
función cp() regresa la dirección de cad, actual(), hace referencia al arreglo de
punteros arreglo, y da, es el objeto del tipo Arredin.

Cuando se encuentra la subcadena (en este caso, seguro que se va a


encontrar), su índice se almacena en la variable rm, se hace referencia al objeto da
usando el operator[]() y como argumento rm.

Jove - 270 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

Este operador, primero verifica que rm tenga un valor válido, si no lo tiene llama
a la función error(), para indicar que ha ocurrido un error, si rm esta dentro del rango,
regresa un puntero al elemento cuyo índice fué dado como argumento (rm).
En seguida se presenta una copia de la función, para mayor facilidad de su
análisis.

void *Arredin::operator[](int x)
{
if(x < 0 || x >= tam)
error("operator[] - Indice fuera de Rango");
return arreglo[x]; // Incluso si esta vacio.
}

La dirección regresada es aplicada al operator delete, para que sea eliminada


del heap. Una vez liberada la memoria del heap, se llama a la función remueve() para
colocar en esa posición de arreglo, un 0, lo que nos indica que esa celda esta vacía.

-----------------------------------------
NOTE que mientras se hace referencia a un puntero desde el objeto da, se debe
hacer siempre un cast a un puntero a cadena (cadena *), antes de que pueda ser
utilizado, ésto es necesario ya que si no se utiliza este artificio, el compilador va a
producir un mensaje de error, por intentar una dereferencia a un puntero del tipo void.
-----------------------------------------

Por último se imprimen las cadenas restantes, cuyas direcciones de ínicio, se


encuentran en arreglo, mientras van siendo eliminadas del heap.

Cuando salimos del ámbito del objeto da, se llama a su destructor. El cual nos
elimina del heap, todo el arreglo.

4.b) Características de Arredin.

La función de la librería realloc() toma un bloque de memoria asignada por


malloc(), calloc() o realloc() y expandiendo o reduciendolo. Si se quiere expander el
bloque de memoria y realloc() no encuentra espacio para el bloque, en donde
anteriormente se encontraba, copiara el bloque ya expandido en un nuevo espacio, lo
suficientemente grande para que lo contenga.

Si analizamos la función suma() definida en la clase Arredin, se puede ver que


en la función se realiza la misma actividad, aunque con menor eficiencia --El código
siempre es cambiado de lugar y copiado a esa nueva posición-- ya que no verifica si el
nuevo espacio ya incrementado, cabe en la posición anterior, actividad sí llevada a cabo
por la función realloc().

Jove - 271 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

4.c) Duplicando espacio para cadenas.

El programa anterior de la clase cadena lee la dirección de ínicio de una


constante de cadena, en realidad es lo que hemos venido haciendo en programas
anteriores. Ahora bien, si se desea manejar un caso más general de cualquier tipo de
(char *), en particular consideremos el siguiente caso; Si se desea crear un objeto a
cadena desde un arreglo local, y no se desea apuntar a un arreglo que pueda ser
cambiado, o el cual al salir de su ámbito se vuelva inválido.

La solución a este problema es asignar memoria sobre el heap y copiar la


cadena dentro de su área privada de datos. Con ello, se va a tener dos copias
separadas del dato, no importando lo que les suceda a los datos fuera de la clase.

Esta es otra situación en donde no se conoce sino hasta en tiempo de ejecución


cuanto espacio va a requerir un objeto. Cuando se asigna memoria sobre el heap, se
deberá solicitar memoria para toda la cadena. Afortunadamente la librería de ANSI C
nos proporciona un paquete de funciones de manejo de cadenas, lo que nos facilita
este trabajo. Entre estas funciones tenemos:

strlen() Nos regresa el tamaño de una cadena, sin incluir el NULO.

strcpy() Copia el contenido de un segundo argumento, en el primer argumento,


donde, ambos argumentos son punteros a caracter, incluyendo la
terminación en CERO.

En la siguiente aplicación, modificamos la clase cadena, para mostrar este


hecho.

// Programa PLCP66.CPP
// Una clase cadena, la cual maneja cualquier tipo
// de apuntador a caracter (dato static o automático)

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

// Una clase para guardar una cadena y luego imprimirla.

class cadena {
char *cad;
public:
cadena(char *msg = "") // Duplica el mensaje como un
{ // dato privado.
cad = new char[strlen(msg) + 1];
strcpy(cad, msg);
}
char *cp() { return cad; }

Jove - 272 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

friend ostream& operator<<(ostream& s, cadena& sp) {


s << sp.cad;
return s;
}
};

void main()
{
char *cads = "Este es un mensaje clave";
cadena S(cads);

cout << "Mensaje original: " << cads << endl;


cout << "En el objeto S : " << S << endl;
cads[0] = 'H';
cads[1] = 'O';
cads[2] = 'L';
cads[3] = 'A';
cout << "cads después de su modificación: " << cads << endl;
cout << "S después de su modificación: " << S << endl;
}

OBSERVACIONES.

La salida nos resulta:

Mensaje original: Este es un mensaje clave


En el objeto S : Este es un mensaje clave
cads después de su modificación: HOLA es un mensaje clave
S después de su modificación: Este es un mensaje clave

La sentencia:

cad = new char[strlen(cad) + 1];

Crea un arreglo de caracteres sobre el heap, el tamaño del arreglo es la longitud


de la cadena cad más un byte, para almacenar el CERO. El puntero cad contiene la
dirección de ínicio del arreglo de caracteres. El contenido de msg y su terminación
CERO son copiados en la nueva asignación de memoria mediante la sentencia:
strcpy(cad, msg);

Es posible también asignar memoria para un tipo predefinido o un arreglo de


elementos de un tipo predefinido. Por ejemplo, para crear asignación dinámica para
almacenar un tipo double, podemos escribir:

double *dp = new double;

Jove - 273 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

La librería de ANSI C, nos brinda una función llamada strdup(), la cual asigna
almacenamiento para una cadena, además nos copia la cadena en ese espacio, y nos
regresa la dirección de inicio del espacio asignado, sin embargo strdup() usa la función
malloc() y no el operador new, así que se deberá recordar al liberar la memoria usar
free() y no delete.

4.d) Ejemplo de codificado, con asignación dinámica a cadena.

En este ejemplo se crea una clase, en donde mediante una función miembro
almacenamos una cadena secreta codificada, usando una llave (dada como cadena)
para su codificación.
Posteriormente dando la llave, podemos reconstruir la cadena.

Veamos un ejemplo:

La interface nos resulta.

// PROGRAMA PLCP67.H
// Interface, clase que guarda un cadena de prueba.
// codificado y decodificado

#ifndef ENCODE_H_
#define ENCODE_H_

class code {
char *code_cad;
unsigned char make_key(char *);
public:
code(char *msg, char *key); // Instala un mensaje
char *decode(char *key); // Decodifica y lee
};

#endif // ENCODE_H_

El dato privado, puntero a char code_cad, almacena la dirección del heap, donde
es almacenada la cadena codificada.

La función, también privada. make_key, construye la llave, que es la única que


nos puede dar acceso a la cadena de nuestro interés.
La función code(), es la que nos almacena la cadena en el heap y la codifica.
La función decode(), al recibir la llave correcta, nos muestra la cadena
almacenada en el heap después de decodificarla.

Veamos su implementación.

Jove - 274 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

// Programa PLCP67.CPP
// Uso de asignación dinámica a cadenas
#include <iostream.h>
#include <string.h>
#include "plcp67.h"

unsigned char code::make_key(char *keycad)


{
unsigned char ckey = 0;
for(char *cp = keycad; *cp != 0; cp++)
ckey += *cp;
return ckey;
}

code::code(char *msg, char *key)


{
unsigned char ckey = make_key(key);
code_cad = new char[strlen(msg) + 1];
strcpy(code_cad, msg); // Se guarda el mensaje
for(char *cp = code_cad; *cp != 0; cp++)
*cp += ckey; // Usa la llave para codificar el mensaje
} // caracter por caracter.

char *code::decode(char *key)


{
unsigned char ckey = make_key(key);
for(char *cp = code_cad; *cp != 0; cp++)
*cp -= ckey; // Decodificación de la cadena
return code_cad;
}

La función make_key(), a partir de la cadena dada como llave construye un


número el cual es usado para codificar la cadena de interés, ésto se realiza en la
función code(), la cual además de reservar espacio en el heap, y almacenar la cadena,
codifica cada caracter de la cadena, agregandole el valor de la llave calculado en la
función make_key().

Para decodificar la cadena usamos la función decode(), la cual recibe la llave


(como cadena), reconstruye el número que representa el código, y luego, recorre la
cadena codificada en memoria y a cada caracter le va restando el valor calculado en la
función make_key(), con lo que de nuevo es restaurada la cadena original, como
regresa la dirección de inicio de la cadena en el heap, ésta puede ser impresa sin
ninguna dificultad.

Veamos una Aplicación.

Jove - 275 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

// Programa PLCP67A.CPP
// Aplicación de la clase "code"
#include <iostream.h>
#include "plcp67.h"

void main()
{
code mensa_oc("Esta es la prueba de cadena", "abre");
cout << mensa_oc.decode("abre") << endl;
}

OBSERVACIONES.

En la primer línea de main(), definimos un objeto mens_oc y lo inicializamos,


tanto con la cadena a codificar, como con la llave, la cual en este caso es "abre".
Para obtener de nuevo la cadena, de memoria, ya decodificada, llamamos a la
función decode(), dandole como argumento la cadena que sirvió de llave en la función
code().

5.- El mecanismo para la creación dinámica de objetos.

Cuando nos topamos con problemas en el manejo del área de almacenamiento


dinámico en memoria, es muy útil saber como C++ maneja este tipo de memoria.

5.a) El orden de llamada de constructores y destructores.

Debido a que C++ maneja la creación de objetos dinámicos y no solo la


asignación dinámica de memoria, en la llamada al operador new, también se realiza
una llamada al constructor de la clase, new nos regresa la dirección donde "vive" el
objeto. De manera similar, el destructor del objeto debera ser usado antes de que se
pierda la referencia al objeto.

Cuando se usa new para crear un objeto, se asigna la memoria suficiente para
almacenar los elementos de datos del objeto. (estos elementos normalmente se
encuentran en la pila y en el área de almacenamiento de memoria static) y a
continuación se llama al constructor del objeto. En el caso de que el objeto sea un
conjunto de otros objetos (ya sea a través de una herencia de clases o porque el objeto
contenga objetos miembro), los constructores son llamados siguiendo un orden; En
primer lugar se llamará al constructor de la clase base, luego el/los constructor(es) para
los objeto miembro, y por último el constructor de la clase derivada.

Jove - 276 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

Cuando se usa delete, para destruir un objeto, él cual fué previamente creado y
almacenado en el heap, la computadora, realiza primero un limpiado del objeto
mediante la llamada a su destructor. Si el objeto es un conglomerado de otros objetos el
destructor es llamado en un determinado orden, siendo el orden inverso del orden en el
cual se llamaron los constructores; primero se llama al destructor de la clase derivada,
luego a los destructores de los objetos miembro, y finalmente al destructor de la clase
base.

Aunque la herencia todavía no se ha explicado en detalle, utilizaremos para esta


discución un pequeño ejemplo, mediante el cual mostraremos el orden en el cual se
llaman a los constructores y a los destructores de un objeto, él cual involucra objetos
miembro y una clase base.
Analicemos el siguiente código.

// Programa PLCP68.CPP
// Análisis del orden de llamadas
// a constructores y destructores.
// Usando herencia y objetos miembro.
#include <iostream.h>

class miembro {
int x;
public:
miembro(int i) {
cout << "Constructor de miembro : " << i << endl;
x = i;
}
~miembro() {
cout << "Destructor de miembro : " << x << endl;
}
};

class base {
int xx;
miembro M;
public:
// Veamos como se inicializa un objeto miembro.
base(int a, int b) : M(b) {
cout << "Constructor de base : " << a << ", "
<< b << endl;
xx = a;
}
~base () {
cout << "Destructor de base : " << xx << endl;
}
};

Jove - 277 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

// Aquí es donde se produce la herencia también como


// otro objeto miembro.
class derivada : public base { // <----- herencia
int xxx;
miembro MM;
public:
derivada(int a, int b, int c) : base(a, b), MM(c) {
cout << "Constructor de derivada: " << a << ", " << b
<< ", " << c << endl;
xxx = a;
}
~derivada() {
cout << "Destructor de derivada: " << xxx << endl;
}
};

void main()
{
{
cout << "Creando objeto de derivada D(1,2,3)" << endl;
derivada D(1,2,3);
cout << "Derivada sale de ámbito" << endl;
} // Aquí se llama el destructor
cout << "Creando un objeto de derivada en el heap Dp(4,5,6)"
<< endl;
derivada *Dp = new derivada(4,5,6);
cout << "Destruyendo el objeto en el heap" << endl;
delete Dp;
}

OBSERVACIONES.

En el ejemplo anterior, cada constructor y destructor contienen una sentencia


cout, de tal forma que cuando son llamados podemos ver el orden en que se realizan
las llamadas.
La clase miembro es usada como un objeto miembro de ambas clases, de class
base y de class derivada. Para mostrar el orden en el que los objetos miembro son
construidos, el nombre de las clases corresponde a su papel desempeñado, lo que nos
permite ver el orden de llamadas de los constructores y destructores para; objetos
miembro, clase base y los objeto miembro contenidos en la clase base.

En el caso de usar un objeto de una clase, como dato miembro de otra clase, el
objeto deberá ser inicializado de una manera especial. Refiriendonos al constructor de
la clase donde fué definido un objeto, como dato miembro. El objeto miembro es

Jove - 278 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

llamado después de la lista de argumentos, pero antes del cuerpo del constructor. A
esta parte del constructor se le conoce como lista de inicialización del constructor.

base(in a, int b) : M(b) {


cuerpo del constructor;
}

Cuando una clase hereda de otra clase, la clase base deberá ser inicializada
antes del cuerpo del constructor de la clase derivada.

derivada(int a, int b, int c) : base(a, b), MM(c) {


cuerpo del constructor
}

La manera de especificar las llamadas a los constructores de una clase base y a


los objetos miembro en una clase derivada, nos sirve como recordatorio del orden en
que son llamados. De esta manera los constructores de una clase base o de un objeto
miembro, son llamados antes que el constructor de la clase derivada.

A continuación se muestra la salida del programa.

Creando objeto de derivada D(1,2,3)


Constructor de miembro : 2
Constructor de base : 1, 2
Constructor de miembro : 3
Constructor de derivada: 1, 2, 3
Derivada sale de ámbito
Destructor de derivada: 1
Destructor de miembro : 3
Destructor de base : 1
Destructor de miembro : 2
Creando un objeto de derivada en el heap Dp(4,5,6)
Constructor de miembro : 5
Constructor de base : 4, 5
Constructor de miembro : 6
Constructor de derivada: 4, 5, 6
Destruyendo el objeto en el heap
Destructor de derivada: 4
Destructor de miembro : 6
Destructor de base : 4
Destructor de miembro : 5

Como se puede ver, primero se llama al constructor de la clase miembro, ésto se


debe a que al llamar primero a la clase base, antes de llamar al constructor de la clase
base se llama desde la clase base al constructor de la clase miembro, a continuación se

Jove - 279 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

llama el contructor de la clase base, en seguida se llama al constructor de la clase


miembro, miembro de la clase derivada y por último al constructor de la clase derivada.

Cuando sale de ámbito, el objeto de la clase derivada, primero se llama al


destructor de la clase derivada, a continuación se llama al destructor de la clase base,
luego al destructor de la clase base y por último al destructor miembro de la clase base.

Si notamos el orden fué el inverso, con que fueron llamados los constructores.

En el caso de la creación de un objeto en el heap, el proceso de llamado a los


constructores y a los destructores es el mismo, solo que ahora, los constructores son
llamados cuando se llama a new y los destructores, cuando se llama de delete.

5.b) Arreglos de objetos.

Si se asigna memoria a un arreglo de objetos sin manejar inicializadores, la clase


a la que pertenecen los objetos deberá contener un constructor sin argumentos. A este
constructor se le llama constructor de default. La sintaxis para asignar memoria a un
arreglo de objetos es, para una clase llamada objeto.

objeto *op = new objeto[10];

Cuando se elimina del heap un arreglo de objetos de una clase, mediante un


destructor, se hará, precediendo la dirección de los objetos con un conjunto de
paréntesis cuadrados vacios, como se muestra.

delete []op;

Esto le dice al compilador que es necesario realizar una serie de llamadas al


destructor para cada objeto en el arreglo, y no solamente para el primer objeto. En
versiones anteriores de C++, era necesario definir la cantidad de veces que se debia
llamar al destructor, sin embargo si no se daba el número correcto se podia generar un
bug, de consecuencias catastroficas.

También es posible usar esta notación para arreglos de tipos predefinidos, pero
este tipo de datos no tienen destructores, en un objeto sin destructor, los argumentos
son ignorados. Sin embargo es siempre una buena idea emplear esta notación cuando
se destruyen arreglos.

Los constructores y los destructores son llamados automáticamente para cada


elemento, lo mismo sucede si el arreglo es creado con new y destruido con delete.

El siguiente ejemplo ilustra lo dicho anteriormente.

// Programa PLCP69.CPP

Jove - 280 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

// Llamada de constructores y destructores


// para cada objeto en un arreglo.
#include <iostream.h>

class linda {
static int i;
public:
linda() {
cout << "Llamada a constructor " << ++i << endl;
}
~linda() {
cout << "Llamada a destructor " << i-- << endl;
}
};

void objeto_auto()
{
linda A[5];
}

void objeto_dina()
{
linda *B = new linda[4];
delete []B;
}
int linda::i = 0;

void main()
{
cout << "Llamada objeto_auto........" << endl;
objeto_auto();
cout << "Llamada a objeto_dina......" << endl;
objeto_dina();
}

OBSERVACIONES.

La salida nos resulta:

Llamada objeto_auto.......
Llamada a constructor 1
Llamada a constructor 2
Llamada a constructor 3
Llamada a constructor 4

Jove - 281 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

Llamada a constructor 5
Llamada a destructor 5
Llamada a destructor 4
Llamada a destructor 3
Llamada a destructor 2
Llamada a destructor 1
Llamada a objeto_dina......
Llamada a constructor 1
Llamada a constructor 2
Llamada a constructor 3
Llamada a constructor 4
Llamada a destructor 4
Llamada a destructor 3
Llamada a destructor 2
Llamada a destructor 1

Podemos observar que tanto para el caso de objetos creados en la pila, como
para objetos creados en el heap, usando new y delete, la llamada al constructor y la
llamada al destructor se realiza para cada elemento del arreglo.

5.c) Operaciones internas.

Cuando se usa el operador new se le deberá dar al operador un argumento del


tipo de dato que se desea crear, de tal manera de que pueda saber la cantidad de
memoria a asignar.
Cuando se llama a delete, sin embargo, es necesario solo dar la dirección de
inicio (para un arreglo de objetos esta dirección deberá ir precedida por el juego de
corchetes [], de tal manera de que el destructor sea llamado para todos los objetos del
arreglo.
El algoritmo usado por new para conocer la cantidad de memoria asignada a un
arreglo u objeto, produce el guardado de dicho valor (cantidad de elementos) en algún
lugar de la memoria, puede ser en una tabla o al final del bloque de memoria asignado,
ésto depende de la implementación, cuando new, asigna memoria para un arreglo,
también se guarda el número de elementos almacenados en memoria.
Cuando se libera un bloque de memoria usando delete, el sistema marca el
bloque de memoria que ha sido liberado, de tal manera que la siguiente vez que se
llama a new, si la cantidad de memoria solicitada cabe en dicho bloque, éste bloque es
reasignado para almacenar la nueva información.

5.d) Fragmentación del heap.

Si un programa realiza una gran cantidad de asignaciones y liberaciones de


memoria se producirá un fenómeno llamado fragmentación del heap, entonces aunque
exista suficiente memoria para almacenar un determinado bloque, dicha memoria se

Jove - 282 -
INSTITUTO TECNOLOGICO
de la laguna
Programación Orientada a Objetos en C++

encuentra esparcida en pequeñas porciones en el heap, y no existe un bloque contiguo


del tamaño necesario para almacenar el bloque considerado.
Se pudiece pensar en diseñar un compactador de memoria, él cual pudiera dejar
la memoria libre como un solo bloque en la parte superior del heap, este compactador
debería tener control sobre los apuntadores y las longitudes de los bloques existentes, y
su relación con las variables usadas en el programa, lo que no resulta nada fácil al
tratar de diseñar un compactador de propósito general.
Algunos sistemas operativos como Apple Macintosh desarrollan una
compactación del heap, usando una técnica de doble indirección, un puntero lo maneja
el sistema operativo y puede cambiarlo, y el otro es para uso del programa que se
encuentra corriendo.
En el caso de new y delete no hacen referencia al sistema operativo, aunque en
un momento dado se pudieran implementar para hacerlo.

5.e) Punteros NULL

Generalmente, el valor de NULL para un puntero tiene un significado especial


(Note el uso de 0 en vez de NULL en C++ para incrementar su portabilidad). Significa,
que el puntero no esta apuntando a ninguna dirección. El 0 es un valor conveniente ya
que también es el valor lógico falso, entonces, ésto nos permite usarlo en sentencias
como while(ptr) o if(ptr).

Aunque es posible dereferenciar un puntero con un valor NULL, casi nunca se


hace ésto, ya que en algunas máquinas ésto puede producir que en realidad el puntero
si esta apuntando a una dirección, dirección que puede ser inclusive del sistema
operativo, y causar una catastrofe en el sistema.

En C++ es bastante común utilizar el valor 0, para indicar que un puntero esta
vacio. Esto es perfectamente soportado por delete, se puede llamar a delete con un
puntero a 0, lo cual no tiene ningún efecto, por lo que al liberar memoria usando delete,
no es necesario verificar si el puntero es 0 o no lo es.

ANSI C en la actualidad no especifica que el valor de NULL deberá ser 0. Una


implementación puede usar cualquier valor para NULL, él cual debera convertir de su
valor a 0 bajo ciertas circunstancias. Por ello no es muy conveniente usar el NULL, de
ahi que C++ sugiere el uso del 0, para garantizar su portabilidad, por el otro lado un cast
a NULL puede traer problemas.

5.f) Multiples liberaciones de memoria.

La memoria asignada de manera dinámica solamente debrá ser liberada una vez,
si se libera más de una vez, puede ser desastroso.

Jove - 283 -

You might also like