En este artículo, y los posteriores hasta completar una serie de varios, voy a hablar sobre las variables automáticas, y las variables persistentes. El manejo de la pila y el montón. Como se trabaja con estos dos almacenes de datos desde C++, como se hace desde la plataforma .Net, y como desde C++/CLI.
Los tipos de dato en C++/CLI
En C++/CLI tenemos acceso a los tipos de datos de C++ (int, long, char, etc.) y a los tipos de datos que proporciona la librería de clases base (BCL) de .Net (System.Int32, System.Long, System.Int16, System.String, etc.).
En C++ los datos pueden ser instancias de datos atómicos, o bien instancias de datos definidos por el usuario, estructuras (struct) o clases (class). Mientras que en .Net, todos los datos, son instancias de una estructura o de una clase. Dicho más claramente y con un ejemplo concreto. El tipo int de C++, es un tipo atómico. Mientras que el tipo System.Int32 de .Net (del cual instanciamos en C# con la palabra reservada int, o en Visual Basic mediante Integer), es una estructura. Con sus propiedades y métodos. De la misma forma, el tipo DateTime de .Net, también es una estructura, y el tipo String es una clase.
La pila y el montón de C++
En C++, a la hora de crear un dato variable, podemos hacerlo de dos formas, en cuanto a en que zona de memoria va a crearse, bien directamente en la pila, bien reservando memoria dinámicamente en el montón y obteniendo un puntero a dicha variable en la pila. Veamos más detenidamente ambos casos.
Supongamos que deseamos crear una variable de tipo entero en la pila. Procederíamos de la forma.
int nValor1;
El compilador reservara el espacio necesario para almacenar un valor numérico dentro del rango de los enteros. La memoria ocupada por la variable, será liberada cuando el ámbito en el cual la variable existe, finalice su ejecución.
Supongamos que deseamos crear la misma variable en el montón, de forma dinámica. En primer lugar, declararíamos una variable de tipo puntero al tipo de dato que nos interese, en este caso int. Posteriormente, mediante el operador new reservaremos memoria de forma dinámica en el montón, obteniendo el puntero a dicha zona de memoria. Como se ve en el siguiente código.
// Se reserva espacio en la pila para almacenar un puntero a entero
int *nValor2;
// Se crea una variable para almacenar un entero en el monton, y
// apuntamos a ella desde nValor2
nValor2 = new int;
// Guardamos el valor entero 20 en la memoria apuntada por nValor2
*nValor2 = 20;
La memoria ocupada por la variable nValor2, también será liberada cuando el ámbito de la misma finalice. Pero, recordemos, nValor2 es una variable puntero. Su contenido, es la dirección de memoria en la cual se encuentra alojada otra variable. Dicha variable, recordemos, alojada en el montón, es persistente, y su ciclo de vida durara desde su creación, hasta la finalización del programa. Por tanto, la liberación de la variable que se encuentra alojada en el montón, es responsabilidad directa, y única, del programador. Para lo cual deberá utilizar el operador delete. Veamos el siguiente código de ejemplo donde se nos ilustrara todo esto.
void FuncionEjemplo(int intParametro)
{
// ptrEntero es una varible local. Contendra la dirección de
// memoria de una variable de tipo entero. Cuando la ejecución
// de la funcion FuncionEjemplo finalice, la memoria
// ocupada por ptrEntero se liberara, pero no la ocupada
// por la variable a la que apunta.
int *ptrEntero;
// intResultado es una variable local. Su contenido sera
// un valor numerico entero. Cuando finalice la ejecución
// de la función FuncionEjemplo, la memoria ocupada por
// la variable intResultado sera liberada de forma automatica.
int intResultado;
// Aqui se crea una variable de tipo entero en el monton
// y se obtiene la dirección de memoria donde se crea, //guardandose
// en ptrEntero.
ptrEntero = new int;
// Vamos a sumar en contenido de la variable intParametro, con
// el contenido de la variable alojada en el monton a la cual
// apunta ptrEntero. Su resultado se alamacenara en
// intResultado.
intResultado = intParametro + *ptrEntero;
// Retornamos el contenido de intResultado y se finaliza la
// ejecución de la
// función FuncionEjemplo. Las variables intParametro, ptrEntero
// e intResultado
// seran eliminadas y la memoria que ocupan liberada de forma
// automatica.
// Pero no sera asi para la memoria apuntada por la variable
// ptrEntero, que se encuentra
// en el monton. Si el programador no se encarga de liberar
// dicha memoria, existira
// indefinidamente hasta la finalización del programa
return intResultado;
}
En el ambito de la anterior función, existen tres variables locales alojadas en la pila (a las variables que se alojan en la pila tambien se les conoce como variables automaticas. Debido a que su finalización se produce de forma automatica, al finalizar su ambito). Estas son, el parametro intParametro, y las variables locales ptrEntero e intResultado.
Dentro de la función se utiliza el operador new para construir en el monton una variable de tipo entero y guardar la dirección de memoria donde esta ha sido creada en la variable ptrEntero.
Cuando finaliza la ejecución de la función, todas la variables locales, y la variable encargada de recoger el parametros, son eliminadas y su memoria liberada. Pero la variable que se creo en el monton, mediante el operador new, no es liberada. Las variables almacenadas en el monton, son persistentes, y su liberación solo se produce cuando el programador utiliza el operador delete. Este es un error muy frecuente en las aplicaciones desarrolladas en C++. El programador “olvida” liberar la memoria ocupada por las variables persistentes. Como consecuencia de ello, tendremos una porción de la memoria ocupada innecesariamente. Y, ademas, dicha memoria es inaccesible, ya que, al finalizar la ejecución de la función, la variable puntero que contenia la referencia a la misma, fue eliminada. Por tanto, toda referencia a dicha variable ha sido eliminada.
Es muy frecuente en la programación en C++ crear las intancias de clases, objetos, en el monton. Ya que es mucho mas efectivo manejar referencias a los objetos (punteros) que a los objetos en si mismo. sobre todo en los pasos de parametro a funciones.
Tipos valor y tipos referencia en .Net
Cuando Sun Microsystem desarrollo el lenguaje Java, tubo en cuenta el problema de la creación y liberación de objetos en el monton. Como hemos visto en el apartado anterior, en el desarrollo de aplicaciones en C++, es muy frecuente crear las instancias de clase en el monton, y tambien es muy frecuente olvidarse de liberar la memoria ocupada por las variables alojadas en el monton.
Java es un lenguaje 100% orientado a objetos. Y sus diseñadores pensaron que las variables, todas, deberian ser instancias de clases, es decir, todos los tipos de datos deberian ser clases (al contrario que ocurre en C++, donde los tipos atomicos no son clases). Toda instancia de una clase (objeto/variable) debia crearse en el monton, y obtener una referencia al mismo en la pila. Pero querian liberar de la tarea de reservar y liberar memoria al programador. De forma que no se produjesen los mencionados problemas de consumo innecesario de memoria.
Para ello, la maquina virtual de Java, dispone de un proceso interno denominado recolector de basura que examina que objetos se encuentran en el monton, y cuales de ellos han dejado de ser referenciados desde la pila. Librando la memoria ocupada por estos objetos. De esta forma el lenguaje Java libera al programador de la tarea de liberar de forma manual a los datos persistentes.
Microsoft, al desarrollar la plataforma .Net, dio una vuelta de tuerca a esta idea. Como hemos dicho, es habitual en C++ crear las intancias de clase en el monton. Pero no lo es la creación de variables de tipos atomicos. Mientras que en Java todas las variables se crean en el monton, Microsoft quiso que el desarrollo de una aplicación .Net se aproximara mas a como actuaria un programador C++. De forma que, los datos de “poco peso” se creasen en la pila, y los datos mas “pesados” se creasen en el monton.
Por ello, en la plataforma .Net existen datos tipo valor y tipo referencia. Los datos tipo valor son aquellos que cuando los creamos se alojan en la pila (denominada pila administrada) y los tipo referencia se crean en el monton (llamado monton administrado). Concretamente, toda variable instanciada de una estructura, es un tipo valor (se creara en la pila) y toda variabla instanciada de una clase, es de tipo referencia (se creara en el monton). Poniendo un ejemplo concreto, el tipo System.Int32 es una estructura.
Así que cuando en C# creamos una variable de tipo int o en Visual Basic creamos una variable de tipo Integer, estamos instanciando un dato de la estructura System.Int32 de la BCL (librería de clases base), y por tanto la variable se alojara en la pila.
Por el contrario el tipo System.String es una clase. Asi que cuando creamos una variable de tipo string estamos alojandola en el monton.
Hasta aquí, he comentado como se maneja la pila y el montón desde C++. Cual es el proposito de alojar variables en uno u otro almacen de datos. Que errores son los mas comunes en este tipo de tareas, y como la plataforma .Net pretende solucionar este tipo de errores mas comunes.
En el proximo articulo veremos como trabajamos con estos almacenes de datos con C++/CLI y que diferencias y similitudes tenemos con respecto al C++ tradicional y con el resto de lenguajes de la plataforma .Net.
Suscribirse a:
Enviar comentarios (Atom)
No hay comentarios:
Publicar un comentario