Bienvenidos sean a este post, hoy vamos a hablar de los apuntadores, espero poder expresarlo de una forma que podamos entenderlo. Sin mas preámbulos empecemos.
Brevemente, comencemos con una explicación de 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, 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 apuntadores 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:

apunt.cpp

# include <iostream>

using namespace std;

int main()
{
        unsigned short peqVar = 5;
        unsigned long lrgVar = 65535;
        long sVar = 65535;
        cout << "Valor de la variable tipo short sin signo: \t";
        cout << peqVar << endl;
        cout << "Direccion de la variable tipo short: \t";
        cout << &peqVar << endl;
        cout << "Valor de la variable tipo long sin signo: \t";
        cout << lrgVar << endl;
        cout << "Direccion de la variable tipo long: \t";
        cout << &lrgVar << endl;
        cout << "Valor de la variable topo long con signo: \t";
        cout << sVar << endl;
        cout << "Direccion de la variable tipo long con signo: \t";
        cout << &sVar << endl;
        return 0;
}

Si compilamos nuestro programa y lo ejecutamos obtendremos la siguiente salida:

tinchicus@dbn001vrt:~/programacion/c++$ ./program/apunt
Valor de la variable tipo short sin signo: 5
Direccion de la variable tipo short: 0xbfc3438e
Valor de la variable tipo long sin signo: 65535
Direccion de la variable tipo long: 0xbfc34388
Valor de la variable topo long con signo: 65535
Direccion de la variable tipo long con signo: 0xbfc34384
tinchicus@dbn001vrt:~/programacion/c++$

Como se ve primero mostramos el valor de la variable y en la linea inferior se ve la dirección de memoria donde esta almacenado el valor de la variable (el valor de memoria puede variar dependiendo el equipo), la misma se muestra si se le antepone el ampersand (&), también conocido como operador de dirección, antes de la variable para mostrar la dirección de memoria. Para guardar dicha dirección en una variable se debe declarar de la siguiente forma:

 int * apEdad = NULL; 

Como ven el asterisco (*) es el indicador en la declaración de que la variable apEdad va a ser un apuntador, la nomenclatura que se acepta mas habitualmente (en lengua castellana) es declararlo con ap (apuntador) al principio, en ingles se utiliza la letra p (pointer), digo estos 2 ejemplos porque seguramente son los que mas se veran por estas latitudes. Vamos a ver una simple declaración de apuntador y como se le asigna un valor:

unsigned short int queTanViejo = 50;
unsigned short int * apEdad = NULL;
apEdad = &queTanViejo;

Supongamos que como en el caso anterior nosotros ya tenemos declarado el valor de la variable, entonces en ese caso no es necesario declararlo como NULL, sino que simplemente ya se lo asignamos:

unsigned short int queTanViejo = 50;
unsigned short int * apEdad = &queTanViejo;

Ahora pasemos a explicar que utilidad tiene el asterisco (*), este es un operador de indireccion u operador de desreferencia, como dijimos anteriormente el apuntador va a tener un valor de dirección de memoria pero también va a ser capaz de tener el valor almacenado en dicha dirección, supongamos que tenemos que asignar a suEdad el valor de queTanViejo, de la forma mas habitual seria:

unsigned short int suEdad;
suEdad = queTanViejo;

Ahora supongamos que queremos efectuar lo mismo pero con el apuntador apEdad:

unsigned short int suEdad;
suEdad = *apEdad;

Como se ve, primero se pone el asterisco (*) esto dice al programa que debe utilizar el valor que esta almacenado en el  apuntador. La expresión que se utiliza para referenciar a este signo es “el valor guardado en “. La expresión seria, “tomar el valor guardado en la dirección apEdad y asignarselo a la variable suEdad“. Veamos como se manipula el valor de una variable atraves de un apuntador por medio del siguiente ejemplo:

apunt00.cpp

# include <iostream>

using namespace std;

typedef unsigned short int USHORT;

int main()
{
        USHORT miEdad;
        USHORT * apEdad = NULL;
        miEdad=5;
        cout << "miEdad: " << miEdad << endl;
        apEdad=&miEdad;
        cout << "apEdad: " << *apEdad << "\n\n";
        cout << "*apEdad = 7 \n ";
        *apEdad = 7;
        cout << "*apEdad: " << *apEdad << endl;
        cout << "miEdad: " << miEdad << "\n\n";
        cout << "*apEdad = 9 \n ";
        *apEdad = 9;
        cout << "*apEdad: " << *apEdad << endl;
        cout << "miEdad: " << miEdad << endl;
        return 0;
}

