Problemas con punteros II

Memoria dinamica

Las operaciones de reservar memoria dinamica y liberarla, con new y delete, estan enteramente en manos del programador, esto proporciona gran flexiblidad de recursos pero tambien oporturnidad para diversos tipos de errores. Cada vez que aparece el operador new en relacion a un objeto deberia haber una aplicacion del operador delete a ese mismo objeto. Los problemas suelen generarse por dos modalidades de error:
1-No liberar la memoria dinamica reservada para un objeto.
2-Intentar borrar, o desreferenciar, un objeto ya borrado.

1-Memoria no liberada

La memoria reservada dinamicamente necesita ser liberada de modo explicito con delete. Si existe un objeto que ya no usamos, y que fue almacenado dinamicamente, nos encontramos frente a una 'fuga de memoria' (memory leak), el caso mas critico se presenta cuando ya no es posible acceder al objeto, pues no sera posible borrarlo. Durante la ejecucion del programa el numero de 'fugas de memoria' puede multiplicarse hasta agotar los recursos disponibles.

Un caso muy simple donde no es posible acceder al objeto para liberar memoria es el siguiente:

int f (int a) 
{
char* p = new char [a];
return 0;
}

La funcion no hace nada interesante, es simplemente el esquema de un error posible. La memoria reservada para el puntero 'p' no ha sido liberada al salir de la funcion, la direccion apuntada por 'p' se pierde, y no sera posible liberar esa memoria en ningun sitio. Es el modo mas simple de producir una fuga de memoria.
Habria sido necesario agregar la linea:
  delete [] p;
antes de salir de la funcion para que ese monto de memoria hubiera sido liberado.

Mientras que es tecnicamente posible reservar memoria en una funcion y liberar esa memoria en otra funcion, se considera una practica riesgosa, por la posiblidad de olvidar quien tiene la responsabilidad de liberar memoria. Una posible solucion es reservar memoria para el objeto en la funcion llamadora, pasar el objeto como parametro (por referencia) y retornarlo, asi la responsabilidad de reservar y liberar memoria respecto al objeto estara en manos de una misma funcion. El esquema seria el siguiente:

