SlideShare uma empresa Scribd logo
1 de 28
Baixar para ler offline
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                                                                               -1-



     Clases y manejo de memoria dinámica en C++
Repaso a las funciones de manejo de memoria dinámica en C ............................................................................1
  Función calloc()...................................................................................................................................................1
  Función malloc() .................................................................................................................................................2
  Función realloc() .................................................................................................................................................2
  Función free() ......................................................................................................................................................3
El operador new frente a malloc ............................................................................................................................3
El operador delete frente a free ..............................................................................................................................8
Sobrecarga de new y de delete ................................................................................................................................9
  Sobrecarga de new y delete respecto a una clase específica.............................................................................11
Clases con punteros miembros .............................................................................................................................14
El Puntero this ......................................................................................................................................................22
  Funciones estáticas y el puntero this .................................................................................................................24
Paso y retorno de objetos ......................................................................................................................................24
Paso y retorno de referencias a objetos................................................................................................................26
Ejercicios propuestos ............................................................................................................................................27




Repaso a las funciones de manejo de memoria dinámica en C

Función calloc()
Prototipo:                                  void *calloc(int num, int tam);
Archivo cabecera:                           stdlib.h
Valor devuelto:                             Devuelve un puntero al primer byte del bloque de memoria reservada, o un
puntero
                                            NULL en el caso de no haberse podido reservar el bloque de memoria solicitado
Finalidad:                                  Reserva un bloque de memoria para almacenar num elementos de tam bytes cada
                                            uno de ellos. Todo el bloque de memoria queda iniciado a 0.

• num: indica el número de elementos con la misma estructura que ocuparán el bloque de memoria reservado.
• tam: indica el tamaño en bytes de cada uno de los elementos que van a ocupar el bloque de memoria
  reservada.

La cantidad de memoria reservada viene determinada por el resultado que se obtiene al multiplicar el número de
elementos a almacenar en el bloque de memoria por el tamaño en bytes de cada uno de esos elementos, es decir,
num * tam.

El uso de esta función se puede ilustrar con el siguiente ejemplo:

             int n=100, *pt;
             ...
             pt = ( int * ) calloc (n, sizeof(int));