Como se ve en el ejemplo, el valor de miEdad se modifica a través del apuntador apEdad, donde se puede ver que va a recibir 2 valores distintos (*apEdad = 7 y *apEdad = 9) lo cual hace que se modifique el valor en esa dirección de memoria por ende también varia el valor de miEdad. Compilemos y ejecutemos el programa para ver su salida:

tinchicus@dbn001vrt:~/programacion/c++$ ./program/apunt00
miEdad: 5
apEdad: 5
*apEdad = 7
*apEdad: 7
miEdad: 7
*apEdad = 9
*apEdad: 9
miEdad: 9
tinchicus@dbn001vrt:~/programacion/c++$

Recuerden que no importa lo que suceda siempre que se le asigne el valor de la dirección de memoria al apuntador, se le va a asignar la dirección correcta confíen plenamente en el lenguaje y en el compilador. Ahora que nos familiarizamos con la sintaxis pasemos  al uso de apuntadores, su utilización es para estos 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.

En este post seguiremos con los dos primeros y el ultimo quedara para un próximo post, pasemos al primer punto la pila y el heap.

Creo que esto no lo explique pero vamos a verlo, las funciones se dividen en 5 secciones de memoria:

  • Espacio de nombres global
  • Heap
  • Registros
  • Espacio de codigo
  • Pila

En Pila se guardan las variables locales de la función, en el espacio de código 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 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 la libere del heap, como dijimos anteriormente la pila 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 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:

new

unsigned short int * apApuntador;
apApuntador = new unsigned short int;

o sino

unsigned short int * apApuntador = new unsigned short int;

delete

delete apApuntador;

Para evitar el inconveniente que mencionamos anteriormente:

Animal *apPerro = new Animal;
delete apPerro;
apPerro = NULL;
.....
delete apPerro;   // En este caso no hace ningun error en el programa.

Ahora veamos un ejemplo de como se manipula los apuntadores para el heap:

apunt01.cpp

# include <iostream>

using namespace std;

int main()
{
        int variablelocal = 5;
        int * apLocal = &variablelocal;
        int * apHeap = new int;
        *apHeap = 7;
        cout << "variablelocal: " << variablelocal << endl;
        cout << "*apLocal: " << *apLocal << endl;
        cout << "*apHeap: " << *apHeap << endl;
        delete apHeap;
        apHeap = new int;
        *apHeap = 9;
        cout << "*apHeap: " << *apHeap << endl;
        delete apHeap;
        return 0;
}

Como ven siempre que se va a cambiar la información almacenada en el heap se debe liberar primero para evitar fugas de memoria dado que si no hubiera existido el delete se hubiera generado otra reserva de memoria para la variable apHeap y esto nos hubiera quitado espacio para futuras reservas, la practica correcta es como se ve en el ejemplo, antes de modificar la misma se debe eliminarla y volverla a declarar para que se genere correctamente. Asi como vimos anteriormente la creación de un tipo de variable en el heap, también se puede generar reserva para objetos, la sintaxis es exactamente la misma, veamos con el ejemplo que utilizamos para ver clases y objetos. En ese caso creamos un objeto llamado Gato, veamos como seria reservarle un espacio en el heap:

Gato *apGato = new Gato;

La eliminación de objetos se hace también a través de delete, cuando se llama a delete también se llama al destructor de ese objeto para que libere la memoria de la pila. Veamos un breve ejemplo:

apunt02.cpp

# include <iostream>

using namespace std;

class GatoSimple
{
public:
        GatoSimple();
        ~GatoSimple();
private:
        int suEdad;
};

GatoSimple::GatoSimple()
{
        cout << "Se llamo al constructor.\n";
        suEdad = 1;
}
GatoSimple::~GatoSimple()
{
        cout << "Se llamo al destructor.\n";
}

int main()
{
        cout << "GatoSimple Pelusa ... \n";
        GatoSimple Pelusa;
        cout << "GatoSimple *apFelix = new GatoSimple ... \n";
        GatoSimple *apFelix = new GatoSimple;
        cout << "delete apFelix ... \n";
        delete apFelix;
        cout << "saliendo, observe como se va Pelusa.... \n";
        return 0;
}

