INICIACIÓN AL MANEJO DE LOS PUNTEROS
INTRODUCCIÓN
Debido al miedo general que provocan los punteros y, la mala información y explicación en la mayoría de talleres que corren por Internet e incluso en algunos libros, me dispongo a crear este pequeño texto intentando explicarlos lo mejor que pueda ya que cuesta mucho encontrar información decente.
MANEJANDO LOS PUNTEROS
¿Qué es un puntero? Un puntero es una variable que hace referencia a una dirección de memoria, como puede ser 0x000F212A, en hexadecimal. Un ejemplo es:
Código
int a; //Declaración de una variable entera int *ptr; //Declaración de un puntero
Pero es mejor así:
Código
int a = 0; //Declaración de una variable entera int *ptr = NULL; //Declaración de un puntero
Otros ejemplos:
Código
char a = 0; //Declaración de una variable de caracteres char *ptr = NULL; //Declaración de un puntero
Ya hemos visto la declaración de una variable normal y la de un puntero, pero nos preguntamos, ¿y por qué de la segunda manera? Al hacer esto, inicializamos las variables con un valor predeterminado en cambio de la primera forma las variables tienen un valor indeterminado y pueden contener, como llamamos en programación, basura. Recordemos que NULL es una constante declarada en los archivos de cabecera estándar y su valor es 0 o 0x00000000.
Antes de continuar, aclaremos dudas. Hemos visto que un puntero almacena una dirección de memoria como 0x000F212A o NULL y, también hemos dicho que si no sabemos a qué apuntar le asignamos NULL, pero claro, tener una dirección inválida es una estupidez, pero si hacemos por ejemplo:
Código
a = 1; ptr = &a;
El operador "&" obtiene la dirección de memoria de una variable cuando se aplica a esta. Por lo tanto:
a = 1 => 0x000F212A (imaginemos que tiene esta dirección de memoria).
ptr = &a => 0x000F212A (consecuentemente, aplicando este operador obtenemos la misma dirección de memoria).
Podríamos decir que ptr "apunta a" a. Ahora, para comprobar lo que acabamos de hacer:
Código
std::cout << "a= " << a << std::endl; std::cout << "ptr= " << *ptr << std::endl;
Lo que nos mostrará en pantalla será:
Código:
a= 1
ptr= 1
Vemos que sale lo mismo. El operador "*" aplicado a una variable de puntero obtiene el valor almacenado en una dirección de memoria indicada por este. Y además, podemos hacer lo siguiente:
Código
*ptr = 2;
Estamos haciendo que el contenido apuntado por ptr almacene ahora: 2. O sea, como ptr apunta a a, es como hacer:
Código
a = 2;
Luego, si hacemos:
Código
std::cout << "a= " << a << std::endl; std::cout << "ptr= " << *ptr << std::endl;
Lo que nos mostrará ahora en pantalla es:
Código:
a= 2
ptr= 2
ARREGLOS Y PUNTEROS
Un ejemplo de arreglo sería:
Código
int x[4] = {11, 12, 13, 14};
Para este arreglo se le asigna una memoria contigua de la siguiente manera:
A x se le asigna la dirección de memoria del primer elemento del arreglo, en este caso: 11. Ahora vamos a crear un puntero al arreglo y a enseñar lo que pasa, jugando con el puntero:
Código
int *ptr = NULL; ptr = x; std::cout << *ptr << std::endl; ptr++; std::cout << *ptr << std::endl; ptr++; std::cout << *ptr << std::endl; ptr++; std::cout << *ptr << std::endl;
Lo que nos mostrará en pantalla será:
Código:
11
12
13
14
Antes de empezar, una cosa importante: para que un puntero apunte a un arreglo, no hace falta que tenga el operador "&", ya que un arreglo es un puntero constante y por la misma razón no se puede tampoco incrementar un arreglo.
Bien, ptr apunta al primer elemento del arreglo, en el cual su dirección de memoria es 0x0012FF60. Lo mostramos por pantalla y, lógicamente como hemos puesto el operador "*" antes de ptr nos saldrá el contenido almacenado en esta posición de memoria: 11. Seguidamente, hacemos un postincremento al puntero, por lo tanto aumentará la dirección de memoria y apuntará al segundo elemento del arreglo; la dirección de memoria será 0X0012FF64, recordemos que un entero ocupa 4 bytes. Luego mostramos el contenido de esta dirección de memoria: 12. Hacemos esto hasta llegar al último elemento.
MEMORIA DINÁMICA
Comprendiendo todo lo explicado hasta ahora nos sirve para comprender el funcionamiento y las características más básicas de los punteros, ahora bien, entrando en el sector de la memoria dinámica, profundizamos y obtenemos una mayor potencia de estos mismos. Recordemos que se crea un puntero de la siguiente manera:
Código
int *ptr = NULL;
Recordemos también, que al hacer esto el puntero apunta a una dirección inválida de memoria por lo que hacer:
Código
*ptr = 1;
Sería inválido. Si queremos reservar memora exclusivamente para que nuestro puntero apunte a algo lo hacemos con el operador new. Por ejemplo:
Código
int *ptr = new int;
Con el operador new reservamos memoria dinámica y, se le pasa como operando el tipo de variable a reservar. Luego de esto, se retorna la dirección de esta memoria reservada, por lo que necesitamos obligatoriamente un puntero para acceder a esta memoria.
Pero, al igual que reservamos memoria dinámica, también la podemos borrar o destruir. Y es obligatorio borrar toda esta memoria reservada ya que si no podemos ocasionar un agotamiento de memoria. Por ejemplo:
Código
int *ptr = new int; delete ptr;
Reservamos memoria exclusivamente para el puntero con el operador new y, la eliminamos al final con el operador delete (el nombre del puntero es el que debe acompañar a delete).
ARREGLOS DINÁMICOS
Ya hemos visto el funcionamiento de la memoria dinámica y cómo utilizarla pero, aún podemos sacarle más jugo al tema. La aplicaremos a los arreglos. Un ejemplo de arreglo utilizando dinámico:
Código
int *ptr = new int[5];
En este caso, reservamos cinco celdas consecutivas, o sea un arreglo y, cada de una de ellas tiene la memoria de un entero, 4 bytes.
Código
for(int n = 0; n < 5; n++) std::cout << ptr[n] << std::endl;
Así ya accedemos a los elementos del arreglo.
La ventaja de utilizar arreglos dinámicos es que puedes establecer su tamaño en tiempo de ejecución, justo al contrario que los arreglos normales. Para establecer el tamaño de un arreglo dinámico, se puede hacer por ejemplo:
Código
int n = 0; std::cin >> n; int *ptr = new int[n];
De la misma forma que se puede eliminar la memoria dinámica normal, también se puede eliminar los arreglos dinámicos. Un ejemplo sería:
Código
int *ptr = new int[5]; delete[] ptr;
La única diferencia con el delete de antes es que sólo se le añaden estos signos "[]" después del delete.
DESPEDIDA
Aquí termina este escrito y, espero que me haya explicado bien y que os haya servido para comprender un poquito más los punteros. Podéis contactar conmigo para cualquier duda que tengáis o simplemente, comentarlo acá.
Saludos