Bienvenidos sean a este post, hoy veremos un tema muy visagra en este lenguaje.
Antes de iniciar hablemos breveremos sobre la memoria. La memoria se divide en segmentos, estos almacenan la información que vamos utilizando en el programa, este segmento asignado a su vez tiene una ubicación que se llama dirección de memoria y el tamaño de la misma va a depender del tipo de información que almacenemos. Si es un dato de tipo unsigned long int seria de 4 bytes, o sea 32 bits, esto sera reservado por el sistema y ese segmento va a tener una dirección y un tamaño dependiendo de la data, y así con cada tipo de dato. Ahora bien, si utilizaramos un lenguaje de muy bajo nivel (assembler o código maquina) estos datos seran importantisimos pero para nuestro caso no es necesario, no tenemos tanta injerencia en la misma dado que el compilador es el que se encarga de administrar las direcciones de memoria y recursos de la misma. Un vez explicado de forma muy breve lo que es la memoria y como funciona la misma diremos que los punteros son un tipo de variable que almacena la dirección de memoria de la información de otra variable. Veamos un ejemplo de como se utiliza esto:
# include <iostream>
int main()
{
unsigned short peqVar = 5;
unsigned long lrgVar = 65535;
long sVar = 65535;
std::cout << "Valor de la variable tipo short sin signo: \t";
std::cout << peqVar << std::endl;
std::cout << "Direccion de la variable tipo short: \t";
std::cout << &peqVar << std::endl;
std::cout << "Valor de la variable tipo long sin signo: \t";
std::cout << lrgVar << std::endl;
std::cout << "Direccion de la variable tipo long: \t";
std::cout << &lrgVar << std::endl;
std::cout << "Valor de la variable topo long con signo: \t";
std::cout << sVar << std::endl;
std::cout << "Direccion de la variable tipo long con signo: \t";
std::cout << &sVar << std::endl;
return 0;
}
Este es un codigo simple donde primero definimos tres variables de distintos tipos y distintos valores luego mostraremos cada uno de los valores asignados, seguido de la direccion de memoria de la variable donde esta contenido el valor. Para ello, anteponemos el operador de direccion (&) en cada variable. Si lo compilan y ejecutan tendran una salida semejante a esta:
$ ./apuntar
Valor de la variable tipo short sin signo: 5
Direccion de la variable tipo short: 0x7ffd09ddfaee
Valor de la variable tipo long sin signo: 65535
Direccion de la variable tipo long: 0x7ffd09ddfae0
Valor de la variable topo long con signo: 65535
Direccion de la variable tipo long con signo: 0x7ffd09ddfad8
$
Nota:
Las direcciones de memoria pueden diferir a la de ustedes.
Se logro lo que explicamos anteriormente. Ya vimos que con este operador podemos obtener la direccion de memoria pero como podemos utilizarlo? Tomemos del ejemplo anterior una variable y definamos el siguiente puntero:
long * psVar = NULL;
Esto es la definicion mas basica de un puntero. Siempre debe ser del mismo tipo que la variable donde apuntara. El asterisco (*) es el encargado de indicar que esta variable sera un puntero y lo iniciamos con el valor NULL. Por otro lado, se los denomina al inicio como p para indicar que es un puntero. Tanto la forma de iniciacion como el nombre son consideradas buenas practicas pero no son obligatorios. Si agregaramos el puntero seria de la siguiente manera:
long sVar = 65535;
long * psVar = NULL;
psVar = &sVar;
En el caso anterior podriamos haber omitido la asignacion de NULL porque ya tenemos un valor para la variable donde apuntaremos. Es decir podriamos haber hecho esto:
long sVar = 65535;
long * psVar = &sVar;
Vamos a trabajar con los punteros y los valores y para ello vamos a analizar el siguiente ejemplo:
# include <iostream>
typedef unsigned short int USHORT;
int main()
{
USHORT var;
USHORT * pVar = NULL;
std::cout << "Ingresa un valor: ";
std::cin >> var;
pVar = &var;
std::cout << "var = " << var << std::endl;
std::cout << "pVar = " << *pVar << std::endl;
std::cout << "Ingresa otro valor: ";
std::cin >> *pVar;
std::cout << "var = " << var << std::endl;
std::cout << "pVar = " << *pVar << std::endl;
return 0;
}
El typedef al inicio es simplemente para crear un alias y facilitarnos el tipo para la creacion de la variable y el puntero. Luego en el main declaramos una variable. Seguido a esto un puntero que por el momento sera nulo. Lo siguiente es solicitar que ingresemos un valor y lo almacenaremos en la variable. Con el valor ingresado en la variable almacenaremos la direccion de memoria de la variable en el puntero. El siguiente paso sera mostrar el valor tanto de la variable como del puntero, para el segundo caso usamos el asterisco para indicar que queremos el valor donde esta apuntando y no su direccion de memoria. Lo siguiente sera ingresar nuevamente otro valor pero esta vez lo almacenaremos en el puntero en lugar de la variable. Si observan, volvemos a usar el asterisco para indicarle que lo almacenaremos en la direccion de memoria donde apunta y finalmente mostramos los valores tanto de la variable como el puntero. Que creen que sucedera? Bueno, compilemos y veamos como se comporta:
$ ./apuntar
Ingresa un valor: 10
var = 10
pVar = 10
Ingresa otro valor: 22
var = 22
pVar = 22
$
Analicemos esta salida. En el primer caso, al ingresar un valor a la variable y luego asignarla al puntero nos mostro los mismos valores porque los dos apuntan a la misma direccion. En el segundo caso, ocurre lo mismo porque si bien nosotros usamos el puntero, este ira a la direccion donde esta apuntando. Por lo tanto, modificara la direccion de memoria de la variable y afectara a esta. En un ejemplo tan basico no tiene razon de ser pero cuando nos codigos sean mas complejos, y lo seran, veran que trabajar con punteros nos facilitara mucho las cosas y especialmente en el ahorro de memoria.
Esto es la introduccion a que es un puntero pero ahora pasaremos a hablar sobre su utilizacion. Los punteros son usados especialmente para tres procedimientos:
- Manejar datos en el heap
- Tener acceso a los datos miembro y a las funciones de las clases
- Pasar variables por referencia a las funciones
Pasemos a hablar sobre el primer procedimiento. Antes de hablar sobre el heap debemos mencionar como se compone una funcion en la memoria:
- Espacio de nombres global
- Heap
- Registros
- Espacio de codigo
- Stack
En Stack se guardan las variables locales de la función, en el espacio de código ira el código del mismo, en Espacio de nombre global se guardan las variables que van a ser globales, los registros se utilizan para el mantenimiento interno de las funciones, el registro de la parte inferior de la pila y del apuntador de instrucciones y finalmente el resto de la memoria queda para el heap.
Como hemos visto hasta ahora las variables locales solamente estan dentro de la función, y ahora sabemos que se manipulan en la pila o stack y cuando termina la misma esta es eliminada, ante una necesidad dicho valor no la vamos a tener a disposición, una solución a esto es que se utilice una variable global pero esto nos traeria el inconveniente que cada vez que necesitamos generar una variable nueva debemos irnos al principio y declararla esto también conlleva de que el código puede quedar confuso y dificil de mantener y/o depurar (en el caso de que sea muy largo) una solución para estos inconvenientes seria guardar dicha data en el heap. Esto nos permitiria tener acceso a esa información y que tampoco la misma este disponible para el resto del programa como si fuera global. Esto nos ayuda a llevar un mejor control de la información que vamos manipulando en el programa. Todo esto se logra mediante la reserva de espacio en el heap para que pueda almacenar dicho valor, y despues con el apuntador saber de donde extraer dicha información. Lo bueno de este tipo de practica es que esta información va a estar disponible hasta que se libere del heap, como dijimos anteriormente la pila o stack se limpia cuando la función termina y en este caso perdemos el valor generado por la misma, lo malo del otro método es que nosotros debemos limpiar dicha data cuando ya no sea necesaria.
Esto nos ayuda, como dijimos anteriormente, evitar el uso de variables globales que pueden ser accedidas por todo el programa, y con los apuntadores pueden ser accedidos desde las funciones donde fueron generados. Para crear y eliminar el espacio en el heap, se utilizan 2 comandos: new y delete. new es el comando para crear el espacio en el heap, básicamente es para crear la reserva y declarar el tipo de valor que se va a almacenar en esa porción de memoria. En el caso de delete lo que se hace es liberar ese espacio reservado, el único detalle que deben tener en cuenta nunca liberar 2 veces el mismo espacio porque esto provocaria que el programa termine, para evitar esto por cada vez que se libera el espacio se le deberia asignar un valor nulo al apuntador. Pasemos a ver la sintaxis de los mismos y lo comentado anteriormente:
unsigned short int * pPuntero;
pPuntero = new unsigned short int;
unsigned short int * pPuntero = new unsigned short int;
Estas son las dos formas posibles de utilizar a new en un puntero. Veamos como es la sintaxis de delete:
delete pPuntero;
Ya vimos como se puede asignar un espacio en el heap y como eliminar, todo en la teoria, pero vamos a ponerlo en practica mediante el siguiente ejemplo y veremos unas particularidades mas:
# include <iostream>
class Gato
{
public:
Gato();
~Gato();
int ObtenerEdad() const { return *suEdad; }
void AsignarEdad(int edad) { *suEdad = edad; }
int ObtenerPeso() const { return *suPeso; }
void AsignarPeso(int peso) { *suPeso = peso; }
private:
int * suEdad;
int * suPeso;
};
Gato::Gato()
{
std::cout << "Iniciando valores en Gato..." << std::endl;
suEdad = new int(2);
suPeso = new int(5);
}
Gato::~Gato()
{
std::cout << "Borrando valores en Gato..." << std::endl;
delete suPeso;
delete suEdad;
}
int main()
{
Gato * Pelusa = new Gato;
std::cout << "Pelusa tiene " << (*Pelusa).ObtenerEdad();
std::cout << " años de edad y pesa ";
std::cout << (*Pelusa).ObtenerPeso() << " kgs.\n";
Pelusa -> AsignarEdad(5);
std::cout << "Pelusa tiene " << Pelusa -> ObtenerEdad();
std::cout << " años de edad.\n";
delete Pelusa;
return 0;
}
Lo primero que haremos sera definir una clase para crear nuestros objetos. En la parte publica, tenemos dos prototipos para el constructor y destructor de la clase. Luego una serie de metodos para distintas tareas:
- ObtenerEdad(), es para obtener y devolver el valor del puntero suEdad
- AsignarEdad(int edad), este es para asignar un valor al puntero suEdad
- ObtenerPeso(), es para obtener y devolver el valor del puntero suPeso
- AsignarPeso(int peso), este es para asignar un valor al puntero suPeso
Si observan en los metodos para obtener el valor de los punteros utilizamos la palabra const. Esta es para evitar que el valor se modifique accidentalmente de alguna forma y solo pueden devolver el valor que encuentran. En la parte privada declaramos a los punteros que trabajaremos con los metodos anteriores.
Lo primero que haremos sera definir al constructor de la clase anterior. En ella primero indicamos que se iniciaran los valores de la clase. Observen que llamamos directamente al nombre del puntero, sin asterisco, en ambos casos usamos a new seguido del mismo tipo de estos y en ellos le pasaremos un valor para que sea el valor inicial de cada puntero.
Luego definimos al destructor. Aqui nuevamente mencionamos la accion que realizaremos y luego mediante delete eliminaremos a cada puntero creado con new en el constructor. Pasemos a hablar sobre el main.
Primero crearemos un objeto de la clase anterior pero con una curiosidad. En este caso, el objeto sera un puntero y usamos el new para crear el espacio en memoria, recuerden que no debemos preocuparnos porque el compilador manejara la asignacion por nosotros, y lo siguiente sera mostrar los valores de las propiedades (los punteros con los datos) mediante los metodos correspondientes. Aqui usaremos dos formas de poder utilizar los metodos. Analicemos el primero:
(*Pelusa).ObtenerEdad()
Esta es la primera forma donde indicamos entre parentesis el puntero que usaremos para poder llamar al metodo que contiene. La segunda forma es la siguiente:
Pelusa -> ObtenerEdad()
En este caso utilizamos directamente el nombre, seguido del operador de funcion y luego el nombre del metodo/funcion que deseamos llamar. Tambien usamos el metodo de AsignarEdad para modificar y finalmente mostrar el nuevo valor. Compilemos y veamos como es la salida:
$ ./apuntar
Iniciando valores en Gato...
Pelusa tiene 2 años de edad y pesa 5 kgs.
Pelusa tiene 5 años de edad.
Borrando valores en Gato...
$
Como podemos ver no fue necesario asignar una direccion de memoria de una variable sino que mediante la funcion new nos creo un espacio para el tipo de dato y a su vez le asignamos un valor inicial. Como podemos ver en la segunda linea. En la tercer linea vemos como el uso de AsignarEdad cambio el valor del puntero. Y finalmente, vemos el llamado al destructor que se encargara de eliminar los punteros creados en el heap. Pero esto que vimos tiene un secreto y es un puntero oculto llamado this. Este se encarga de manejar cada elemento de manera personal y si bien en otros lenguajes lo debemos usar, en este no es necesario y el mismo compilador se encargara de asignarlo por nosotros. Pasemos a comentar sobre otro tema sobre los punteros.
Un error muy común es la aparición de punteros perdidos o deambulantes, estos aparecen como dijimos cuando el puntero es eliminado pero no se le asigna ningún valor NULL, dado que se puede volver a borrar el mismo o volver a utilizarlos y esto genera un resultado impredecible por ende, un error aleatorio en el programa, donde minimamente finalizara el mismo sin algún tipo de error. Otro tipo de error que nos puede suceder es que el programa no devuelva los valores correctos del mismo, esto es debido a que con el comando delete nosotros le decimos al programa libera la dirección de memoria del puntero pero no le decimos que borre dicha información y al generar otro puntero y estar liberada esa dirección de memoria la misma se puede mezclar con el nuevo valor dando uno completamente distinto (recuerden que en la memoria se maneja con código binario, no con el valor en si) por eso siempre que se libere (o elimine) un puntero se debe asignarle un valor NULL para que esa dirección de memoria no tenga ninguna valor y no se mezcle con algún valor nuevo.
La palabra reservada const puede ser utilizada con los punteros, este puede estar en cualquier parte de la declaración pero dependiendo donde esta va a tener un significado u otro. Veamos un ejemplo de sintaxis:
const int * pUno;
int * const pDos;
En el primer caso seria para que el valor donde apuntamos es de tipo constante pero podriamos modificar la direccion donde apunta. En cambio, la segunda opcion es utilizada para que el puntero sea constante y no pueda apuntar a ningun otro elemento. Esta segunda opcion es mas interesante para utilizarla cuando hacemos metodos constantes, como vimos en la clase Gato.
En resumen, hoy hemos visto a punteros, que son, para que sirven, como se utilizan, asi como tambien algunos ejemplos practicos donde los utilizammos. Espero les haya resultado de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donatión
It’s for site maintenance, thanks!
$1.50