Pasemos a probarlo, compilemos y ejecutemos nuestro programa:

tinchicus@dbn001vrt:~/programacion/c++$ ./program/apunt02
GatoSimple Pelusa …
Se llamo al constructor.
GatoSimple *apFelix = new GatoSimple …
Se llamo al constructor.
delete apFelix …
Se llamo al destructor.
saliendo, observe como se va Pelusa….
Se llamo al destructor.
tinchicus@dbn001vrt:~/programacion/c++$

Si seguimos el programa, la primera vez se llama al constructor cuando se crea el objeto, y luego es llamado nuevamente para crearlo en el heap, ahora para liberarlo se hace una sola vez a través del comando delete que primero lo libera del heap y luego de la pila como venia siendo normalmente pero este proceso es de orden automatico. Como vimos en el post de clases y objetos, cuando declaramos un objeto mediante el operador de punto podemos acceder a los datos miembro y sus funciones. Los datos del objeto que estan cargados en el heap se deben desreferenciar con el asterisco y parentesis, un ejemplo de sintaxis seria asi:

(*apFelix).ObtenerEdad();  // El apuntador del caso anterior con una función de ejemplo.

Un operador de método abreviado para esta sintaxis es el signo menos (-) seguido del signo mayor (>), veamos la sintaxis equivalente:

apFelix -> ObtenerEdad();

Ahora veamos como se manipulan los datos miembro de una clase en el heap y también vamos a ver la sintaxis antes explicada:

apunt03.cpp

# include <iostream>

using namespace std;

class GatoSimple
{
public:
        GatoSimple();
        ~GatoSimple();
        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;
};

GatoSimple::GatoSimple()
{
        suEdad = new int(2);
        suPeso = new int(5);
}

GatoSimple::~GatoSimple()
{
        delete suPeso;
        delete suEdad;
}

int main()
{
        GatoSimple * Pelusa = new GatoSimple;
        cout << "Pelusa tiene " << (*Pelusa).ObtenerEdad();
        cout << " años de edad.\n";
        Pelusa -> AsignarEdad(5);
        cout << "Pelusa tiene " << Pelusa -> ObtenerEdad();
        cout << " años de edad.\n";
        delete Pelusa;
        return 0;
}

Compilemos y probemos nuestro programa, la salida es la siguiente:

tinchicus@dbn001vrt:~/programacion/c++$ ./program/apunt03
Pelusa tiene 2 años de edad.
Pelusa tiene 5 años de edad.
tinchicus@dbn001vrt:~/programacion/c++$

En este ejemplo se ve como se usa espacio en el heap para datos miembros de una clase como se genera un apuntador, como puede ser llamado (se ven las 2 sintaxis validas) y como se elimina el apuntador y este también destruye el objeto y los datos cargados en el heap. Ahora vamos a hablar de this, este es un apuntador oculto que tienen todas las funciones, en otros lenguajes yo lo utilizo explicitamente para que el programa cuando necesita ejecutar un metodo en un objeto le informa que este es el elemento y de ahi ejecutar el metodo propio. Mas adelante en otro post voy a ahondar un poco mas en esto (this) ya que creo que seria demasiado complicado de aplicarlo ahora pero es bueno dejar escrito que este es un apuntador oculto que genera el compilador por cada funcion.

Un error muy común es la aparición de apuntadores perdidos o deambulantes, estos aparecen como dijimos cuando el apuntador 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 apuntador pero no le decimos que borre dicha información y al generar otro apuntador 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 apuntador 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 apuntadores, 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 * apUno; // Aca se declara que el tipo de valor apuntador es constante.

int * const apDos; // y aqui apDos es constante y no se puede apuntar a ninguna otro cosa.

Como vimos en clases y objetos, las funciones miembro de una clase pueden ser definidas como const y por ende los valores de estas funciones no puede ser modificadas, por lo que para declarar un apuntador a este tipo de funciones también debe hacerlo como const.

En resumen, hoy visto como es la memoria, como esta dividida, que son los apuntadores, como utilizarlos, como trabajan, como utilizarlos con clases y objetos, espero les haya sido util sigueme en Twitter, Facebook o Google+ para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.

Anuncios