lunes, 14 de marzo de 2016

Los punteros y C++

C++ en sí no es un lenguaje tan complicado. Lo complicado es lidiar con los errores que a menudo nos aparecen al intentar resolver un problema. Pienso que Stroustrup, un ejemplo para la comunidad informática, lo pensó más de dos veces antes de crear todas las herramientas que caracterizan a este lenguaje. "C++ nos provee herramientas muy prácticas para..." es un enunciado que a menudo encontrarán en distintas páginas de referencia. Y no los culpo a decir verdad: si ya es increible el hecho de que con 4 tipos básicos de datos (numericos, caracteres, booleano, objeto) sea posible construir programas bastante utiles y complejos, ¿qué nos detiene a pensar que el uso de un tipo de dato más no nos será de mayor ayuda? Creo que ya saben a qué tipo de datos me refiero. Exacto! los punteros (gracias por levantar la mano ¬¬).

Antes de abordar nuestro tema de interés es necesario tener en claro los tres componentes de una variable:
  - Nombre
  - Valor
  - Direccion en memoria (DM)

De estos, solo los dos ultimos son accesibles desde el exterior: si deseamos obtener el valor de la variable "v" simplemente escribimos "v" y C++ lo reemplazará por 5. Para obtener la DM, debemos usar el prefijo "&":
    int v = 5;  // declaramos v = 5
    cout << v;  // se imprime 5
    cout << &v; // se imprime la direccion en memoria de v
Teniendo una DM, puedo acceder al valor que contiene usando el prefijo "*":
    cout << *(&v); // se imprime 5, porque 5 es el valor contenido en la DM de v
Dado que este no es un blog de "referencia de C++", no es necesario dar una definición detallada de los punteros. Creo que basta con decir que son variables que almacenan las direcciones en memoria de otras variables (a esto se le llama "apuntar"). Acabo de mencionar que los punteros son "variables", por lo tanto podríamos hacer una similitud con las lineas anteriormente escritas:
    int* p = &v; // declaramos el puntero a entero p, que contiene la DM de v
    cout << p;   // se imprime la DM de v
    cout << &p;  // se imprime la DM de p
Pero como "p" contiene a "&v", podriamos acceder al valor que contiene "&v":
    cout << *p;  // se imprime 5
Creo que hasta el momento todo está claro, lo suficiente como para comprender la existencia de punteros que apuntan a otros punteros.

Ahora, cambiando no tan drásticamente de tema, ¿qué sucede con los arreglos en C++? De seguro al momento de resolver un problema con la premisa "mostrar todos los elementos del arreglo" se les ocurrió hacer lo siguiente:
    int Arr[5] = {3,7,9,4,5}; // arreglo de ejemplo definido en memoria estática
    cout << Arr;              // mostrar el valor contenido de Arr en pantalla
Esto puede funcionar en Python, donde existe un método predefinido para realizar dicha operacion (sin tomar en cuenta que en Python existen listas, no arreglos). Pero en C++ no. Tras ejecutar el programa anterior se mostrará en pantalla la DM del primer valor de Arr. Sabemos que para acceder a dicho valor tendríamos que utilizar corchetes y el indice 0, pero intentemos acceder de otra manera:
    cout << Arr[0] << endl; // se imprime 3
    cout << *(Arr) << endl; // se imprime 3
Exacto! Podríamos usar la misma lógica definida lineas atrás para acceder a Arr[0]. ¿Cómo accedemos al segundo elemento? Si nos detenemos un momento a ver las DMs notaremos algo peculiar: la diferencia entre las DM de dos datos adyacentes es 4. Para ilustrar esto, asumiremos que Arr[0] se aloja en 0x8a66c700
    cout << &(Arr[0]) << endl; // imprime 0x8a66c700
    cout << &(Arr[1]) << endl; // imprime 0x8a66c704
    cout << &(Arr[2]) << endl; // imrpime 0x8a66c708
    cout << &(Arr[3]) << endl; // imprime 0x8a66c70c
    cout << &(Arr[4]) << endl; // imprime 0x8a66c710
Esto sucede porque una variable de tipo int ocupa 32 bits en memoria (4 bytes). Si hubieramos definido Arr como un arreglo de tipo double la diferencia seria de 8. Bien. Ahora veamos si al momento de restar 2 DM el resultado es realmente 4:
    cout << &(Arr[3])-&(Arr[2]); // imprime 1
Está claro que al momento de definir Arr, se reservaron 32 bits para cada dato del arreglo. Cada DM individualmente solo puede reservar 8 bits, por lo que se necesitan otras 3 para poder almacenar un entero. Nos será imposible acceder a 0x8a66c701 o 0x8a66c702 o 0x8a66c703 porque la información que contienen esta siendo utilizada por 0x8a66c700 (explicado de manera informal: es como si se hubiesen fusionado y ahora 0x8a66c700 es una DM que reserva 32 bits por sí sola). A partir de esto, podriamos comprender por qué la diferencia entre &(Arr[3]) y &(Arr[2]) es 1 y no 4. Si quisiéramos acceder al valor contenido en la primera DM desde la ultima DM, podríamos usar la siguiente instrucción:
    cout <<   &(Arr[2])+1;  // imprime &(A[3])
    cout << *(&(Arr[2])+1); // imprime 4;
Si quisiéramos acceder a cualquier dato (el elemento "n") del arreglo desde la DM del primer elemento podriamos escribir "*(&(Arr[0])+n)". Pero recordemos lo descrito en párrafos anteriores: ¡Arr es un puntero al primer elemento del arreglo!
    cout << *(Arr);   // imprime 3
    cout << *(Arr+1); // imprime 7
    cout << *(Arr+2); // imprime 9
    cout << *(Arr+3); // imprime 4
    cout << *(Arr+4); // imprime 5
Listo! A decir verdad lo que hicimos va mas allá que solo aprender otra manera de acceder a los datos de un arreglo: podríamos dejar de ver los arreglos como objetos y empezar a verlos como simples Estructuras de Datos que reservan una cantidad finita de espacios en memoria para almacenar valores de un determinado tipo (lo que realmente son). También podríamos dejar de ver el uso de corchetes como "acceso al dato ubicado en un posicion x" y empezar a verlo como la simplificacion de la operacion arriba descrita:
    // Arr[x] es la simplificacion de *(Arr + x)
Si esto es cierto, podríamos extender un poco más el tema. 

Sabemos que a nivel interno las direcciones en memoria actúan como índices de un arreglo enorme. Cada vez que declaramos una nueva variable ésta se ubica en la siguiente DM disponible a la última DM utilizada. Si continuamos con el ejemplo anterior y definimos 2 nuevos enteros "a" y "b", sus DM serían adyacentes:
    int a=1, b=2;
    cout << &a << endl; // imprime 0x8a66c714
    cout << &b << endl; // imprime 0x8a66c718
Si quisiéramos acceder al valor de "b" desde "a", tendríamos que hacer lo siguiente:
    cout << *(&a + 1); // imprime 2
Pero también podríamos emplear el método clásico para "acceder al elemento ubicado en la posicion x de un arreglo":
    cout << (&a)[1]; // imprime 2
Todo tiene sentido ahora.