martes, 31 de julio de 2012

Punteros en C++ - Parte II: Introducción

En el post anterior Punteros en C++ - Parte I: Introducción, expliqué lo más básico sobre punteros: su definición y su significado. Sin embargo, quedó pendiente explicar la pregunta más importante acerca de su existencia: Porqué querríamos almacenar direcciones de memoria en variables? La pregunta es totalmente válida, más aún con las complicaciones inherentes de usarlos. El objetivo de este post es responder la pregunta planteada anteriormente.

En el post anterior asumimos que la memoria era un conjunto de celdas en donde era posible almacenar información, lo cual es cierto, pero no del todo exacto. El sistema operativo segmenta la memoria cuando se inicia: una parte dedicada a los procesos del sistema operativo, otra para almacenar procesos de usuario, un segmento de memoria libre, etc. Nos vamos a enfocar en los dos últimos mencionados.
Cuando un programa se ejecuta, el sistema operativo le asigna una porción de memoria para que éste almacene su código máquina y los datos que usa. La parte de la memoria que un programa usa para los datos (es decir las celdas en donde se almacenan las variables) se llama Memoria Pila (o Stack). Se llama así porque es una estructura de tipo FIFO, en la que las asignaciones (y desasignaciones) de memoria se hacen siempre por el mismo extremo de la memoria.

La mala noticia es que el tamaño de la memoria pila de un programa no es tan grande como quisiéramos. Veamos el siguiente programa:

#include <iostream>
#include <cstdlib>

using namespace std;

int main(int argc, char* argv[]){
 
 //MB necesarios
 cout << (sizeof(double)*10000000)/(1024*1024) << endl;
 double A[10000000];
 return EXIT_SUCCESS;
}
En este programa, intentamos imprimir un mensaje con la cantidad de MB necesarios para almacenar un arreglo de 10 millones de double's. A continuación, creamos un arreglo A de double con 10 millones de elementos. Esto es aproximadamente 76MB de memoria necesaria (asumiendo que un double ocupa 8 bytes). Cuando ejecutamos el programa, el sistema operativo le asigna su memoria pila al proceso e intenta asignar una porción de memoria pila para A. El problema es que la memoria pila no puede crecer en tiempo de ejecución (y tampoco tiene 76MB disponibles!), por lo que nuestro programa termina intempestivamente con un stack overflow (o violación de segmento). Nota: el comportamiento de este programa puede cambiar, dependiendo del computador en el que se ejecuta. Yo lo ejecuté en un Core 2 Duo con 2GB de memoria.

Sin embargo, el comportamiento de nuestro programa está lejos de ser el que queremos. Tengo un computador con 2GB de memoria y no puedo ejecutar un programa que usa 76MB? Ok, no hay porqué desesperarnos, en realidad sí se puede, pero no así. La explicación para este fenómeno es que el sistema operativo limita la capacidad para cada proceso, porque sino entonces no podríamos ejecutar varios procesos a la vez compartiendo la memoria.

La buena noticia es que existe (gracias a Dios!) el segmento de memoria libre (Aleluya!). Más comúnmente conocido como Heap, éste segmento es una gran porción de memoria que el sistema operativo deja para el uso de quienes lo requieran. Además, así como esta porción de memoria es libre, entonces su comportamiento necesita ser dinámico para que los programas pidan más memoria cuando la necesiten y la liberen cuando ya no. Para mantener las cosas aún en regla, el sistema operativo administra el Heap, de manera que cuando un programa pide memoria libre, el sistema operativo busca si existe la cantidad de memoria que se requiere. Si existe, la reserva y le devuelve al programa la dirección de memoria en donde inicia la memoria reservada. Y Oh sorpresa!!! necesitamos almacenar esa dirección de memoria en algún lado, osea un puntero.

Dicho resumidamente, un puntero tiene que ser usado siempre que hacemos uso de la memoria libre, la cual nos permite usar mucha memoria a nuestro antojo (siempre bajo los límites de la memoria física, claro). Si no tuviéramos los punteros, no podríamos hacerlo y nuestros programas tendrían una capacidad limitada de memoria a usar.

Un inconveniente de usar la memoria libre, es que el programador es el responsable de solicitar memoria y de liberarla. En programas complejos, esto resulta en un verdadero dolor de cabeza para los principiantes, pero nada, que bastante práctica, no logre solucionar. Veamos el siguiente programa:

#include <iostream>
#include <cstdlib>

using namespace std;

int main(int argc, char* argv[]){
 
 //MB de memoria necesarios
 cout << (sizeof(double)*100000000)/(1024*1024) << endl;
 double* A = new double[100000000];
 /*Hacemos lo que tengamos que hacer con A*/
 delete[] A;
 return EXIT_SUCCESS;
}

En C++, el operador new se encarga de reservar memoria en la memoria libre. La cantidad de memoria solicitada está implícita en el tipo de dato que lo sucede. En el ejemplo,  solicitamos una cantidad de memoria para almacenar 100 millones de double's (ojo que solicitamos 10 veces más que en el programa anterior, equivalente a 762MB). La dirección de memoria en donde empieza la memoria reservada es retornada y se almacena en A, que es un puntero a double.  Finalmente, cuando nuestro programa va a terminar, liberamos la memoria reservada con delete.

Aunque el uso de la memoria dinámica y los punteros asociados es una alternativa que nos permite emplear mucha más memoria, su uso debe ser mesurado. Si tu programa, va a usar poca memoria, entonces dale prioridad al almacenamiento estático. El empleo de la memoria dinámica conlleva más procesamiento a nivel del sistema operativo (por el tema de mantener registro de la porciones de memoria ocupadas y libres ) y por lo tanto siempre añadirá cierto tiempo de procesamiento adicional. Por otro lado, si tu programa necesita memoria en tiempo de ejecución, la única alternativa es el empleo de memoria dinámica, por lo que en esos casos el uso de punteros puede ser la única alternativa.

Espero haber despejado algunas dudas acerca del uso de los punteros en C++, y si no, los siguientes posts pueden dar mejores luces acerca de su uso e importancia. En el siguiente post, hablaré de una útil aplicación de los punteros: estructuras de datos dinámicas.

Nota final: si deseas conocer más acerca del funcionamiento de la memoria en sistemas operativos modernos, recomiendo el estudio del Sistema Operativo Minix 3, cuyo código fuente (enteramente escrito en C y assembler) permite indagar a muy bajo nivel cómo se organiza todo dentro de nuestro computador. Además, el núcleo de Minix inspiró a Linus Torvalds a crear Linux :).

2 comentarios:

  1. Gracias ahora tengo un conocimiento más amplio acerca del manejo de puntos en C++...!

    ResponderEliminar
  2. Si puedes seguir haciendo más post acerca de punteros estaría genial, Gracias!

    ResponderEliminar