int f (char* c)
{
...............
return 0;
}
int main()
{
int b = 34;
char* t = new char[b];
f(t);
...............
delete [] t;
...............

Otro modo de provocar problemas es reservar memoria dinamica por segunda vez para un puntero, antes que haya sido liberada la primer reserva. Por ejemplo:

void f (int a, int b) 
{
char* p = new char[a];
......................
p = new char[b];
.....................

Aqui la memoria reservada por el primer uso de new ya no podra ser liberada, pues se ha perdido su direccion. Toda segunda asignacion del puntero 'p' sin antes liberar la memoria dinamica asociada a el, producira fugas de memoria. Otra variacion del mismo problema es el siguiente:

void f (int a)
{
char* p = new char[a];
char* q = new char[a];
......................
p = q;
.....................
delete [] p;
delete [] q;
}

Este esquema de error es importante pues, como se vera mas adelante, se presenta en forma velada en problemas con constructores de objetos. El error es reasignar el puntero 'p' antes de liberar la memoria por el reservada. Esto tiene dos consecuencias negativas:
1- La memoria reservada originalmente por 'p' no podra ser liberada.
2- La memoria reservada por 'q' sera liberada dos veces (muy problematico).

Un modo de fallar en liberar acertadamente la memoria reservada con new, en relacion a un array, consiste en aplicar el operador delete, olvidando los corchetes entre el operador y el nombre del puntero.

int f () 
{
char* p = new char[100];
........................
delete p;                     //error, solo libera un elemento de 'p'
delete [] p;                  //bien, libera el array apuntado por 'p'

2-Operar con un objeto ya borrado

La segunda familia de problemas se produce por intentar desreferenciar o usar un puntero al cual ya se ha aplicado el operador delete. El principal recurso para evitar este problema es (una vez aplicado el operador delete) setear este puntero a NULL, esto protege contra posteriores usos equivocados de delete, pues por convencion la aplicacion de delete a un puntero nulo no tiene ningun efecto.

Tambien es un error desreferenciar un puntero al que se ha aplicado delete sin antes asignarle una nueva direccion. La razon es que el puntero esta apuntando a alguna zona que almacena valores indeterminados, sobreescribir alli puede destruir datos pertenecientes a otras variables o a otras reservas dinamicas. La solucion es reasignar una direccion al puntero antes de desreferenciarlo, norma general que tambien soluciona el problema de los punteros nulos. Los pasos correctos se ejemplifican en el siguiente codigo:

int f () {
char cad[] = "hola";

char* p = new char[40];             //Primera reserva de memoria dinamica
.......................
delete [] p;                        //Liberacion de mem dinamica 
p = NULL;                           //Precaucion por posible sobreborrado de 'p'
.......................
p = cad;                            //Nueva asignacion
*p = 'a';                           //Desreferenciacion de 'p' 

Nunca se debe desreferenciar un puntero sin antes asignarle un valor.

Datos miembros punteros y copia de objetos

Los datos miembro de un objeto pueden ser inicializados mediante un constructor, una inicializacion de copia, o asignacion de copia.
Suponiendo la existencia de una clase llamada "Clasex" veamos las siguientes lineas:

Clasex a;
Clasex b;
Clase c;
Clasex d = b;        //Inicializacion de copia (constructor copia)
c = a;               //Asignacion de copia

La sola declaracion de los objetos 'b' y 'a', sin parametros, invoca un constructor por defecto. En la tercera linea no se invoca al constructor, se realiza una copia del objeto 'b' en el objeto 'a'. A menos que se especifique algo distinto, esta copia (llamada 'asignacion de copia'), produce una replica miembro a miembro de los datos privados de 'b' en los datos privados de 'a'.
A primera vista esto es muy natural y no problematico, pero si entre los datos privados figuran punteros entonces pueden plantearse importantes problemas.

class Clasex {
int x;
char ch;
char* cad;
public:
Clasex (int n = 40) { cad = new char [n];
}            //Constructor default
~Clasex () {delete [] cad;}                      //Destructor 
..............
}

void f() {
Clasex a;         //Invoca constructor
Clasex b = a;     //Inicializacion de copia - Problemas con el puntero!
Clase c;          //Invoca constructor
c = b;            //Asignacion de copia - Problemas con el puntero!
}

Aqui hay tres objetos "Clasex" en juego. Se trata de objetos locales, por lo tanto el destructor sera invocado de modo automatico tres veces al salir de la funcion.
El primer problema se plantea cuando tomamos conciencia de que, en la funcion "f ( )", el constructor es llamado solo dos veces (el destructor: tres veces), tanto la inicializacion de copia como la asignacion de copia no utilizan el constructor, sino que copian datos miembro a miembro.
El esquema de los tres objetos seria el siguiente:

'a' 'b' 'c'
Datos privados.
Una copia individual
por cada objeto
a.x
a.ch
a.cad
b.x
b.ch
b.cad
c.x
c.ch
c.cad
Funciones publicas:
una copia para todos
los objetos
Clasex::Clasex (int);
Clasex::~Clasex();

La copia de los datos 'x' y 'ch' no presenta ningun problema, cada objeto tiene su 'x' y su 'ch' en distintas localidades de memoria, copiar estas variables es copiar el valor almacenado. Cada objeto tiene tambien una localidad de memoria para 'su' puntero 'cad', el problema es adonde apuntan esos punteros.
La copia de punteros (ej, a.cad = b.cad) es copia de las direcciones a la que apuntan. Pero esas direcciones, en nuestro ejemplo, son localidades de memoria reservadas mediante memoria dinamica, por lo tanto, luego de:

a = b;

Sucedera que los punteros "cad" de ambos objetos apuntaran a la misma zona de memoria reservada con new, y es aqui donde se presenta el problema. Hay tres objetos y por lo tanto tres punteros, cada puntero deberia tener sus propios bytes reservados (el default para nuestro ejemplo es 40). Una llamada comun al constructor reserva esos bytes, y son diferentes para cada objeto, pero una asignacion de copia hace que un puntero deje de apuntar a 'su propia zona' y apunte a los mismos bytes que el otro puntero.
Como consecuencia:
-Una zona de memoria queda fuera de alcance, no pudiendo ser liberada y se crea una fuga de memoria
-Dos punteros apuntan a la misma zona, cuando se invoque el destructor, este liberara dos veces una misma zona de memoria, lo que es muy problematico.

La solucion. Cuando entre los datos privados hay punteros es necesario explicitar un constructor de copia diferente al default, para evitar la copia entre punteros, y es necesario tambien proveer de un asignador de copia diferente al default, esto significa que para disponer de la notacion "a = b" sera necesario sobrecargar el operador '=' y darle un sentido diferente, que evite la copia de punteros.

La naturaleza del problema puede aclararse con un codigo que no utiliza clases pero que presenta el mismo error.

void f () {
char* a = new char[40];
char* b = new char[40];
a = b;                      //Error! no se debe reasignar sin antes liberar memoria
delete [] b;
delete [] a;
}

Cada puntero tiene 'sus' 40 bytes de memoria dinamica, al reasignar "a=b" el puntero 'a' deja de apuntar a la zona de memoria reservada con 'new', esa direccion se pierde y no podra ser liberada (fuga de memoria). Por otra parte, las dos invocaciones de 'delete' cometen el error de liberar una misma zona de memoria.

PRINCIPAL

Free Web Hosting