Aula Macedonia


Curso de Programación Orientada a Objetos en C++


Artículo realizado por
Fernando Rodríguez.





Capítulo 9.
Gestión de memoria; los operadores new y delete.

En este penúltimo capítulo del curso vamos a hablar de los operadores destinados a la gestión de memoria. Todos recordamos, como programadores de C, que en este lenguaje teníamos instrucciones como malloc o free, entre otras, para reservar espacio a datos en tiempo de ejecución. El gran problema de estas instrucciones, además de su moderada complejidad, consistía en que eran funciones, es decir, era necesario incluir el consabido archivo de cabecera, que hacía aumentar, de forma considerable, el tamaño final del ejecutable.

Con el C++ tenemos dos excelentes operadores para la gestión de memoria: new y delete. Gracias a la incorporación de estos operadores, en C++, no va a ser necesario definir ningún archivo de cabecera extra, es decir, new y delete son operadores no funciones con lo que van incluidos en el propio lenguaje C++. Además de esta innegable ventaja, new y delete no son tan "estrictos" como malloc y free pues permiten usarse sin tener que recurrir a una ahormado de tipos, es decir, no tenemos que convertir un puntero de tipo void (que era el puntero devuelto por malloc) al puntero que nos interesa. El operador new devuelve un puntero que es el que exactamente estábamos persiguiendo.

Un poco de teoría sobre memoria

Durante los ejemplos de los últimos capítulos del curso hemos estado utilizando, de vez en cuando, estos operadores para crear punteros a objetos (aunque también podríamos haberlos utilizado para crear cualquier otro tipo de variables). Las variables de tipo dinámico creadas por el operador new y eliminadas con el operador delete, se almacenan en una porción de memoria llamada heap (o montón). Las variables locales a una función, es decir, las que se definen en tiempo de compilación, junto a los argumentos que ésta recibe se almacenan en una porción de memoria llamada pila (o stack).

La memoria heap es capaz de dar "cobijo" a estructuras de datos muy grandes ya que su espacio depende de la cantidad de memoria virtual que tengamos en el sistema propio. Es por esto que se utilicen siempre para alojar grandes estructuras como arrays u instancias a clases de gran tamaño. El principal problema en este tipo de memoria es que nosotros, como programadores, debemos de hacernos cargo de "limpiar" la memoria al abandonar el programa ya que de lo contrario, nuestras variables dinámicas seguirán estando ahí y podrán producir serios fallos en el sistema al intentar arrancar otros programas.

La memoria de pila, por el contrario, es de mucho menor tamaño. Esta memoria es reservada en tiempo de compilación y no es capaz de variar; siempre tiene el mismo tamaño. En esta memoria se alojan las variables locales a una función y los propios argumentos que esta recibe. Las variables declaradas localmente a una función se caracterizan por no tener que necesitar un seguimiento tan "exhaustivo" como las de tipo dinámico. Esto hace que su utilización sea muy sencilla pues el compilador se encarga de borrarlas y de gestionar la memoria por nosotros en el momento en que nuestro programa en ejecución, abandona la función en cuestión. Suele ser común que cuando no se tiene cuidado a la hora de declarar las variables en la pila se produzcan "stack's overflows" (pila desbordada) en tiempo de ejecución.

La principal diferencia, pues, entre el heap y la pila es que la pila es una porción de memoria de tipo estático, es decir, hay un límite que no se puede sobrepasar, mientras que la memoria heap es dinámica, es decir, varía en función de la que necesitemos en cada momento y con los compiladores de 32 bits, esta memoria tiene una capacidad enorme que es directamente proporcional a la memoria virtual libre en el equipo del usuario. Por memoria virtual se entiende toda la memoria del sistema, tanto la ram libre como la del disco duro (que también se puede utilizar como memoria para "alocatear" los programas o procesos).

El operador new

El operador new viene a hacer las veces de la función malloc del C tradicional. Con la función malloc, nosotros podíamos reservar memoria en tiempo de ejecución. El principal problema es que siempre debíamos de especificar el tipo de puntero que queríamos que se nos devolviera pues esta función retornaba el puntero genérico void, esto es, era una función algo más compleja de lo habitual. He aquí un ejemplo con la vieja malloc:


// Ejemplo que reserva memoria para 1000 enteros, es decir,
// se reservan 1000x2bytes de un int = 2000 bytes.

int *m_buffEnteros;
m_buffEnteros = (int *) malloc(1000);

Como se comentaba más atrás, el uso de operador new es mucho más sencillo. Para reservar toda esa buffer, sólo deberíamos de hacer esto:


int* m_buffEnteros;
m_buffEnteros = new int[1000];

o de forma más clara y usual


int* m_buffEnteros = new int[1000];

En estos dos ejemplos de uso de malloc y new hemos declarado un puntero llamado m_buffEnteros que apunta a una zona de memoria reservada para 1000 enteros y que tiene un tamaño de 2000 bytes. En el caso de que no existiera memoria disponible en el sistema, el operador new devolvería un valor igual a 0. Por otro lado, como el operador new hace una comprobación de tipos, si el puntero no es del tipo correcto se lanza un mensaje de error. Así, si queréis ser previsores se recomendaría hacer algo así:


int* m_buffEnteros = new int[1000] ;

if (m_buffEnteros)
     cout << "\n¡Se ha reservado memoria!";
else 
     cout << "\n¡Error: No se ha reservado memoria!";

 

De todos modos, es muy raro que no exista memoria disponible (pero sí posible), sobretodo si estáis trabajando con un compilador de 32 bits como puede ser el Visual C++, últimas versiones, con lo que esa comprobación quizás resulte excesivamente preventiva.

El operador delete

El operador delete sirve para liberar la memoria que hayamos reservado con el operador new. Es realmente similar al free. El único inconveniente que podría ocasionar el uso del operador delete sería el utilizarlo en aquellos casos en el que el puntero a borrar realmente no ha sido reservado correctamente con la llamada a new y tiene un valor no nulo. En los demás casos, esto es, cuando el puntero valga NULL o realmente apunte a una zona de memoria que sí ha sido correctamente reservada.

La forma de utilizar el operador delete es muy sencilla, basta con poner delete y seguidamente el puntero:

delete m_buffEnteros;

De la misma forma que se escribió la forma de ser precavidos con el uso de new, pongo a continuación una forma elegante para utilizar delete:


if (m_buffEnteros)
{
     delete m_buffEnteros
     m_buffEnteros = NULL;
}

Para terminar

Utilizad new y delete pues facilitan un montón el trabajo con memoria y realmente todo son ventajas. Ni que decir tiene que el trabajo de los punteros que se han reservado con estos operadores es el de siempre, es decir, si estáis trabajando con variables hay que utilizar el operador uniario "*" para acceder al valor. Si trabajáis con estructuras o instancias, esto es, objetos, deberéis de utilizar el operador "->" para acceder a los miembros. Si revisáis ejemplos de capítulos anteriores veréis casos de estos. De todas formas, cuidado con la memoria ;).

¿Y ahora qué?

Realmente ya se han redactado todos los capítulos del curso de programación orientada a objetos con C++ que si recordáis fue establecido en el capítulo 1. De todas formas, se hará un capítulo más en el que se abordará un caso muy práctico y de vigente actualidad: "Las MFC y el Visual C++". Por tanto, ¡hasta el próximo y último!.





AULA MACEDONIA
a
MACEDONIA Magazine