Una de las funciones de los apuntadores que ya hemos visto durante las notas anteriores es que almacenan direcciones de memoria. Es momento de conocer su otra funcionalidad, esto es, poder utilizar esa misma dirección para acceder al valor que contiene la variable o constante a la que nos estamos refiriendo.
Como seguro recuerdas, el símbolo asterisco actúa como un operador que nos permite declarar un apuntador, así también, es este mismo operador el que nos permitirá acceder al valor que almacena la variable o constante en cuestión.
Vamos a conocer entonces cómo realizar lo anterior y también daremos un vistazo a los arreglos para así poner en práctica lo que has aprendido hasta el momento.
El operador de indirección
En este apartado, el símbolo (*) se conoce con el nombre de operador de indirección (aunque también es posible encontrarlo como operador de desreferencia o, en inglés, dereference operator). Este operador se coloca inmediatamente a la izquierda de algún apuntador que nosotros elijamos y, de esta manera, estaremos pidiendo que se modifique o se acceda a un valor.
Veamos un ejemplo, para esto declaremos e inicialicemos una variable de tipo entero junto con un apuntador que nos permita referirnos a dicha variable:
int var = 4; int *ptr;
Enseguida guardemos la dirección:
ptr = &var;
Y, por último, cambiemos el valor de la variable por medio de nuestro apuntador (en este caso sí que será necesario colocar el operador de indirección junto con el identificador):
*ptr = 7; // Esto es equivalente a realizar: var = 7;
Utilizando el operador de indirección modificamos el valor de la variable a la que el apuntador se está refiriendo, si no tuviera este operador (y como ya se ha comentado), estaríamos modificando el valor del apuntador mismo, es decir, estaríamos cambiando la dirección que almacena, es muy importante tener esto en cuenta para evitar errores en nuestros programas.
Para comprobar que el cambio ha funcionado mostremos en pantalla el valor de la variable:
cout << "valor: " << var << endl; // Si estás usando C++ printf("valor: %d\n", var); // Si estás usando C
O bien podríamos utilizar el apuntador para obtener el mismo resultado:
cout << "valor: " << *ptr << endl; // Si estás usando C++ printf("valor: %d\n", *ptr); // Si estás usando C
Es muy importante asegurarnos que la dirección que almacena nuestro apuntador haga referencia a una variable existente. De no ser así, querer leer o modificar el valor usando el operador de indirección podría generar errores.
Otra forma de manipular arreglos
En notas anteriores, y para efectos prácticos, tratamos siempre de imaginar que las variables que creábamos estaban localizadas en espacios contiguos de la memoria, pero esto no es necesariamente cierto en la realidad. Aunque declaremos nuestras variables una después de la otra en nuestros programas, puede y estas se encuentren separadas por varios espacios, pero los arreglos son la excepción.
Frecuentemente vemos a los arreglos como una colección de datos que se encuentran uno después del otro, así es exactamente como aparecen en la memoria. Al momento en que nosotros creamos un arreglo, se reservan en memoria espacios consecutivos para almacenar cada uno de los valores del arreglo.
¿Cómo nos servirían entonces los apuntadores para tratar con arreglos? La clave está en conocer algo muy importante: el identificador del arreglo se puede tratar como un apuntador al primer elemento de la colección.
Para ilustrar lo que acabamos de descubrir, vayamos con otro ejemplo:
int pares[] = {2, 4, 6, 8};
En esa única línea hemos declarado e inicializado un arreglo con 4 elementos de tipo entero. Si quisiéramos acceder al primer elemento, lo común es realizar algo como esto:
printf("primer elemento: %d\n", pares[0]);
Pero, con base en lo que ya hemos aprendido al inicio de esta nota, podríamos realizar algo como lo siguiente:
printf("primer elemento: %d\n", *pares);
Y listo, podrás ver que el resultado es el mismo y que efectivamente el identificador del arreglo funciona como un apuntador al primer elemento de la lista. ¿Qué piensas se debería hacer si desearas acceder a los elementos siguientes?
Ya que los elementos en el arreglo están dispuestos de forma consecutiva y, sabiendo que el identificador se trata como un apuntador al primer elemento, es suficiente con sumar una unidad a dicho apuntador si queremos acceder al segundo elemento, sumar dos si queremos acceder al tercero y así sucesivamente. Recorramos e imprimamos los valores del arreglo haciendo uso de la aritmética de apuntadores:
for (int i = 0; i < 4; i++) { cout << *(pares + i) << endl; } // - SALIDA - // 2 // 4 // 6 // 8
Parece que estamos haciendo uso de una nueva notación para imprimir los valores del arreglo pero no es exactamente así, ya conocemos todo eso, vamos viendo cada parte:
pares
Simplemente el apuntador al primer elemento.
pares + i
Sumamos el valor de “i” a nuestro apuntador, de esta forma es como pasaremos del primer elemento al segundo, de ahí al tercero y de este al cuarto y último. Esta imagen te ayudará a comprenderlo mejor:
*(pares + i)
Y aquí solo utilizamos el operador de indirección para acceder al valor en cuestión; si te preguntas por qué hacemos uso de paréntesis, simplemente es porque el operador de indirección tiene mayor precedencia que el operador de suma, de esta manera nos aseguramos que primero se realice la suma del apuntador y luego accedamos al valor al que se refiere la nueva dirección. Si llegásemos a quitar los paréntesis quedaría algo como esto:
*pares + i
Esto daría como resultado acceder al valor de la primera posición del arreglo y sumarle una unidad. Lo que se nos mostraría en pantalla, utilizando el bucle for
que tenemos, sería:
// - SALIDA - // 3 // 3 // 3 // 3
Es importante asegurarnos de que no estamos accediendo a una posición inválida cuando estamos haciendo uso de apuntadores para manejar algún arreglo. A diferencia del error de compilación que se nos presentaría si hacemos algo como esto:
pares[4] = 10; // Error. El arreglo solo se puede referenciar hasta el índice 3
En este otro caso el compilador no nos diría nada:
*(pares + 4) = 10;
Y esto nos podría traer errores por querer acceder a una posición que no sabemos qué contiene ni para qué se esté utilizando.
Para terminar
Como habrás apreciado, el uso de apuntadores es un arma de doble filo, así como pueden sernos de bastante ayuda en algunas situaciones, debemos de tener cuidado de utilizarlos correctamente para no arruinar el programa que estemos creando.
En esta ocasión vimos un poco sobre cómo se relacionan los apuntadores con los arreglos, en la siguiente nota hablaremos sobre el uso de los apuntadores junto con las estructuras, aquí aprenderemos sobre un nuevo operador que en ocasiones causa confusiones.
Cualquier duda o sugerencia que tengas puedes escribirla en los comentarios, nos vemos en la próxima. Hasta entonces.