En este ejemplo la función calloc reserva un bloque de memoria de n enteros, y retorna un puntero void a dicho
bloque, o NULL si n fuera 0 o se produjera un error al no poder reservar suficiente espacio para reservar la


Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                                                                      (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                           -2-


memoria solicitada. Sobre el puntero que se retorna se debe hacer una operación de cast apropiada, para
asignarlo al puntero del tipo que se está tratando.


Función malloc()
Prototipo:                    void *malloc(int tam);
Archivo cabecera:             stdlib.h
Valor devuelto:               Devuelve un puntero al primer byte del bloque de memoria reservada, o un
puntero
                              NULL en el caso de no haberse podido reservar el bloque de memoria solicitado
Finalidad:                    Reserva un bloque de memoria de tam bytes.

• tam: indica el tamaño en bytes del bloque de memoria que se desea reservar.

La cantidad de memoria reservada será de tam bytes.

El uso de esta función se puede ilustrar con el siguiente ejemplo:

         struct Registro *ptr_reg;
         ptr_reg = ( struct Registro * ) malloc (sizeof (struct Registro));

La función malloc reserva un bloque de memoria lo suficientemente grande como para acoger por completo al
tipo de dato, y retorna un puntero void a dicho bloque, o NULL si el tamaño del argumento es 0 o se ha
producido un error al no poder reservar suficiente espacio para alojar al tipo de dato. Sobre el puntero que se
retorna se debe hacer una operación de cast apropiada, para asignarlo al puntero del tipo que se está tratando.


Función realloc()
Prototipo:                    void *realloc(void *ptr, int nuevo_tamaño);
Archivo cabecera:             stdlib.h
Valor devuelto:               Devuelve un puntero al primer byte del bloque de memoria reservada, o un
puntero
                              NULL en el caso de no haberse podido reservar el bloque de memoria solicitado
Finalidad:                    Cambia el tamaño del bloque de memoria apuntada por ptr al nuevo tamaño
                              indicado por nuevo_tamaño

• ptr: puntero que apunta al bloque de memoria reservado.
• nuevo_tamaño: valor en bytes que indica el nuevo tamaño del bloque de memoria apuntado por ptr y que
  puede ser mayor o menor que el original.

En estas tres funciones es importante comprobar que el puntero devuelto por ellas no es un puntero nulo
(NULL) antes de hacer uso del bloque de memoria reservado.




Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                       (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                              -3-



Función free()
Prototipo:                    void free(void *ptr);
Archivo cabecera:             stdlib.h
Valor devuelto:               Ninguno
Finalidad:                    Libera el bloque de memoria apuntada por ptr y que previamente ha sido asignado
                              mediante malloc() o calloc().

• ptr: variable puntero que debe contener la dirección de memoria del primer byte del bloque de memoria que
  deseamos liberar. En caso de que no sea un puntero previamente reservado por malloc() o calloc(), la
  llamada a la función free() puede ocasionar una parada o interrupción del sistema.




El operador new frente a malloc
En el lenguaje C, la región de memoria que está disponible a la hora de la ejecución recibe el nombre de heap.
En C++ el espacio de memoria disponible se conoce como almacenamiento libre1. La diferencia entre los dos se
encuentra en las funciones que se utilicen para el acceder a esta memoria.

El método para solicitar memoria del heap en C se basa en el uso de la función malloc y sus derivadas (calloc,
realloc, ...). En C++ no es correcto utilizar malloc para alojar de forma dinámica una nueva instancia de una
clase. La razón es que si se usa malloc, se pierden las ventajas que nos ofrecen los constructores, ya que se llama
al constructor de la clase cada vez que se crea un objeto. Si se usa malloc para crear un objeto, se tiene un
puntero a un bloque de memoria sin iniciar. Entonces se pueden hacer llamadas a métodos con objetos que no
están bien construidos, y que por lo tanto van a dar resultados erróneos.

La técnica más adecuada es utilizar la alternativa que ofrece C++ mediante el operador new.

El operador new conoce la clase del objeto, o el tipo de la variable, que se quiere alojar en el almacenamiento
libre. De esta forma este operador llama automáticamente al constructor que corresponda, dependiendo de los
argumentos que se especifiquen, para iniciar la memoria que ha reservado.

La forma de utilizar este operador es la siguiente:
                      <nombre_puntero> = new <nombre> [<argumentos>];

De la sintaxis de new se puede deducir que no es una función, y que por lo tanto no tiene una lista de
argumentos entre paréntesis. Es un operador que se aplica al nombre del tipo, pero no se tiene que realizar un
cast 2 ni utilizar el operador sizeof para determinar su tamaño, porque new conoce el tipo de dato y su tamaño.

Si new no encuentra espacio para alojar lo que se le pide, devuelve 03.

El operador new reemplaza a las funciones malloc y calloc de la biblioteca estándar de C, pero no sustituye a
realloc. La función realloc puede ser simulada, pero la aproximación que se haga supondrá siempre un aumento


1
    Free store.
2
  El compilador comprueba que el tipo del puntero que devuelve new y el tipo del puntero al que se asigna son
del mismo tipo. En caso contrario genera un error.
3
  En C++ un puntero nulo vale 0 en lugar de NULL.

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                          (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                             -4-


de las líneas del código fuente, así como conocer el tamaño original del espacio reservado mediante el puntero
que se va cambiar. Pero afortunadamente se puede utilizar generalmente realloc para cambiar el tamaño del
espacio reservado con new.

Como se ha dicho, con new se puede reservar espacio para cualquier tipo de dato, siendo muy utilizado para
reservar espacio para matrices4. Un ejemplo:
                  float *centros;
                  centros = new float[numero];

Para ilustrar todo lo visto hasta el momento del operador new se ha realizado este sencillo programa de ejemplo:
// Programa: malloc versus new
// Fichero: MALVNEW.CPP
#include <iostream.h>
#include <string.h>
#include <stdlib.h>
class Prueba {
     int i, j;
     char   *s;
public:
   Prueba() {    // Constructor por defecto
     i=j=5;
     s=new char[5];
     strncpy(s, "Hola", 5);
   }
    Prueba(char *cad, int a, int b) { // Constructor con parámetros
      i=a; j=b;
      s=new char[strlen(cad)+1];
      strcpy(s, cad);
    }
    ~Prueba () {delete [] s;} // Destructor

    int miembro(void) { // Función para mostrar los datos de la clase
       cout << "n" << s << "n";
       return i+j;
    }
    void cambia(void) { // Función para ilustrar el uso de realloc
       int i=strlen(s);
       s=(char *)realloc(s, strlen(s)+2);
       s[i++]='+';
       s[i]='0';
    }
}; // Fin de la definición de la clase




4
 Las matrices que se reservan de forma dinámica no pueden recibir valores iniciales. Cuando se trate de una
matriz de objetos se debe realizar un constructor por defecto, en caso contrario el compilador generará un error.

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                         (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                             -5-


void main (void)
{
// Creamos un objeto dinámico con malloc
  Prueba *P1;
  P1=(Prueba *) malloc (sizeof (Prueba));

// Y nos vamos a estrellar: Salen caracteres raros por pantalla e
//                            incluso se puede colgar el programa
  cout << "n" << P1->miembro() << "n";


// Creamos un par de objetos dinámicos con new
  Prueba *P2, *P3;

     P2=new Prueba;        // Usa el constructor por defecto
     P3=new Prueba("Hola Caracola",4,3); // Usa el constructor con parámetros

// Todo es correcto
  cout << "n" << P2->miembro() << "n";
  cout << "n" << P3->miembro() << "n";

// Usamos realloc
  P2->cambia();
  P3->cambia();

// Y todo sigue OK
  cout << "n" << P2->miembro() << "n";
  cout << "n" << P3->miembro() << "n";


// Se libera la memoria que hemos reservado
// Es buena técnica de programación liberar el espacio reservado
// con malloc mediante la función free, y el espacio reservado con
// new con el operador delete.
  free(P1);
  delete P2;
  delete P3;
}

El operador new tiene otra ventaja sobre malloc que todavía no se ha tratado.

Es una buena técnica de programación controlar los posibles errores derivados de la falta de espacio a la hora de
reservarlo en el manejo dinámico de la memoria.

Cuando se programa en ANSI C, el programador tiene que construirse sus propios mecanismos para detectar
que se ha producido un error cuando se ha llamado a malloc5.

Sin embargo en C++, se puede seguir utilizando estas mismas técnicas de programación cuando se utiliza el
operador new6, o por el contrario se puede elegir una alternativa más conveniente que nos ofrece este lenguaje.



5
    Se recuerda que cuando esto ocurre malloc devuelve NULL.
6
    Ya que new devuelve 0 si no hay espacio.

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                         (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                             -6-


C++ define un puntero a función, de forma que cuando ocurre un error, la función a la que apunta el puntero es
llamada. La definición de dicho puntero es:
                                     void (* _new_handler)();


El uso de este puntero tiene el inconveniente de no ser estándar. Por ello se recomienda el uso de una función de
biblioteca definida en new.h, y que se llama set_new_handler, que toma como argumento un puntero a función,
y asigna la función al puntero _new_handler. Aunque este segundo método es más común, y por lo tanto más
conveniente, tampoco es estándar7.

// Programa: Manejo de errores en la reserva de espacio con new
//           Versión Borland C++
// Fichero: NEW_ERR.CPP

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

#define PASO 20000

void MemoriaInsuficiente(void)
{
   cerr << "naEl espacio en el Almacenamiento Libre agotó.n";
   exit(1);
}

void main (void)
{
  long total=0;
  set_new_handler ( MemoriaInsuficiente );

// Otra manera de hacerlo con Borland C++
//_new_handler = MemoriaInsuficiente;

    for(;;) {
    char *espacio = new char[PASO];
    total+=PASO;
    cout << "Llevamos reservados " << total << " bytes n";
    }
}




7
  Se hicieron pruebas con los compiladores Borland C++ 3.1, Visual C++ 1.0, y DJGPP 2.6.0. Sólo el
compilador de Borland aceptó la definición directa _new_handler = funcion;
Cuando se utilizó la función set_new_handler(funcion); el compilador de Borland y el DJGPP, funcionaron
sin problemas, pero para realizar esta operación con Visual C++ hubo que utilizar la función _set_new_handler
y redefinir el prototipo de la función que manejaba el error.


Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                         (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                         -7-


// Programa: Manejo de errores en la reserva de espacio con new
//           Versión Microsoft C++
// Fichero: NEW_ERR.CPP

#include <iostream.h>
#include <new.h>
#include <stdlib.h>
int MemoriaInsuficiente(size_t size)
{
  cerr << "naEl espacio en el Almacenamiento Libre se agotó.n";
  exit(1);
  return 0;
}

void main (void)
{
  long total=0;
  _set_new_handler ( MemoriaInsuficiente );

     for(;;) {
     char *espacio = new char[20000];
     total+=20000;
     cout << "Llevamos reservados " << total << " bytes n";
     }
}


En resumen:

Ventajas de new frente a malloc.
   Calcula de forma automática el tamaño del tipo para el que se está reservando memoria
   Proporciona el tipo correcto del puntero
   Es posible dar valores iniciales al objeto que se crea mediante el operador new
   La notación es mucha más clara
   Se puede definir una función que se encargue del manejo de los errores por falta de espacio en la zona de
   almacenamiento libre

Cuando usar uno u otro.
   Tratar de usar ambos sistemas de manejo de memoria dinámica a la vez puede acarrear posibles problemas
   de inconsistencias
   Se recomienda el uso en C++ del operador new siempre que sea posible8, desechando para casos muy
   concretos el uso de las funciones derivadas de malloc
   En caso de seguir empleando las funciones derivadas de malloc, recordar que cuando se vaya a reservar
   espacio para una clase, se debe utilizar el operador new, o crear unos métodos que permitan iniciar las
   variables privadas, y llamarlos nada más reservar el espacio con malloc




8
    Las ventajas que se obtienen son obvias.

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                     (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                            -8-



El operador delete frente a free
El operador delete se puede decir que es para new, lo que free para malloc. Esto es, libera los bloques de
memoria, dejándolos listos para futuras reservas.

La sintaxis de delete es muy sencilla:
                                         delete <nombre_puntero>;

Cuando se trata de liberar el espacio asociado un puntero a un objeto, delete de forma automática llama al
destructor.

El operador delete se debe utilizar sólo con punteros que retorne new. Mientras que la función free se debe usar
con punteros que apunten a zonas de memoria reservadas mediante funciones derivadas de malloc. De no
hacerse así, y dependiendo de los compiladores, puede causar graves problemas9. Si se intenta liberar dos veces
un mismo puntero, los resultados pueden ser catastróficos. Estas dos circunstancias deben ser controladas por el
programador, ya que el compilador no las detecta.

Se puede liberar un puntero nulo sin consecuencias adversas.

El uso de delete con los tipos preestablecidos no tiene misterios, salvo en el caso de las matrices, que se debe
usar la siguiente sintaxis:
                               delete [tamaño] <nombre_puntero>;

En la mayoría de los compiladores basta con poner los corchetes vacíos, ya que ignoran el número que se ponga
entre ellos.
Un sencillo ejemplo del uso del operador unario delete puede ser el siguiente:
// Programa: Uso de delete
// Fichero: DELETE1.CPP
#include <iostream.h>
#include <new.h>
void main (void)
{
      int *i, *vi;
         i=new int;
         *i=6;
         vi= new int[2];
         vi[0]=0; vi[1]=1;
         cout << *i << " " << vi[0] << " " << vi[1] << "n";
         delete i;             // Elimina el objeto
         delete [] vi;         // Libera una array de objetos
}



9
  Si se intenta liberar la memoria que se ha reservado mediante new para una instancia de una clase mediante
free, se está perdiendo la llamada al destructor como consecuencia inmediata.

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                        (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                                 -9-



Sobrecarga de new y de delete
En C++ es posible redefinir los operadores new y delete sobrecargándolos, si se quiere construir un
procedimiento de manejo de memoria personalizado.
Los esqueletos de las funciones que sobrecargan estos operadores son:

                     void *operator new (size_t tamaño)
                     {
                           // Reserva dinámica
                           return puntero_void;
                     }

                     void operator delete (void puntero)
                     {
                           // Liberar memoria a la que apunta el puntero
                     }

El operador new toma como argumento un tipo size_t que es un tipo entero que puede contener el mayor bloque
individual de memoria que se pueda reservar. El número de bytes a reservar se indican mediante el parámetro
tamaño. Además cualquier operador new que se cree debe devolver un puntero void.

En cuanto al operador delete toma como argumento un puntero de tipo void, el cual apunta a la zona de
memoria que debe liberar. Además cualquier operador delete que sea creado por el programador no debe
retornar nada.

Si se redefine el operador new de esta forma se sigue llamando al constructor de la clase cuando se tiene una
instancia dinámica de una clase.

Sin embargo, puede surgir algún tipo de problemas, cuando se sobrecargan estos operadores, con el uso de
realloc debido a que el nuevo sistema de manejo de memoria dinámica utilice las mismas técnicas de manejo de
memoria que las funciones de biblioteca del C.

Para ilustrar la sobrecarga de los operadores new y delete se va ha realizar un sencillo programa que inicie el
contenido del bloque de memoria que se quiere reservar a 0 antes de utilizarlo10.

// Programa: Sobrecarga de los operadores new y delete
// Fichero: N&DOVER1.CPP

#include <iostream.h>
#include <string.h>
#include <stdlib.h>
void *operator new ( size_t s ) {
      void *ptr=calloc(1, s);
      return ptr;
}
void operator delete ( void * ptr ) {
      free ( ptr );
}


10
     Se utilizará la función calloc porque ésta inicia a 0 todo el bloque de memoria que reserva.

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                             (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                       - 10 -




Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca   (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                              - 11 -


void main(void) {
      float *p=new float[5];
      for (register int i=0; i<5; cout << " " << p[i++]);
      delete [] p;
}

También se puede redefinir el operador new de forma que tome parámetros adicionales, especificando los
argumentos entre paréntesis.
Para ilustrar esta característica se va a modificar el programa anterior, de forma que en lugar de rellenar el
espacio en memoria con ceros, se inicie con un carácter determinado.

// Programa: Sobrecarga de los operadores new y delete
// Fichero: N&DOVER2.CPP

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

void *operator new ( size_t s, int relleno )
{
   void *ptr;
   if ( (ptr = malloc(s) )!=NULL)
       memset(ptr, relleno, s);
   else
       exit(1);

    return ptr;
}

void operator delete ( void * ptr )
{
   free ( ptr );
}

void main(void)
{
   char *p=new ('/') char[40];
   for (register int i=0; i<40; cout                      << p[i++] << " ");
   delete [] p;
}




Sobrecarga de new y delete respecto a una clase específica
Esta característica va a permitir personalizar el manejo de memoria dinámica para una clase individual.

Cuando se sobrecargan estos operadores con respecto a una clase en concreto, implica que cuando se utilicen
estos operadores con cualquier otro tipo de datos se van a emplear los operadores new y delete originales, o
aquellos que se hayan sobrecargado de forma global.

Para sobrecargar estos operadores para una clase se tienen que declarar dos funciones miembro que tengan por
nombre operator new y operator delete. Estos operadores tienen precedencia sobre los operadores new y
delete globales.

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                         (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                               - 12 -


Cuando se va a procesar el operador new para una instancia de una clase, el compilador primero comprueba si
están definidos para dicha clase, y en caso afirmativo utiliza las versiones específicas de la clase. En caso
contrario, si han sido sobrecargados globalmente, emplea estas nuevas versiones globales, y en último caso
utilizaría los operadores estándar new y delete.

Para ilustrar la sobrecarga de estos operadores respecto de una clase, se presenta el siguiente ejemplo:

// Programa: Sobrecarga de los operadores new y delete respecto de una
//           clase
// Fichero: N&DOVER3.CPP
#include <iostream.h>
#include <string.h>
#include <stddef.h>
#define NOMBRES 10
class Nombre {
   char nombre[25];
public:
   Nombre ( const char * );
   void Presenta ( void ) const;
   void *operator new (size_t s);
   void operator delete (void *ptr);
   ~Nombre() {}; // Destructor que no hace nada
};
// Memoria para manejar un número fijo de instancias de la clase Nombre
char memo[NOMBRES] [sizeof (Nombre)];
int en_uso[NOMBRES];
// Funciones miembro de la clase Nombre
Nombre::Nombre ( const char *cad )
{
   strncpy (nombre, cad, 25 );
}
void Nombre::Presenta ( void ) const
{
   cout << "n" << nombre << "n";
}
// Sobrecarga de los operadores new y delete para la clase Nombre
void * Nombre::operator new ( size_t s )
{
   for (int p=0; p < NOMBRES; p++) {
      if ( !en_uso[p] ) {
          en_uso[p] = 1;
          return memo + p;
      }
   }
    return 0;
}




Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                           (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                              - 13 -


void Nombre::operator delete ( void * ptr )
{
   en_uso[ ( (char *)ptr - memo[0] ) / sizeof ( Nombre ) ] = 0;
}

void main ( void )
{
   Nombre * directorio [NOMBRES];
   char nombre[25];

    for (register int i=0; i<NOMBRES; i++) {
       cout << "Introduce el nombre #" << i+1 << ": ";
       cin >> nombre;
       directorio[i] = new Nombre ( nombre );
    }

    for (i=0; i<NOMBRES; i++) {
       directorio[i] -> Presenta();
       delete directorio[i];
    }

}

Notas:
   Este programa aprovecha la característica de conocer que no habrá nunca más de un número pequeño de
   instancias de clases al mismo tiempo, pero que se están alojando y desalojando de forma continua. Así se
   crean unas versiones específicas de new y delete, que trabajan mucho más rápido que la versión global.
   Se crea una matriz global que pueda contener todas las instancias de la clase (memo), y se hace que los
   operadores new y delete manejen dicha matriz.
   Se cuenta también con un vector de enteros de tantas entradas como filas tiene la matriz de instancias,
   llamado en_uso, y que va a servir como banderas, para indicar si la correspondiente entrada en la matriz
   memo está en uso o no.
   Cuando se ejecuta la sentencia directorio[i]=new Nombre( nombre ), el compilador invoca al operador
   new propio de la clase. Este encuentra un entrada libre en la matriz memo, y retorna su dirección. Después
   de esto el compilador llama al constructor de la clase, el cual usa el espacio en la matriz, y lo inicia con una
   cadena de caracteres. Finalmente, se asigna a la entrada de la matriz directorio un puntero al objeto recién
   creado.
   Por su parte, cuando se ejecuta la sentencia delete directorio[i], el compilador llama al destructor, que en
   este ejemplo no hace nada. Después de lo cual se invoca al operador delete propio de la clase. El operador
   delete encuentra la situación del objeto dentro de la matriz memo y lo marca como libre, con lo que el
   espacio está de nuevo preparado para alojar otra cadena.
   De lo aquí explicado se concluye que el operador new se llama antes que el constructor, y el operador
   delete se llama después del destructor.




Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                          (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                         - 14 -



Clases con punteros miembros
Una práctica habitual es utilizar los operadores new y delete en las funciones miembros de una clase, como ya
se ha visto en varios de los ejemplos que hasta ahora se han presentado.

Pero en este apartado se va a estudiar con más profundidad esta posibilidad, comentando ciertos problemas que
pueden surgir, y la forma de solventarlos.

Para ello se va a crear una clase Cadena para el manejo de cadenas de caracteres, que el lector puede
personalizar y ampliar como ejercicio práctico de programación.

CADENA.H

#ifndef __CADENA_H__
#define __CADENA_H__

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

class Cadena {
      char *cadena;
      unsigned longitud;
public:
      Cadena(void);
      Cadena(const char *);
      Cadena(char, unsigned);
      void CambiaCaracter(int, char);
      char MuestraCaracter(int) const;
      int Longitud(void) const {return longitud;}
      void Muestra(void) const {cout << cadena;}
      void Agnadir(const char *);
      ~Cadena();
};

#endif


CADENA.CPP

// Programa: Clases con punteros miembros. Clase Cadena
//           Fichero Principal.
// Fichero: CADENA.CPP

#include "cadena.h"

// Constructores

Cadena::Cadena()
{
      cadena=0;               // Iniciamos puntero a "null" en C++
      longitud=0;
}



Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                     (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                       - 15 -


Cadena::Cadena(const char *cad)
{
      longitud=strlen(cad);
      cadena=new char[longitud+1];
      strcpy(cadena, cad);
}

Cadena::Cadena(char c, unsigned n)
{
      longitud=n;
      cadena=new char[n+1];
      memset(cadena, c, n);
      cadena[n]='0';
}

// Funciones miembro de la clase Cadena

void Cadena::CambiaCaracter(int indice, char c)
{
      if ( (indice >= 0) && (indice<longitud) )
            cadena[indice]=c;
}

char Cadena::MuestraCaracter(int indice) const
{
      if ( (indice >= 0) && (indice<longitud) )
            return cadena[indice];

         return 0;
}

void Cadena::Agnadir(const char *cad)
{
      char *tmp;

         longitud += strlen(cad);
         tmp=new char[longitud+1];
         strcpy(tmp, cadena);
         strcat(tmp, cad);
         delete [] cadena;
         cadena=tmp;
}


// Destructor
Cadena::~Cadena()
{
      delete [] cadena;
}




Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca   (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                            - 16 -


CADDEMO1.CPP

// Programa: Clases con punteros miembros. Clase Cadena
//           Fichero Principal.
// Fichero: CADDEMO1.CPP

#include "cadena.h"

void main(void)
{
      Cadena Micadena ("esta es mi cadena.");

         Micadena.Muestra();
         cout << "n";

         Micadena.CambiaCaracter(0, 'E');
         Micadena.Muestra();
         cout << "n";

         Micadena.Agnadir(" OK !!!!!");
         Micadena.Muestra();
         cout << "n";
}

Para almacenar las cadenas nunca es lo más apropiado utilizar matrices, debido a que en principio se desconoce
las longitudes que éstas pueden tener. En su lugar, tal y como se ha hecho en el ejemplo, se puede tener un
puntero a carácter como miembro, y de forma dinámica reservar la cantidad exacta de memoria que necesite
cada instancia de la clase.

El constructor que recibe un puntero a carácter utiliza el operador new para reservar la memoria necesaria para
almacenar la cadena. Después copia el contenido de la cadena en el espacio reservado. Como resultado de todo
esto se tiene que cada objeto Cadena no es un bloque contiguo de memoria, sino que consiste en dos bloques de
memoria: El primero contiene la longitud y un puntero a la cadena, y el otro almacena la cadena de caracteres
en sí.11

La clase Cadena es un típico ejemplo que necesita un destructor, ya que cuando se destruye un objeto por salir
del alcance en el que definido, se libera el espacio de memoria que ocupa el primer bloque de forma automática,
pero el espacio que se reservó con new, debe ser liberado de forma explícita. De lo contrario el espacio con las
cadenas de caracteres no sería nunca liberado, y el programa podría quedarse sin memoria en cualquier
momento.

Pero este ejemplo presenta un posible problema. Supongamos que utilizamos la clase Cadena con el siguiente
programa principal:

// Programa: Clases con punteros miembros. Clase Cadena
//           Fichero Principal.
// Fichero: CADDEMO2.CPP



11
  Si se utilizará sizeof para calcular el tamaño de un objeto Cadena, sólo se conseguiría en tamaño del bloque
que contiene el entero y el puntero. Sin embargo, las diferentes instancias de la clase Cadena pueden tener
cadenas de caracteres de diferente tamaño.

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                        (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                       - 17 -


#include "cadena.h"
#include <conio.h>

void main(void)
{
   Cadena   c1("Esta es la primera cadena"),
   c2("Esta es otra cadena");
    clrscr();
    cout << "nLa cadena c1 es: ";
    c1.Muestra();
    cout << "nLongitud de c1 es: " << c1.Longitud() << "n";
    cout << "nLa cadena c2 es: ";
    c2.Muestra();
    cout << "nLongitud de c2 es: " << c2.Longitud() << "n";
// Asignamos la primera cadena a la segunda, y mostramos
// ambas cadenas, y sus longitudes
   c2=c1;
   cout << "nLa cadena c1 es: ";
   c1.Muestra();
   cout << "nLongitud de c1 es: " << c1.Longitud() << "n";
   cout << "nLa cadena c2 es: ";
   c2.Muestra();
   cout << "nLongitud de c2 es: " << c2.Longitud() << "n";
// Ambas tienen el mismo contenido y la misma longitud
// como era de esperar
// Ahora cambiamos algo al objeto c1 y repetimos la
// operación de mostrar ambos objetos
   c1.CambiaCaracter(1, 'S');
   cout << "nLa cadena c1 es: ";
   c1.Muestra();
   cout << "nLongitud de c1 es: " << c1.Longitud() << "n";
   cout << "nLa cadena c2 es: ";
   c2.Muestra();
   cout << "nLongitud de c2 es: " << c2.Longitud() << "n";
// Pero que pasa si yo he cambiado sólo la cadena del objeto c1,
// y se han modificado los dos objetos ((( Qué pasaaaaa !!!
// Ahora añadimos algo al objeto c1 y repetimos la
// operación de mostrar ambos objetos
   c1.Agnadir(" :-)))");
   cout << "nLa cadena c1 es: ";
   c1.Muestra();
   cout << "nLongitud de c1 es: " << c1.Longitud() << "n";
   cout << "nLa cadena c2 es: ";
   c2.Muestra();
   cout << "nLongitud de c2 es: " << c2.Longitud() << "n";
// (((( Esto es una ruina !!!! La cadena del objeto c1 ha sido
// modificada correctamente, pero la del objeto c2 se ha ido
// a la ...
}


Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca   (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                       - 18 -




Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca   (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                          - 19 -


En el ejemplo se han creado dos objetos c1 y c2 de la clase Cadena. Para después hacer una asignación de c1 en
c2. Cuando se realiza una asignación de un objeto a otro, el compilador realizaría lo siguiente:

                                    c2.longitud = c1.longitud;
                                    c2.cadena = c1.cadena;

En la asignación del miembro longitud no hay ningún problema. sin embargo, como el miembro cadena es un
puntero, el resultado de la asignación es que c1.cadena y c2.cadena apuntan a la misma zona de memoria. Es
decir, ambos objetos comparten la cadena de caracteres.

De forma gráfica:




Aquí esta la explicación de los efectos no deseados que encontrábamos en el programa CADDEMO2.CPP.



Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                      (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                             - 20 -


Otros problemas más graves se pueden llegar a dar cuando el objeto sale del alcance donde fue definido. Cuando
el destructor de la clase se llama para el objeto c1, se elimina dicho objeto liberándose también la memoria
donde apuntaba el puntero cadena. Cuando el destructor se llama de nuevo para el objeto c2, se eliminará el
puntero miembro cadena. Pero como los dos miembros cadena de ambos objetos tienen el mismo valor,
significa que un puntero ha sido eliminado dos veces, esto puede tener consecuencias imprevistas.

Además, el contenido original del miembro cadena del objeto c2 se ha perdido, y ese bloque de memoria no será
liberado.

Este problema ocurre con cualquier clase que contiene punteros miembro y reserva memoria del
almacenamiento libre.

En conclusión el operador de asignación que nos ofrece por defecto el compilador, no es adecuado para usarlo
entre clases. La solución será reemplazar el que se nos ofrece por defecto por uno escrito por nosotros mismos, y
que realmente realice la asignación de forma adecuada.

Como ya es sabido, una de las características del C++ es que se permite la sobrecarga de operadores12. Lo que se
va a hacer es dotar al operador de asignación de un significado especial cuando se refiera a la clase Cadena.

Para redefinir el operador de asignación se debe crear una función miembro que se llame operator=. De esta
forma cuando se vaya a realizar una asignación entre objetos de la clase Cadena, el compilador utilizará el
operador de asignación escrito para dicha clase.

El nuevo operador de asignación puede ser algo como:

// Operador de asignación

void Cadena::operator= (const Cadena &fuente)
{
   longitud = fuente.longitud;
   delete [ ] cadena;
   cadena = new char[longitud + 1];
   strcpy ( cadena, fuente.cadena);
}




         12
              Que se estudiará con detalle más adelante

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                         (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                             - 21 -


El operador recibe como parámetro una referencia como constante13 a un objeto Cadena. El resultado ahora es
correcto, y se ilustra en la siguiente figura:




Una vez modificado el fichero cabecera, y el fichero cuerpo de la clase, compilar y ejecutar el programa
CADDEMO2.CPP.

Pero que ocurriría si se realiza una auto asignación c1 = c1; Esto que parece absurdo, y que casi nadie va a
utilizar tiene otras formas más comunes:

                                    Cadena *Ptr_cad = &c1;
                                    // Más adelante ...
                                    c1 = *Ptr_cad;

Lo que ocurrirá es que en la definición del nuevo operador de asignación, primero borra la cadena de caracteres,
y después reserva un nuevo espacio, donde copia el contenido del espacio recién reservado en él mismo. Esto
tiene como resultado que se introduce basura, y la pérdida de la cadena de caracteres del objeto. Para que el
operador trabaje bien en todos los casos, se debe utilizar el puntero this14 para prever el caso de la auto
asignación.

Cuando una clase tiene punteros miembro, se debe tener en cuenta a la hora de trabajar con constructores copia.

Como se vio cuando se trató el tema de los constructores copia, estos iniciaban un objeto con los valores de otro
objeto. Aquí se tiene involucrado el uso del operador asignación. Pero como se ha visto en este mismo apartado,
el operador de asignación por defecto nos va a dar resultados erróneos si la clase tiene punteros miembro. Así si
se hiciera lo siguiente:
                                    Cadena c2(c1);

Se estaría iniciando el objeto c2 con los valores del objeto c1, pero al utilizar el operador de asignación por
defecto, lo que se estaría haciendo es que tanto c1 como c2 apuntarán a la misma cadena de caracteres.

13
  Indicando que dicho operador no puede modificar el objeto que recibe
14
   El puntero this se estudia en el siguiente apartado. Por lo que respecta a la correcta manera de crear el
operador de asignación para la clase Cadena, realizar el ejercicio 3 de este mismo capítulo.

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                         (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                            - 22 -


La solución pasa por construir un constructor copia propio. Como ejemplo se va a escribir el constructor copia
para la clase Cadena.

// Constructor Copia
Cadena::Cadena ( const Cadena &fuente)
{
   longitud = fuente.longitud;
   cadena = new char [longitud +1];
   strcpy (cadena, fuente.cadena);
}

La realización del constructor copia es muy similar a la realización del operador asignación. Pero hay algunas
diferencias entre ellos:
     Un operador de asignación actúa sobre un objeto que existe, mientras que un constructor copia crea uno
     nuevo. Como resultado, un operador de asignación puede tener que borrar la memoria que se reservó
     originalmente para el objeto que recibe los datos.
     Un operador de asignación tiene que comprobar las auto asignaciones. El constructor copia no tiene que
     hacerlo, porque en su caso no existen auto asignaciones.
     Para permitir asignaciones encadenadas, un operador de asignación debe retornar *this. Por ser un
     constructor, el constructor copia no puede devolver valores.




El Puntero this
       Cada vez que se invoca a una función miembro, se le pasa automáticamente un puntero al objeto que la ha
       invocado
       Se puede acceder a ese puntero utilizando la palabra reservada this
       El puntero this es un parámetro implícito a toda función miembro15
       Para acceder a un elemento concreto de un objeto cuando se utiliza el objeto en sí, se emplea el operador
       punto (.)
       Para acceder a un elemento concreto de un objeto cuando se utiliza un puntero al objeto, se emplea el
       operador flecha (->)

Cuando se llama a una función miembro, el compilador asigna la dirección del objeto al puntero this, y después
se llama a la función. Cada vez que una función miembro accede a un dato miembro, se está utilizando de forma
implícita el puntero this.

Por ejemplo considérese el siguiente programa en C++:

#include <iostream.h>

class D {
   int i,j,k;
public:
   D() {i=j=k=0;}




15
     Excepto para las funciones miembro static

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                        (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                             - 23 -


void Mostrar(void)
  {
     cout << "n" << i << " " << j << " " << k;
  }
} A;


void main ()
{
   A.Mostrar();
}


Sería equivalente a este otro:

#include <iostream.h>

class D {
   int i,j,k;
public:
   D() {this->i=this->j=this->k=0;}

void Mostrar(void)
  {
     cout   << "n" << this->i << " " << this->j << " " << this->k;
  }
} A;


void main ()
{
   A.Mostrar();
}

A la vista del ejemplo se puede deducir que es lícito el uso del puntero this cuando se accede a los datos
miembro, aunque es del todo innecesario. También es correcto utilizar la expresión *this para referirse al objeto
que ha llamado a la función miembro.

Así las tres sentencias que aparecen en la siguiente función son equivalentes:

void Mostrar(void) {
   cout << "n" << i << " " << j << " " << k;
   cout << "n" << this->i << " " << this->j << " " << this->k;
   cout << "n" << (*this).i << " " << (*this).j << " " << (*this).k;
}

Otro de los usos del puntero this es comprobar si el objeto que se pasa como parámetro a una función miembro
es el mismo objeto que llamó a dicha función. Esto es fundamental para las auto asignaciones. Lo que se hace es
comprobar que la dirección del objeto que se pasa como argumento es diferente del valor del puntero this, si
esto es así se puede hacer la asignación sin problemas. Sino se sale de la función sin hacer nada.

Si se utiliza *this como valor de retorno de una función, se puede lograr crear operadores que sean asociativos
bien por la derecha, como es el caso de la asignación, bien por la izquierda, como ocurre en las sentencias cout
de este tipo cout << a << b << c;

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                         (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                               - 24 -


El puntero this es un puntero constante, por lo tanto una función miembro no puede cambiar el valor del
puntero, y hacer que apunte a otro sitio. Sin embargo, en las primeras versiones de C++, el puntero this no era
constante, y esto permitía al programador hacer asignaciones al puntero this para personalizar el manejo de la
memoria dinámica. Esto ya no está permitido en las últimas versiones de C++.



Funciones estáticas y el puntero this
Cuando en el capítulo anterior se abordó el tema de las funciones miembro static, se definieron como aquellas
que sólo podían trabajar con miembros dato estáticos. Esto es debido a que este tipo de funciones carecen de
puntero this.

Pero el problema surge si una función necesita de forma imperiosa acceder a un miembro dato de una clase. Para
solucionarlo se le ha de pasar de forma explícita un puntero this. Véase el siguiente ejemplo:

// Programa: Funciones miembro estáticas y el puntero this
// Fichero: STTHIS.CPP

#include <iostream.h>
class Prueba {
   int a, b;
   static int decre;
public:
   Prueba () {a=b=0;}
   Prueba (int a1, int b1=0) {a=a1; b=b1;}
   static int Ejemplo(Prueba *E) {return E->a+E->b-decre;}
};

int Prueba::decre=1;

void main (void)
{
   Prueba P1(1,2);
   cout << Prueba::Ejemplo (&P1) << "n";
}




Paso y retorno de objetos
Hay otras dos situaciones, a parte de en las definiciones, en las que un constructor copia se llama:
             Cuando una función recibe un objeto como parámetro.
             Cuando una función retorna un objeto.

Supongamos un extracto de programa en C++ en el que se muestra una función que recibe un objeto Cadena
como argumento.
// Función que recibe un objeto Cadena como argumento
void ProcesaCadena ( Cadena cad )
{
   // Usa el objeto cad
}

Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                           (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                             - 25 -



void main ()
{
   Cadena c1("Cadena de ejemplo");
   ProcesaCadena ( c1);
}

La función ProcesaCadena recibe un objeto que se pasa por valor. Esto significa que la función tiene su propia
copia privada del objeto.

El parámetro de la función se inicia con el objeto que se pasa como argumento. El compilador de forma implícita
llama al constructor copia para realizar esta iniciación.

Si no se define un constructor copia para realizar la iniciación, el compilador realiza su constructor copia por
defecto, y el objeto cad y el objeto c1 tendrían la misma cadena de caracteres, y por lo tanto cualquier
modificación en la cadena de caracteres de cad modificaría la cadena de caracteres del objeto c1.

También habría problemas en cuanto que el objeto cad tiene un alcance local, y por lo tanto al salir de la función
se llamaría al destructor. Esto significa que el objeto c1 tendría un puntero que apuntaría a una zona de memoria
que ya ha sido liberada, lo cual iba a ser poco recomendable.

Ahora supongamos un extracto de fuente en C++ donde se incluye una función que retorna un objeto Cadena.

// Función que retorna un objeto Cadena
Cadena RetCadena ( void )
{
   Cadena valor ( "Esto es una prueba.");
   return valor;
}

void main ()
{
   Cadena c1;
   c1 = RetCadena();
}

La función RetCadena retorna un objeto Cadena. El compilador llama al constructor copia para iniciar un
objeto temporal y oculto en el alcance de la llamada, usando el objeto que se especifica en la sentencia return.
Este objeto temporal es usado como parte derecha de la asignación que hay en el programa principal.

De nuevo, se necesita un constructor copia. En otro caso el objeto temporal compartiría la misma cadena de
caracteres que el objeto valor, el cual será eliminado cuando la función RetCadena finalice su ejecución, y en
consecuencia la asignación que se hace a c1 no está garantizada.

Como regla a seguir, se debe definir siempre un constructor copia y un operador de asignación cuando se
tenga una clase que contenga punteros miembro y reserve espacio del almacenamiento libre.




Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                         (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                             - 26 -



Paso y retorno de referencias a objetos
En el apartado anterior se ha visto como cada vez que se pasaba un objeto a una función por valor, se llamaba al
constructor copia.

Una forma de simular el paso por valor de un objeto, pero sin tener que llamar al constructor copia sería utilizar
referencias a objetos constantes.

Estudiemos el mismo ejemplo que se vio en el apartado anterior, pero ahora la función recibe una referencia al
objeto constante.

// Función que recibe un objeto Cadena como argumento
void ProcesaCadena ( const Cadena &cad )
{
  // Usa el objeto cad
}

void main ()
{
   Cadena c1("Cadena de ejemplo");
   ProcesaCadena ( c1);
}

Ahora el constructor copia no es llamado porque no se ha construido un nuevo objeto. En su lugar se inicia una
referencia con el objeto que se le pasa. Como resultado se usa el mismo objeto que en la llamada a la función.

El uso de la palabra reservada const es debido a que se quiere asegurar la integridad del objeto, esto es, que no
se pueda modificar el objeto en la función.

Igualmente que una función retorne una referencia en lugar de un objeto es mucho más eficiente. El constructor
copia no se llama cuando se produce el valor de retorno, porque no se crea un objeto temporal, sólo se crea una
referencia temporal.




Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                         (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                           - 27 -



Ejercicios propuestos
1.   Indicar cuál de las siguientes frases es incorrecta y explicar los motivos
     a) La función delete sirve para liberar zonas de memoria que han sido reservadas mediante new.
     b) El uso de malloc está prohibido en C++
     c) La función set_new_handler sirve para que una función determinada se encargue del manejo de los
          errores por falta de espacio en el manejo de memoria dinámica mediante el operador new.
     d) Cuando se crea un objeto con el operador new no se llama al constructor de la clase a menos que se
          indique de forma explícita mediante argumentos.
     e) Aunque no se debe, si se usa free para liberar un puntero a una instancia devuelto por new, se llama al
          destructor si existe.


2. Reescribir la función cambia() del programa MALVNEW.CPP, reemplazando la función realloc por una
simulación de ésta mediante el operador new.


3. Tenemos la clase Cadena.
class Cadena {
         char *cadena;
         unsigned longitud;
public:
         Cadena(void);
         Cadena(const char *);
         Cadena(char, unsigned);
         void CambiaCaracter(int, char);
         char MuestraCaracter(int) const;
         int Longitud(void) const {return longitud;}
         void Muestra(void) const {cout << cadena;}
         void Agnadir(const char *);
         ~Cadena();
};
Se ha redefinido el operador de asignación para dicha clase.

// Operador de asignación
void Cadena::operator= (const Cadena &fuente)
{
   longitud = fuente.longitud;
   delete [] cadena;
   cadena = new char[longitud + 1];
   strcpy ( cadena, fuente.cadena);
}

Escribir de nuevo este operador de asignación de la clase Cadena, de forma que se admita la “auto asignación”:

                  c1 = c1       o   Cadena *ptr_cad = &c1;




Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                       (versión Febrero 2003)
Programación Orientada a Objetos
3 – Clases manejo de memoria dinámica en C++                                                          - 28 -



4. Modificar de nuevo el operador de asignación para que ahora también permita:   c1=c2=c3;


5. Un constructor copia tiene como argumento una referencia a un objeto en lugar de un objeto. ¿Por qué?

6.  Crear una clase apropiada para representar el tipo de dato número complejo. Un número complejo se
representará como (a, b), donde a representa la parte real y b la imaginaria.

Construir además las funciones apropiadas para:
        Asignar valor a un número complejo
        Imprimir un número complejo con el formato (a, b)
        Sumar dos números complejos
        Restar dos números complejos
        Multiplicar dos números complejos

Construir, además, un pequeño programa principal que compruebe el comportamiento de esta clase.


7. Crear una clase apropiada para representar el tipo de dato rectángulo.
Construir además las funciones apropiadas para:
        Calcular el área de un rectángulo
        Calcular el perímetro de un rectángulo
        Dados dos rectángulos, determinar cual es el mayor, teniendo en cuenta que el mayor es aquel que tiene
        mayor área.
        Dados dos rectángulos, determinar si sin idénticos. Ser idénticos implica que tiene el mismo área y el
        mismo perímetro.
        Intercambiar los valores entre dos rectángulos
        Ordenar un vector de rectángulos, de mayor a menor


8. Incluir a la clase Cadena definida en este documento los siguientes métodos:
         Invertir una cadena
         Pasar a mayúsculas
         Determinar si es un palíndromo

Construir además una función que permita ordenar de mayor a menor un vector de Cadenas.




Ingeniería Técnica en Informática de Sistemas (3er curso)
Departamento de Informática y Automática – Universidad de Salamanca                      (versión Febrero 2003)

Mais conteúdo relacionado

Mais procurados

Bibliotecas o librerias_para_c_
Bibliotecas o librerias_para_c_Bibliotecas o librerias_para_c_
Bibliotecas o librerias_para_c_Oziel Solis Juarez
 
Python científico (introducción a numpy y matplotlib))
Python científico (introducción a numpy y matplotlib))Python científico (introducción a numpy y matplotlib))
Python científico (introducción a numpy y matplotlib))kikocorreoso
 
Manual Basico para Encantadores de Serpientes (Python)
Manual Basico para Encantadores de Serpientes (Python)Manual Basico para Encantadores de Serpientes (Python)
Manual Basico para Encantadores de Serpientes (Python)Fco Javier Lucena
 
07 - Tipos de datos definidos por el programador en lenguaje C: struct, typed...
07 - Tipos de datos definidos por el programador en lenguaje C: struct, typed...07 - Tipos de datos definidos por el programador en lenguaje C: struct, typed...
07 - Tipos de datos definidos por el programador en lenguaje C: struct, typed...Diego Andrés Alvarez Marín
 
Sesión 2: Ejemplos y prácticas en Python
Sesión 2: Ejemplos y prácticas en PythonSesión 2: Ejemplos y prácticas en Python
Sesión 2: Ejemplos y prácticas en Pythonmaluacsa
 
Lenguaje C para Administradores de Red - Script II Punteros
Lenguaje C para Administradores de Red - Script II PunterosLenguaje C para Administradores de Red - Script II Punteros
Lenguaje C para Administradores de Red - Script II Punterossirfids
 
Memoria dinamica
Memoria dinamicaMemoria dinamica
Memoria dinamicagusolis93
 
Tema 4 - Tipos datos avanzados (III)
Tema 4 - Tipos datos avanzados (III)Tema 4 - Tipos datos avanzados (III)
Tema 4 - Tipos datos avanzados (III)Pablo Haya
 

Mais procurados (20)

Clase4_Python-CTIC
Clase4_Python-CTICClase4_Python-CTIC
Clase4_Python-CTIC
 
Bibliotecas o librerias_para_c_
Bibliotecas o librerias_para_c_Bibliotecas o librerias_para_c_
Bibliotecas o librerias_para_c_
 
Python científico (introducción a numpy y matplotlib))
Python científico (introducción a numpy y matplotlib))Python científico (introducción a numpy y matplotlib))
Python científico (introducción a numpy y matplotlib))
 
4 memoria dinamica
4 memoria dinamica4 memoria dinamica
4 memoria dinamica
 
Manual Basico para Encantadores de Serpientes (Python)
Manual Basico para Encantadores de Serpientes (Python)Manual Basico para Encantadores de Serpientes (Python)
Manual Basico para Encantadores de Serpientes (Python)
 
07 - Tipos de datos definidos por el programador en lenguaje C: struct, typed...
07 - Tipos de datos definidos por el programador en lenguaje C: struct, typed...07 - Tipos de datos definidos por el programador en lenguaje C: struct, typed...
07 - Tipos de datos definidos por el programador en lenguaje C: struct, typed...
 
Viernes Tecnicos DTrace
Viernes Tecnicos DTraceViernes Tecnicos DTrace
Viernes Tecnicos DTrace
 
LibreríAs De Java
LibreríAs De JavaLibreríAs De Java
LibreríAs De Java
 
Sesión 2: Ejemplos y prácticas en Python
Sesión 2: Ejemplos y prácticas en PythonSesión 2: Ejemplos y prácticas en Python
Sesión 2: Ejemplos y prácticas en Python
 
Python + Ciencia = ♥
Python + Ciencia = ♥Python + Ciencia = ♥
Python + Ciencia = ♥
 
Estructuras lineales
Estructuras linealesEstructuras lineales
Estructuras lineales
 
Matriz Vector
Matriz VectorMatriz Vector
Matriz Vector
 
Lenguaje C para Administradores de Red - Script II Punteros
Lenguaje C para Administradores de Red - Script II PunterosLenguaje C para Administradores de Red - Script II Punteros
Lenguaje C para Administradores de Red - Script II Punteros
 
Memoria memoria dinamica
 Memoria memoria dinamica Memoria memoria dinamica
Memoria memoria dinamica
 
Memoria dinamica
Memoria dinamicaMemoria dinamica
Memoria dinamica
 
Éxito y Fracáso
Éxito y FracásoÉxito y Fracáso
Éxito y Fracáso
 
Tema 4 - Tipos datos avanzados (III)
Tema 4 - Tipos datos avanzados (III)Tema 4 - Tipos datos avanzados (III)
Tema 4 - Tipos datos avanzados (III)
 
C++
C++C++
C++
 
Programación en c++
Programación en c++Programación en c++
Programación en c++
 
Guia final so
Guia final soGuia final so
Guia final so
 

Semelhante a Manejo de la memoria

Reserva y liberación de memoria
Reserva y liberación de memoriaReserva y liberación de memoria
Reserva y liberación de memoriaJose Telleria
 
19189723 estructura-de-datos-programacion-facil
19189723 estructura-de-datos-programacion-facil19189723 estructura-de-datos-programacion-facil
19189723 estructura-de-datos-programacion-facilDariana Acuariogv
 
ARQUITECTURA - JERARQUIA DE MEMORIAS
ARQUITECTURA - JERARQUIA DE MEMORIASARQUITECTURA - JERARQUIA DE MEMORIAS
ARQUITECTURA - JERARQUIA DE MEMORIASNoralma Yanez
 
Creacion de shellcodes para Exploits en Linux/x86
Creacion de shellcodes para Exploits en Linux/x86 Creacion de shellcodes para Exploits en Linux/x86
Creacion de shellcodes para Exploits en Linux/x86 Internet Security Auditors
 
Teoria memorias cache
Teoria memorias cacheTeoria memorias cache
Teoria memorias cachecurrocordoba
 
Estructura de datos c++
Estructura de datos c++Estructura de datos c++
Estructura de datos c++kikeMerck
 
Capítulo 3 Algoritmos recursivos.pdf
Capítulo 3 Algoritmos recursivos.pdfCapítulo 3 Algoritmos recursivos.pdf
Capítulo 3 Algoritmos recursivos.pdfIgor Rodriguez
 
Programación en c++
Programación en c++Programación en c++
Programación en c++andermijan
 
Principios del Diseño Orientado a Objetos (OOD) Aplicados en C
Principios del Diseño Orientado a Objetos (OOD) Aplicados en CPrincipios del Diseño Orientado a Objetos (OOD) Aplicados en C
Principios del Diseño Orientado a Objetos (OOD) Aplicados en CLeandro Francucci
 
1.4.2 stack segment
1.4.2  stack  segment1.4.2  stack  segment
1.4.2 stack segmentgabo
 
1.4.2 pila stack segment
1.4.2 pila stack  segment1.4.2 pila stack  segment
1.4.2 pila stack segmentgabo
 
Administración de memoria y apuntadores
Administración de memoria y apuntadoresAdministración de memoria y apuntadores
Administración de memoria y apuntadoresFranklin Chavez
 
Jyoc java-cap10 clases complementarias y enumerados
Jyoc java-cap10 clases complementarias y enumeradosJyoc java-cap10 clases complementarias y enumerados
Jyoc java-cap10 clases complementarias y enumeradosJyoc X
 

Semelhante a Manejo de la memoria (20)

Cplus
CplusCplus
Cplus
 
Reserva y liberación de memoria
Reserva y liberación de memoriaReserva y liberación de memoria
Reserva y liberación de memoria
 
Tema 11
Tema 11Tema 11
Tema 11
 
19189723 estructura-de-datos-programacion-facil
19189723 estructura-de-datos-programacion-facil19189723 estructura-de-datos-programacion-facil
19189723 estructura-de-datos-programacion-facil
 
ARQUITECTURA - JERARQUIA DE MEMORIAS
ARQUITECTURA - JERARQUIA DE MEMORIASARQUITECTURA - JERARQUIA DE MEMORIAS
ARQUITECTURA - JERARQUIA DE MEMORIAS
 
Creacion de shellcodes para Exploits en Linux/x86
Creacion de shellcodes para Exploits en Linux/x86 Creacion de shellcodes para Exploits en Linux/x86
Creacion de shellcodes para Exploits en Linux/x86
 
Jerarquia de memorias
Jerarquia de memoriasJerarquia de memorias
Jerarquia de memorias
 
Teoria memorias cache
Teoria memorias cacheTeoria memorias cache
Teoria memorias cache
 
INVESTIGACIÓN DE LIBRERÍAS
INVESTIGACIÓN DE LIBRERÍAS INVESTIGACIÓN DE LIBRERÍAS
INVESTIGACIÓN DE LIBRERÍAS
 
Manejo de memoria
Manejo de memoriaManejo de memoria
Manejo de memoria
 
Memoria dinamica
Memoria dinamicaMemoria dinamica
Memoria dinamica
 
Estructura de datos c++
Estructura de datos c++Estructura de datos c++
Estructura de datos c++
 
Capítulo 3 Algoritmos recursivos.pdf
Capítulo 3 Algoritmos recursivos.pdfCapítulo 3 Algoritmos recursivos.pdf
Capítulo 3 Algoritmos recursivos.pdf
 
Programación en c++
Programación en c++Programación en c++
Programación en c++
 
Principios del Diseño Orientado a Objetos (OOD) Aplicados en C
Principios del Diseño Orientado a Objetos (OOD) Aplicados en CPrincipios del Diseño Orientado a Objetos (OOD) Aplicados en C
Principios del Diseño Orientado a Objetos (OOD) Aplicados en C
 
05 - Funciones en lenguaje C
05 - Funciones en lenguaje C05 - Funciones en lenguaje C
05 - Funciones en lenguaje C
 
1.4.2 stack segment
1.4.2  stack  segment1.4.2  stack  segment
1.4.2 stack segment
 
1.4.2 pila stack segment
1.4.2 pila stack  segment1.4.2 pila stack  segment
1.4.2 pila stack segment
 
Administración de memoria y apuntadores
Administración de memoria y apuntadoresAdministración de memoria y apuntadores
Administración de memoria y apuntadores
 
Jyoc java-cap10 clases complementarias y enumerados
Jyoc java-cap10 clases complementarias y enumeradosJyoc java-cap10 clases complementarias y enumerados
Jyoc java-cap10 clases complementarias y enumerados
 

Manejo de la memoria

  • 1. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ -1- Clases y manejo de memoria dinámica en C++ Repaso a las funciones de manejo de memoria dinámica en C ............................................................................1 Función calloc()...................................................................................................................................................1 Función malloc() .................................................................................................................................................2 Función realloc() .................................................................................................................................................2 Función free() ......................................................................................................................................................3 El operador new frente a malloc ............................................................................................................................3 El operador delete frente a free ..............................................................................................................................8 Sobrecarga de new y de delete ................................................................................................................................9 Sobrecarga de new y delete respecto a una clase específica.............................................................................11 Clases con punteros miembros .............................................................................................................................14 El Puntero this ......................................................................................................................................................22 Funciones estáticas y el puntero this .................................................................................................................24 Paso y retorno de objetos ......................................................................................................................................24 Paso y retorno de referencias a objetos................................................................................................................26 Ejercicios propuestos ............................................................................................................................................27 Repaso a las funciones de manejo de memoria dinámica en C Función calloc() Prototipo: void *calloc(int num, int tam); Archivo cabecera: stdlib.h Valor devuelto: Devuelve un puntero al primer byte del bloque de memoria reservada, o un puntero NULL en el caso de no haberse podido reservar el bloque de memoria solicitado Finalidad: Reserva un bloque de memoria para almacenar num elementos de tam bytes cada uno de ellos. Todo el bloque de memoria queda iniciado a 0. • num: indica el número de elementos con la misma estructura que ocuparán el bloque de memoria reservado. • tam: indica el tamaño en bytes de cada uno de los elementos que van a ocupar el bloque de memoria reservada. La cantidad de memoria reservada viene determinada por el resultado que se obtiene al multiplicar el número de elementos a almacenar en el bloque de memoria por el tamaño en bytes de cada uno de esos elementos, es decir, num * tam. El uso de esta función se puede ilustrar con el siguiente ejemplo: int n=100, *pt; ... pt = ( int * ) calloc (n, sizeof(int)); En este ejemplo la función calloc reserva un bloque de memoria de n enteros, y retorna un puntero void a dicho bloque, o NULL si n fuera 0 o se produjera un error al no poder reservar suficiente espacio para reservar la Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 2. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ -2- memoria solicitada. Sobre el puntero que se retorna se debe hacer una operación de cast apropiada, para asignarlo al puntero del tipo que se está tratando. Función malloc() Prototipo: void *malloc(int tam); Archivo cabecera: stdlib.h Valor devuelto: Devuelve un puntero al primer byte del bloque de memoria reservada, o un puntero NULL en el caso de no haberse podido reservar el bloque de memoria solicitado Finalidad: Reserva un bloque de memoria de tam bytes. • tam: indica el tamaño en bytes del bloque de memoria que se desea reservar. La cantidad de memoria reservada será de tam bytes. El uso de esta función se puede ilustrar con el siguiente ejemplo: struct Registro *ptr_reg; ptr_reg = ( struct Registro * ) malloc (sizeof (struct Registro)); La función malloc reserva un bloque de memoria lo suficientemente grande como para acoger por completo al tipo de dato, y retorna un puntero void a dicho bloque, o NULL si el tamaño del argumento es 0 o se ha producido un error al no poder reservar suficiente espacio para alojar al tipo de dato. Sobre el puntero que se retorna se debe hacer una operación de cast apropiada, para asignarlo al puntero del tipo que se está tratando. Función realloc() Prototipo: void *realloc(void *ptr, int nuevo_tamaño); Archivo cabecera: stdlib.h Valor devuelto: Devuelve un puntero al primer byte del bloque de memoria reservada, o un puntero NULL en el caso de no haberse podido reservar el bloque de memoria solicitado Finalidad: Cambia el tamaño del bloque de memoria apuntada por ptr al nuevo tamaño indicado por nuevo_tamaño • ptr: puntero que apunta al bloque de memoria reservado. • nuevo_tamaño: valor en bytes que indica el nuevo tamaño del bloque de memoria apuntado por ptr y que puede ser mayor o menor que el original. En estas tres funciones es importante comprobar que el puntero devuelto por ellas no es un puntero nulo (NULL) antes de hacer uso del bloque de memoria reservado. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 3. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ -3- Función free() Prototipo: void free(void *ptr); Archivo cabecera: stdlib.h Valor devuelto: Ninguno Finalidad: Libera el bloque de memoria apuntada por ptr y que previamente ha sido asignado mediante malloc() o calloc(). • ptr: variable puntero que debe contener la dirección de memoria del primer byte del bloque de memoria que deseamos liberar. En caso de que no sea un puntero previamente reservado por malloc() o calloc(), la llamada a la función free() puede ocasionar una parada o interrupción del sistema. El operador new frente a malloc En el lenguaje C, la región de memoria que está disponible a la hora de la ejecución recibe el nombre de heap. En C++ el espacio de memoria disponible se conoce como almacenamiento libre1. La diferencia entre los dos se encuentra en las funciones que se utilicen para el acceder a esta memoria. El método para solicitar memoria del heap en C se basa en el uso de la función malloc y sus derivadas (calloc, realloc, ...). En C++ no es correcto utilizar malloc para alojar de forma dinámica una nueva instancia de una clase. La razón es que si se usa malloc, se pierden las ventajas que nos ofrecen los constructores, ya que se llama al constructor de la clase cada vez que se crea un objeto. Si se usa malloc para crear un objeto, se tiene un puntero a un bloque de memoria sin iniciar. Entonces se pueden hacer llamadas a métodos con objetos que no están bien construidos, y que por lo tanto van a dar resultados erróneos. La técnica más adecuada es utilizar la alternativa que ofrece C++ mediante el operador new. El operador new conoce la clase del objeto, o el tipo de la variable, que se quiere alojar en el almacenamiento libre. De esta forma este operador llama automáticamente al constructor que corresponda, dependiendo de los argumentos que se especifiquen, para iniciar la memoria que ha reservado. La forma de utilizar este operador es la siguiente: <nombre_puntero> = new <nombre> [<argumentos>]; De la sintaxis de new se puede deducir que no es una función, y que por lo tanto no tiene una lista de argumentos entre paréntesis. Es un operador que se aplica al nombre del tipo, pero no se tiene que realizar un cast 2 ni utilizar el operador sizeof para determinar su tamaño, porque new conoce el tipo de dato y su tamaño. Si new no encuentra espacio para alojar lo que se le pide, devuelve 03. El operador new reemplaza a las funciones malloc y calloc de la biblioteca estándar de C, pero no sustituye a realloc. La función realloc puede ser simulada, pero la aproximación que se haga supondrá siempre un aumento 1 Free store. 2 El compilador comprueba que el tipo del puntero que devuelve new y el tipo del puntero al que se asigna son del mismo tipo. En caso contrario genera un error. 3 En C++ un puntero nulo vale 0 en lugar de NULL. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 4. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ -4- de las líneas del código fuente, así como conocer el tamaño original del espacio reservado mediante el puntero que se va cambiar. Pero afortunadamente se puede utilizar generalmente realloc para cambiar el tamaño del espacio reservado con new. Como se ha dicho, con new se puede reservar espacio para cualquier tipo de dato, siendo muy utilizado para reservar espacio para matrices4. Un ejemplo: float *centros; centros = new float[numero]; Para ilustrar todo lo visto hasta el momento del operador new se ha realizado este sencillo programa de ejemplo: // Programa: malloc versus new // Fichero: MALVNEW.CPP #include <iostream.h> #include <string.h> #include <stdlib.h> class Prueba { int i, j; char *s; public: Prueba() { // Constructor por defecto i=j=5; s=new char[5]; strncpy(s, "Hola", 5); } Prueba(char *cad, int a, int b) { // Constructor con parámetros i=a; j=b; s=new char[strlen(cad)+1]; strcpy(s, cad); } ~Prueba () {delete [] s;} // Destructor int miembro(void) { // Función para mostrar los datos de la clase cout << "n" << s << "n"; return i+j; } void cambia(void) { // Función para ilustrar el uso de realloc int i=strlen(s); s=(char *)realloc(s, strlen(s)+2); s[i++]='+'; s[i]='0'; } }; // Fin de la definición de la clase 4 Las matrices que se reservan de forma dinámica no pueden recibir valores iniciales. Cuando se trate de una matriz de objetos se debe realizar un constructor por defecto, en caso contrario el compilador generará un error. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 5. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ -5- void main (void) { // Creamos un objeto dinámico con malloc Prueba *P1; P1=(Prueba *) malloc (sizeof (Prueba)); // Y nos vamos a estrellar: Salen caracteres raros por pantalla e // incluso se puede colgar el programa cout << "n" << P1->miembro() << "n"; // Creamos un par de objetos dinámicos con new Prueba *P2, *P3; P2=new Prueba; // Usa el constructor por defecto P3=new Prueba("Hola Caracola",4,3); // Usa el constructor con parámetros // Todo es correcto cout << "n" << P2->miembro() << "n"; cout << "n" << P3->miembro() << "n"; // Usamos realloc P2->cambia(); P3->cambia(); // Y todo sigue OK cout << "n" << P2->miembro() << "n"; cout << "n" << P3->miembro() << "n"; // Se libera la memoria que hemos reservado // Es buena técnica de programación liberar el espacio reservado // con malloc mediante la función free, y el espacio reservado con // new con el operador delete. free(P1); delete P2; delete P3; } El operador new tiene otra ventaja sobre malloc que todavía no se ha tratado. Es una buena técnica de programación controlar los posibles errores derivados de la falta de espacio a la hora de reservarlo en el manejo dinámico de la memoria. Cuando se programa en ANSI C, el programador tiene que construirse sus propios mecanismos para detectar que se ha producido un error cuando se ha llamado a malloc5. Sin embargo en C++, se puede seguir utilizando estas mismas técnicas de programación cuando se utiliza el operador new6, o por el contrario se puede elegir una alternativa más conveniente que nos ofrece este lenguaje. 5 Se recuerda que cuando esto ocurre malloc devuelve NULL. 6 Ya que new devuelve 0 si no hay espacio. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 6. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ -6- C++ define un puntero a función, de forma que cuando ocurre un error, la función a la que apunta el puntero es llamada. La definición de dicho puntero es: void (* _new_handler)(); El uso de este puntero tiene el inconveniente de no ser estándar. Por ello se recomienda el uso de una función de biblioteca definida en new.h, y que se llama set_new_handler, que toma como argumento un puntero a función, y asigna la función al puntero _new_handler. Aunque este segundo método es más común, y por lo tanto más conveniente, tampoco es estándar7. // Programa: Manejo de errores en la reserva de espacio con new // Versión Borland C++ // Fichero: NEW_ERR.CPP #include <iostream.h> #include <new.h> #include <stdlib.h> #define PASO 20000 void MemoriaInsuficiente(void) { cerr << "naEl espacio en el Almacenamiento Libre agotó.n"; exit(1); } void main (void) { long total=0; set_new_handler ( MemoriaInsuficiente ); // Otra manera de hacerlo con Borland C++ //_new_handler = MemoriaInsuficiente; for(;;) { char *espacio = new char[PASO]; total+=PASO; cout << "Llevamos reservados " << total << " bytes n"; } } 7 Se hicieron pruebas con los compiladores Borland C++ 3.1, Visual C++ 1.0, y DJGPP 2.6.0. Sólo el compilador de Borland aceptó la definición directa _new_handler = funcion; Cuando se utilizó la función set_new_handler(funcion); el compilador de Borland y el DJGPP, funcionaron sin problemas, pero para realizar esta operación con Visual C++ hubo que utilizar la función _set_new_handler y redefinir el prototipo de la función que manejaba el error. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 7. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ -7- // Programa: Manejo de errores en la reserva de espacio con new // Versión Microsoft C++ // Fichero: NEW_ERR.CPP #include <iostream.h> #include <new.h> #include <stdlib.h> int MemoriaInsuficiente(size_t size) { cerr << "naEl espacio en el Almacenamiento Libre se agotó.n"; exit(1); return 0; } void main (void) { long total=0; _set_new_handler ( MemoriaInsuficiente ); for(;;) { char *espacio = new char[20000]; total+=20000; cout << "Llevamos reservados " << total << " bytes n"; } } En resumen: Ventajas de new frente a malloc. Calcula de forma automática el tamaño del tipo para el que se está reservando memoria Proporciona el tipo correcto del puntero Es posible dar valores iniciales al objeto que se crea mediante el operador new La notación es mucha más clara Se puede definir una función que se encargue del manejo de los errores por falta de espacio en la zona de almacenamiento libre Cuando usar uno u otro. Tratar de usar ambos sistemas de manejo de memoria dinámica a la vez puede acarrear posibles problemas de inconsistencias Se recomienda el uso en C++ del operador new siempre que sea posible8, desechando para casos muy concretos el uso de las funciones derivadas de malloc En caso de seguir empleando las funciones derivadas de malloc, recordar que cuando se vaya a reservar espacio para una clase, se debe utilizar el operador new, o crear unos métodos que permitan iniciar las variables privadas, y llamarlos nada más reservar el espacio con malloc 8 Las ventajas que se obtienen son obvias. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 8. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ -8- El operador delete frente a free El operador delete se puede decir que es para new, lo que free para malloc. Esto es, libera los bloques de memoria, dejándolos listos para futuras reservas. La sintaxis de delete es muy sencilla: delete <nombre_puntero>; Cuando se trata de liberar el espacio asociado un puntero a un objeto, delete de forma automática llama al destructor. El operador delete se debe utilizar sólo con punteros que retorne new. Mientras que la función free se debe usar con punteros que apunten a zonas de memoria reservadas mediante funciones derivadas de malloc. De no hacerse así, y dependiendo de los compiladores, puede causar graves problemas9. Si se intenta liberar dos veces un mismo puntero, los resultados pueden ser catastróficos. Estas dos circunstancias deben ser controladas por el programador, ya que el compilador no las detecta. Se puede liberar un puntero nulo sin consecuencias adversas. El uso de delete con los tipos preestablecidos no tiene misterios, salvo en el caso de las matrices, que se debe usar la siguiente sintaxis: delete [tamaño] <nombre_puntero>; En la mayoría de los compiladores basta con poner los corchetes vacíos, ya que ignoran el número que se ponga entre ellos. Un sencillo ejemplo del uso del operador unario delete puede ser el siguiente: // Programa: Uso de delete // Fichero: DELETE1.CPP #include <iostream.h> #include <new.h> void main (void) { int *i, *vi; i=new int; *i=6; vi= new int[2]; vi[0]=0; vi[1]=1; cout << *i << " " << vi[0] << " " << vi[1] << "n"; delete i; // Elimina el objeto delete [] vi; // Libera una array de objetos } 9 Si se intenta liberar la memoria que se ha reservado mediante new para una instancia de una clase mediante free, se está perdiendo la llamada al destructor como consecuencia inmediata. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 9. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ -9- Sobrecarga de new y de delete En C++ es posible redefinir los operadores new y delete sobrecargándolos, si se quiere construir un procedimiento de manejo de memoria personalizado. Los esqueletos de las funciones que sobrecargan estos operadores son: void *operator new (size_t tamaño) { // Reserva dinámica return puntero_void; } void operator delete (void puntero) { // Liberar memoria a la que apunta el puntero } El operador new toma como argumento un tipo size_t que es un tipo entero que puede contener el mayor bloque individual de memoria que se pueda reservar. El número de bytes a reservar se indican mediante el parámetro tamaño. Además cualquier operador new que se cree debe devolver un puntero void. En cuanto al operador delete toma como argumento un puntero de tipo void, el cual apunta a la zona de memoria que debe liberar. Además cualquier operador delete que sea creado por el programador no debe retornar nada. Si se redefine el operador new de esta forma se sigue llamando al constructor de la clase cuando se tiene una instancia dinámica de una clase. Sin embargo, puede surgir algún tipo de problemas, cuando se sobrecargan estos operadores, con el uso de realloc debido a que el nuevo sistema de manejo de memoria dinámica utilice las mismas técnicas de manejo de memoria que las funciones de biblioteca del C. Para ilustrar la sobrecarga de los operadores new y delete se va ha realizar un sencillo programa que inicie el contenido del bloque de memoria que se quiere reservar a 0 antes de utilizarlo10. // Programa: Sobrecarga de los operadores new y delete // Fichero: N&DOVER1.CPP #include <iostream.h> #include <string.h> #include <stdlib.h> void *operator new ( size_t s ) { void *ptr=calloc(1, s); return ptr; } void operator delete ( void * ptr ) { free ( ptr ); } 10 Se utilizará la función calloc porque ésta inicia a 0 todo el bloque de memoria que reserva. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 10. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 10 - Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 11. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 11 - void main(void) { float *p=new float[5]; for (register int i=0; i<5; cout << " " << p[i++]); delete [] p; } También se puede redefinir el operador new de forma que tome parámetros adicionales, especificando los argumentos entre paréntesis. Para ilustrar esta característica se va a modificar el programa anterior, de forma que en lugar de rellenar el espacio en memoria con ceros, se inicie con un carácter determinado. // Programa: Sobrecarga de los operadores new y delete // Fichero: N&DOVER2.CPP #include <iostream.h> #include <string.h> #include <stdlib.h> void *operator new ( size_t s, int relleno ) { void *ptr; if ( (ptr = malloc(s) )!=NULL) memset(ptr, relleno, s); else exit(1); return ptr; } void operator delete ( void * ptr ) { free ( ptr ); } void main(void) { char *p=new ('/') char[40]; for (register int i=0; i<40; cout << p[i++] << " "); delete [] p; } Sobrecarga de new y delete respecto a una clase específica Esta característica va a permitir personalizar el manejo de memoria dinámica para una clase individual. Cuando se sobrecargan estos operadores con respecto a una clase en concreto, implica que cuando se utilicen estos operadores con cualquier otro tipo de datos se van a emplear los operadores new y delete originales, o aquellos que se hayan sobrecargado de forma global. Para sobrecargar estos operadores para una clase se tienen que declarar dos funciones miembro que tengan por nombre operator new y operator delete. Estos operadores tienen precedencia sobre los operadores new y delete globales. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 12. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 12 - Cuando se va a procesar el operador new para una instancia de una clase, el compilador primero comprueba si están definidos para dicha clase, y en caso afirmativo utiliza las versiones específicas de la clase. En caso contrario, si han sido sobrecargados globalmente, emplea estas nuevas versiones globales, y en último caso utilizaría los operadores estándar new y delete. Para ilustrar la sobrecarga de estos operadores respecto de una clase, se presenta el siguiente ejemplo: // Programa: Sobrecarga de los operadores new y delete respecto de una // clase // Fichero: N&DOVER3.CPP #include <iostream.h> #include <string.h> #include <stddef.h> #define NOMBRES 10 class Nombre { char nombre[25]; public: Nombre ( const char * ); void Presenta ( void ) const; void *operator new (size_t s); void operator delete (void *ptr); ~Nombre() {}; // Destructor que no hace nada }; // Memoria para manejar un número fijo de instancias de la clase Nombre char memo[NOMBRES] [sizeof (Nombre)]; int en_uso[NOMBRES]; // Funciones miembro de la clase Nombre Nombre::Nombre ( const char *cad ) { strncpy (nombre, cad, 25 ); } void Nombre::Presenta ( void ) const { cout << "n" << nombre << "n"; } // Sobrecarga de los operadores new y delete para la clase Nombre void * Nombre::operator new ( size_t s ) { for (int p=0; p < NOMBRES; p++) { if ( !en_uso[p] ) { en_uso[p] = 1; return memo + p; } } return 0; } Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 13. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 13 - void Nombre::operator delete ( void * ptr ) { en_uso[ ( (char *)ptr - memo[0] ) / sizeof ( Nombre ) ] = 0; } void main ( void ) { Nombre * directorio [NOMBRES]; char nombre[25]; for (register int i=0; i<NOMBRES; i++) { cout << "Introduce el nombre #" << i+1 << ": "; cin >> nombre; directorio[i] = new Nombre ( nombre ); } for (i=0; i<NOMBRES; i++) { directorio[i] -> Presenta(); delete directorio[i]; } } Notas: Este programa aprovecha la característica de conocer que no habrá nunca más de un número pequeño de instancias de clases al mismo tiempo, pero que se están alojando y desalojando de forma continua. Así se crean unas versiones específicas de new y delete, que trabajan mucho más rápido que la versión global. Se crea una matriz global que pueda contener todas las instancias de la clase (memo), y se hace que los operadores new y delete manejen dicha matriz. Se cuenta también con un vector de enteros de tantas entradas como filas tiene la matriz de instancias, llamado en_uso, y que va a servir como banderas, para indicar si la correspondiente entrada en la matriz memo está en uso o no. Cuando se ejecuta la sentencia directorio[i]=new Nombre( nombre ), el compilador invoca al operador new propio de la clase. Este encuentra un entrada libre en la matriz memo, y retorna su dirección. Después de esto el compilador llama al constructor de la clase, el cual usa el espacio en la matriz, y lo inicia con una cadena de caracteres. Finalmente, se asigna a la entrada de la matriz directorio un puntero al objeto recién creado. Por su parte, cuando se ejecuta la sentencia delete directorio[i], el compilador llama al destructor, que en este ejemplo no hace nada. Después de lo cual se invoca al operador delete propio de la clase. El operador delete encuentra la situación del objeto dentro de la matriz memo y lo marca como libre, con lo que el espacio está de nuevo preparado para alojar otra cadena. De lo aquí explicado se concluye que el operador new se llama antes que el constructor, y el operador delete se llama después del destructor. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 14. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 14 - Clases con punteros miembros Una práctica habitual es utilizar los operadores new y delete en las funciones miembros de una clase, como ya se ha visto en varios de los ejemplos que hasta ahora se han presentado. Pero en este apartado se va a estudiar con más profundidad esta posibilidad, comentando ciertos problemas que pueden surgir, y la forma de solventarlos. Para ello se va a crear una clase Cadena para el manejo de cadenas de caracteres, que el lector puede personalizar y ampliar como ejercicio práctico de programación. CADENA.H #ifndef __CADENA_H__ #define __CADENA_H__ #include <iostream.h> #include <string.h> class Cadena { char *cadena; unsigned longitud; public: Cadena(void); Cadena(const char *); Cadena(char, unsigned); void CambiaCaracter(int, char); char MuestraCaracter(int) const; int Longitud(void) const {return longitud;} void Muestra(void) const {cout << cadena;} void Agnadir(const char *); ~Cadena(); }; #endif CADENA.CPP // Programa: Clases con punteros miembros. Clase Cadena // Fichero Principal. // Fichero: CADENA.CPP #include "cadena.h" // Constructores Cadena::Cadena() { cadena=0; // Iniciamos puntero a "null" en C++ longitud=0; } Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 15. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 15 - Cadena::Cadena(const char *cad) { longitud=strlen(cad); cadena=new char[longitud+1]; strcpy(cadena, cad); } Cadena::Cadena(char c, unsigned n) { longitud=n; cadena=new char[n+1]; memset(cadena, c, n); cadena[n]='0'; } // Funciones miembro de la clase Cadena void Cadena::CambiaCaracter(int indice, char c) { if ( (indice >= 0) && (indice<longitud) ) cadena[indice]=c; } char Cadena::MuestraCaracter(int indice) const { if ( (indice >= 0) && (indice<longitud) ) return cadena[indice]; return 0; } void Cadena::Agnadir(const char *cad) { char *tmp; longitud += strlen(cad); tmp=new char[longitud+1]; strcpy(tmp, cadena); strcat(tmp, cad); delete [] cadena; cadena=tmp; } // Destructor Cadena::~Cadena() { delete [] cadena; } Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 16. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 16 - CADDEMO1.CPP // Programa: Clases con punteros miembros. Clase Cadena // Fichero Principal. // Fichero: CADDEMO1.CPP #include "cadena.h" void main(void) { Cadena Micadena ("esta es mi cadena."); Micadena.Muestra(); cout << "n"; Micadena.CambiaCaracter(0, 'E'); Micadena.Muestra(); cout << "n"; Micadena.Agnadir(" OK !!!!!"); Micadena.Muestra(); cout << "n"; } Para almacenar las cadenas nunca es lo más apropiado utilizar matrices, debido a que en principio se desconoce las longitudes que éstas pueden tener. En su lugar, tal y como se ha hecho en el ejemplo, se puede tener un puntero a carácter como miembro, y de forma dinámica reservar la cantidad exacta de memoria que necesite cada instancia de la clase. El constructor que recibe un puntero a carácter utiliza el operador new para reservar la memoria necesaria para almacenar la cadena. Después copia el contenido de la cadena en el espacio reservado. Como resultado de todo esto se tiene que cada objeto Cadena no es un bloque contiguo de memoria, sino que consiste en dos bloques de memoria: El primero contiene la longitud y un puntero a la cadena, y el otro almacena la cadena de caracteres en sí.11 La clase Cadena es un típico ejemplo que necesita un destructor, ya que cuando se destruye un objeto por salir del alcance en el que definido, se libera el espacio de memoria que ocupa el primer bloque de forma automática, pero el espacio que se reservó con new, debe ser liberado de forma explícita. De lo contrario el espacio con las cadenas de caracteres no sería nunca liberado, y el programa podría quedarse sin memoria en cualquier momento. Pero este ejemplo presenta un posible problema. Supongamos que utilizamos la clase Cadena con el siguiente programa principal: // Programa: Clases con punteros miembros. Clase Cadena // Fichero Principal. // Fichero: CADDEMO2.CPP 11 Si se utilizará sizeof para calcular el tamaño de un objeto Cadena, sólo se conseguiría en tamaño del bloque que contiene el entero y el puntero. Sin embargo, las diferentes instancias de la clase Cadena pueden tener cadenas de caracteres de diferente tamaño. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 17. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 17 - #include "cadena.h" #include <conio.h> void main(void) { Cadena c1("Esta es la primera cadena"), c2("Esta es otra cadena"); clrscr(); cout << "nLa cadena c1 es: "; c1.Muestra(); cout << "nLongitud de c1 es: " << c1.Longitud() << "n"; cout << "nLa cadena c2 es: "; c2.Muestra(); cout << "nLongitud de c2 es: " << c2.Longitud() << "n"; // Asignamos la primera cadena a la segunda, y mostramos // ambas cadenas, y sus longitudes c2=c1; cout << "nLa cadena c1 es: "; c1.Muestra(); cout << "nLongitud de c1 es: " << c1.Longitud() << "n"; cout << "nLa cadena c2 es: "; c2.Muestra(); cout << "nLongitud de c2 es: " << c2.Longitud() << "n"; // Ambas tienen el mismo contenido y la misma longitud // como era de esperar // Ahora cambiamos algo al objeto c1 y repetimos la // operación de mostrar ambos objetos c1.CambiaCaracter(1, 'S'); cout << "nLa cadena c1 es: "; c1.Muestra(); cout << "nLongitud de c1 es: " << c1.Longitud() << "n"; cout << "nLa cadena c2 es: "; c2.Muestra(); cout << "nLongitud de c2 es: " << c2.Longitud() << "n"; // Pero que pasa si yo he cambiado sólo la cadena del objeto c1, // y se han modificado los dos objetos ((( Qué pasaaaaa !!! // Ahora añadimos algo al objeto c1 y repetimos la // operación de mostrar ambos objetos c1.Agnadir(" :-)))"); cout << "nLa cadena c1 es: "; c1.Muestra(); cout << "nLongitud de c1 es: " << c1.Longitud() << "n"; cout << "nLa cadena c2 es: "; c2.Muestra(); cout << "nLongitud de c2 es: " << c2.Longitud() << "n"; // (((( Esto es una ruina !!!! La cadena del objeto c1 ha sido // modificada correctamente, pero la del objeto c2 se ha ido // a la ... } Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 18. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 18 - Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 19. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 19 - En el ejemplo se han creado dos objetos c1 y c2 de la clase Cadena. Para después hacer una asignación de c1 en c2. Cuando se realiza una asignación de un objeto a otro, el compilador realizaría lo siguiente: c2.longitud = c1.longitud; c2.cadena = c1.cadena; En la asignación del miembro longitud no hay ningún problema. sin embargo, como el miembro cadena es un puntero, el resultado de la asignación es que c1.cadena y c2.cadena apuntan a la misma zona de memoria. Es decir, ambos objetos comparten la cadena de caracteres. De forma gráfica: Aquí esta la explicación de los efectos no deseados que encontrábamos en el programa CADDEMO2.CPP. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 20. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 20 - Otros problemas más graves se pueden llegar a dar cuando el objeto sale del alcance donde fue definido. Cuando el destructor de la clase se llama para el objeto c1, se elimina dicho objeto liberándose también la memoria donde apuntaba el puntero cadena. Cuando el destructor se llama de nuevo para el objeto c2, se eliminará el puntero miembro cadena. Pero como los dos miembros cadena de ambos objetos tienen el mismo valor, significa que un puntero ha sido eliminado dos veces, esto puede tener consecuencias imprevistas. Además, el contenido original del miembro cadena del objeto c2 se ha perdido, y ese bloque de memoria no será liberado. Este problema ocurre con cualquier clase que contiene punteros miembro y reserva memoria del almacenamiento libre. En conclusión el operador de asignación que nos ofrece por defecto el compilador, no es adecuado para usarlo entre clases. La solución será reemplazar el que se nos ofrece por defecto por uno escrito por nosotros mismos, y que realmente realice la asignación de forma adecuada. Como ya es sabido, una de las características del C++ es que se permite la sobrecarga de operadores12. Lo que se va a hacer es dotar al operador de asignación de un significado especial cuando se refiera a la clase Cadena. Para redefinir el operador de asignación se debe crear una función miembro que se llame operator=. De esta forma cuando se vaya a realizar una asignación entre objetos de la clase Cadena, el compilador utilizará el operador de asignación escrito para dicha clase. El nuevo operador de asignación puede ser algo como: // Operador de asignación void Cadena::operator= (const Cadena &fuente) { longitud = fuente.longitud; delete [ ] cadena; cadena = new char[longitud + 1]; strcpy ( cadena, fuente.cadena); } 12 Que se estudiará con detalle más adelante Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 21. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 21 - El operador recibe como parámetro una referencia como constante13 a un objeto Cadena. El resultado ahora es correcto, y se ilustra en la siguiente figura: Una vez modificado el fichero cabecera, y el fichero cuerpo de la clase, compilar y ejecutar el programa CADDEMO2.CPP. Pero que ocurriría si se realiza una auto asignación c1 = c1; Esto que parece absurdo, y que casi nadie va a utilizar tiene otras formas más comunes: Cadena *Ptr_cad = &c1; // Más adelante ... c1 = *Ptr_cad; Lo que ocurrirá es que en la definición del nuevo operador de asignación, primero borra la cadena de caracteres, y después reserva un nuevo espacio, donde copia el contenido del espacio recién reservado en él mismo. Esto tiene como resultado que se introduce basura, y la pérdida de la cadena de caracteres del objeto. Para que el operador trabaje bien en todos los casos, se debe utilizar el puntero this14 para prever el caso de la auto asignación. Cuando una clase tiene punteros miembro, se debe tener en cuenta a la hora de trabajar con constructores copia. Como se vio cuando se trató el tema de los constructores copia, estos iniciaban un objeto con los valores de otro objeto. Aquí se tiene involucrado el uso del operador asignación. Pero como se ha visto en este mismo apartado, el operador de asignación por defecto nos va a dar resultados erróneos si la clase tiene punteros miembro. Así si se hiciera lo siguiente: Cadena c2(c1); Se estaría iniciando el objeto c2 con los valores del objeto c1, pero al utilizar el operador de asignación por defecto, lo que se estaría haciendo es que tanto c1 como c2 apuntarán a la misma cadena de caracteres. 13 Indicando que dicho operador no puede modificar el objeto que recibe 14 El puntero this se estudia en el siguiente apartado. Por lo que respecta a la correcta manera de crear el operador de asignación para la clase Cadena, realizar el ejercicio 3 de este mismo capítulo. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 22. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 22 - La solución pasa por construir un constructor copia propio. Como ejemplo se va a escribir el constructor copia para la clase Cadena. // Constructor Copia Cadena::Cadena ( const Cadena &fuente) { longitud = fuente.longitud; cadena = new char [longitud +1]; strcpy (cadena, fuente.cadena); } La realización del constructor copia es muy similar a la realización del operador asignación. Pero hay algunas diferencias entre ellos: Un operador de asignación actúa sobre un objeto que existe, mientras que un constructor copia crea uno nuevo. Como resultado, un operador de asignación puede tener que borrar la memoria que se reservó originalmente para el objeto que recibe los datos. Un operador de asignación tiene que comprobar las auto asignaciones. El constructor copia no tiene que hacerlo, porque en su caso no existen auto asignaciones. Para permitir asignaciones encadenadas, un operador de asignación debe retornar *this. Por ser un constructor, el constructor copia no puede devolver valores. El Puntero this Cada vez que se invoca a una función miembro, se le pasa automáticamente un puntero al objeto que la ha invocado Se puede acceder a ese puntero utilizando la palabra reservada this El puntero this es un parámetro implícito a toda función miembro15 Para acceder a un elemento concreto de un objeto cuando se utiliza el objeto en sí, se emplea el operador punto (.) Para acceder a un elemento concreto de un objeto cuando se utiliza un puntero al objeto, se emplea el operador flecha (->) Cuando se llama a una función miembro, el compilador asigna la dirección del objeto al puntero this, y después se llama a la función. Cada vez que una función miembro accede a un dato miembro, se está utilizando de forma implícita el puntero this. Por ejemplo considérese el siguiente programa en C++: #include <iostream.h> class D { int i,j,k; public: D() {i=j=k=0;} 15 Excepto para las funciones miembro static Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 23. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 23 - void Mostrar(void) { cout << "n" << i << " " << j << " " << k; } } A; void main () { A.Mostrar(); } Sería equivalente a este otro: #include <iostream.h> class D { int i,j,k; public: D() {this->i=this->j=this->k=0;} void Mostrar(void) { cout << "n" << this->i << " " << this->j << " " << this->k; } } A; void main () { A.Mostrar(); } A la vista del ejemplo se puede deducir que es lícito el uso del puntero this cuando se accede a los datos miembro, aunque es del todo innecesario. También es correcto utilizar la expresión *this para referirse al objeto que ha llamado a la función miembro. Así las tres sentencias que aparecen en la siguiente función son equivalentes: void Mostrar(void) { cout << "n" << i << " " << j << " " << k; cout << "n" << this->i << " " << this->j << " " << this->k; cout << "n" << (*this).i << " " << (*this).j << " " << (*this).k; } Otro de los usos del puntero this es comprobar si el objeto que se pasa como parámetro a una función miembro es el mismo objeto que llamó a dicha función. Esto es fundamental para las auto asignaciones. Lo que se hace es comprobar que la dirección del objeto que se pasa como argumento es diferente del valor del puntero this, si esto es así se puede hacer la asignación sin problemas. Sino se sale de la función sin hacer nada. Si se utiliza *this como valor de retorno de una función, se puede lograr crear operadores que sean asociativos bien por la derecha, como es el caso de la asignación, bien por la izquierda, como ocurre en las sentencias cout de este tipo cout << a << b << c; Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 24. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 24 - El puntero this es un puntero constante, por lo tanto una función miembro no puede cambiar el valor del puntero, y hacer que apunte a otro sitio. Sin embargo, en las primeras versiones de C++, el puntero this no era constante, y esto permitía al programador hacer asignaciones al puntero this para personalizar el manejo de la memoria dinámica. Esto ya no está permitido en las últimas versiones de C++. Funciones estáticas y el puntero this Cuando en el capítulo anterior se abordó el tema de las funciones miembro static, se definieron como aquellas que sólo podían trabajar con miembros dato estáticos. Esto es debido a que este tipo de funciones carecen de puntero this. Pero el problema surge si una función necesita de forma imperiosa acceder a un miembro dato de una clase. Para solucionarlo se le ha de pasar de forma explícita un puntero this. Véase el siguiente ejemplo: // Programa: Funciones miembro estáticas y el puntero this // Fichero: STTHIS.CPP #include <iostream.h> class Prueba { int a, b; static int decre; public: Prueba () {a=b=0;} Prueba (int a1, int b1=0) {a=a1; b=b1;} static int Ejemplo(Prueba *E) {return E->a+E->b-decre;} }; int Prueba::decre=1; void main (void) { Prueba P1(1,2); cout << Prueba::Ejemplo (&P1) << "n"; } Paso y retorno de objetos Hay otras dos situaciones, a parte de en las definiciones, en las que un constructor copia se llama: Cuando una función recibe un objeto como parámetro. Cuando una función retorna un objeto. Supongamos un extracto de programa en C++ en el que se muestra una función que recibe un objeto Cadena como argumento. // Función que recibe un objeto Cadena como argumento void ProcesaCadena ( Cadena cad ) { // Usa el objeto cad } Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 25. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 25 - void main () { Cadena c1("Cadena de ejemplo"); ProcesaCadena ( c1); } La función ProcesaCadena recibe un objeto que se pasa por valor. Esto significa que la función tiene su propia copia privada del objeto. El parámetro de la función se inicia con el objeto que se pasa como argumento. El compilador de forma implícita llama al constructor copia para realizar esta iniciación. Si no se define un constructor copia para realizar la iniciación, el compilador realiza su constructor copia por defecto, y el objeto cad y el objeto c1 tendrían la misma cadena de caracteres, y por lo tanto cualquier modificación en la cadena de caracteres de cad modificaría la cadena de caracteres del objeto c1. También habría problemas en cuanto que el objeto cad tiene un alcance local, y por lo tanto al salir de la función se llamaría al destructor. Esto significa que el objeto c1 tendría un puntero que apuntaría a una zona de memoria que ya ha sido liberada, lo cual iba a ser poco recomendable. Ahora supongamos un extracto de fuente en C++ donde se incluye una función que retorna un objeto Cadena. // Función que retorna un objeto Cadena Cadena RetCadena ( void ) { Cadena valor ( "Esto es una prueba."); return valor; } void main () { Cadena c1; c1 = RetCadena(); } La función RetCadena retorna un objeto Cadena. El compilador llama al constructor copia para iniciar un objeto temporal y oculto en el alcance de la llamada, usando el objeto que se especifica en la sentencia return. Este objeto temporal es usado como parte derecha de la asignación que hay en el programa principal. De nuevo, se necesita un constructor copia. En otro caso el objeto temporal compartiría la misma cadena de caracteres que el objeto valor, el cual será eliminado cuando la función RetCadena finalice su ejecución, y en consecuencia la asignación que se hace a c1 no está garantizada. Como regla a seguir, se debe definir siempre un constructor copia y un operador de asignación cuando se tenga una clase que contenga punteros miembro y reserve espacio del almacenamiento libre. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 26. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 26 - Paso y retorno de referencias a objetos En el apartado anterior se ha visto como cada vez que se pasaba un objeto a una función por valor, se llamaba al constructor copia. Una forma de simular el paso por valor de un objeto, pero sin tener que llamar al constructor copia sería utilizar referencias a objetos constantes. Estudiemos el mismo ejemplo que se vio en el apartado anterior, pero ahora la función recibe una referencia al objeto constante. // Función que recibe un objeto Cadena como argumento void ProcesaCadena ( const Cadena &cad ) { // Usa el objeto cad } void main () { Cadena c1("Cadena de ejemplo"); ProcesaCadena ( c1); } Ahora el constructor copia no es llamado porque no se ha construido un nuevo objeto. En su lugar se inicia una referencia con el objeto que se le pasa. Como resultado se usa el mismo objeto que en la llamada a la función. El uso de la palabra reservada const es debido a que se quiere asegurar la integridad del objeto, esto es, que no se pueda modificar el objeto en la función. Igualmente que una función retorne una referencia en lugar de un objeto es mucho más eficiente. El constructor copia no se llama cuando se produce el valor de retorno, porque no se crea un objeto temporal, sólo se crea una referencia temporal. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 27. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 27 - Ejercicios propuestos 1. Indicar cuál de las siguientes frases es incorrecta y explicar los motivos a) La función delete sirve para liberar zonas de memoria que han sido reservadas mediante new. b) El uso de malloc está prohibido en C++ c) La función set_new_handler sirve para que una función determinada se encargue del manejo de los errores por falta de espacio en el manejo de memoria dinámica mediante el operador new. d) Cuando se crea un objeto con el operador new no se llama al constructor de la clase a menos que se indique de forma explícita mediante argumentos. e) Aunque no se debe, si se usa free para liberar un puntero a una instancia devuelto por new, se llama al destructor si existe. 2. Reescribir la función cambia() del programa MALVNEW.CPP, reemplazando la función realloc por una simulación de ésta mediante el operador new. 3. Tenemos la clase Cadena. class Cadena { char *cadena; unsigned longitud; public: Cadena(void); Cadena(const char *); Cadena(char, unsigned); void CambiaCaracter(int, char); char MuestraCaracter(int) const; int Longitud(void) const {return longitud;} void Muestra(void) const {cout << cadena;} void Agnadir(const char *); ~Cadena(); }; Se ha redefinido el operador de asignación para dicha clase. // Operador de asignación void Cadena::operator= (const Cadena &fuente) { longitud = fuente.longitud; delete [] cadena; cadena = new char[longitud + 1]; strcpy ( cadena, fuente.cadena); } Escribir de nuevo este operador de asignación de la clase Cadena, de forma que se admita la “auto asignación”: c1 = c1 o Cadena *ptr_cad = &c1; Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)
  • 28. Programación Orientada a Objetos 3 – Clases manejo de memoria dinámica en C++ - 28 - 4. Modificar de nuevo el operador de asignación para que ahora también permita: c1=c2=c3; 5. Un constructor copia tiene como argumento una referencia a un objeto en lugar de un objeto. ¿Por qué? 6. Crear una clase apropiada para representar el tipo de dato número complejo. Un número complejo se representará como (a, b), donde a representa la parte real y b la imaginaria. Construir además las funciones apropiadas para: Asignar valor a un número complejo Imprimir un número complejo con el formato (a, b) Sumar dos números complejos Restar dos números complejos Multiplicar dos números complejos Construir, además, un pequeño programa principal que compruebe el comportamiento de esta clase. 7. Crear una clase apropiada para representar el tipo de dato rectángulo. Construir además las funciones apropiadas para: Calcular el área de un rectángulo Calcular el perímetro de un rectángulo Dados dos rectángulos, determinar cual es el mayor, teniendo en cuenta que el mayor es aquel que tiene mayor área. Dados dos rectángulos, determinar si sin idénticos. Ser idénticos implica que tiene el mismo área y el mismo perímetro. Intercambiar los valores entre dos rectángulos Ordenar un vector de rectángulos, de mayor a menor 8. Incluir a la clase Cadena definida en este documento los siguientes métodos: Invertir una cadena Pasar a mayúsculas Determinar si es un palíndromo Construir además una función que permita ordenar de mayor a menor un vector de Cadenas. Ingeniería Técnica en Informática de Sistemas (3er curso) Departamento de Informática y Automática – Universidad de Salamanca (versión Febrero 2003)