Bueno, seguimos adelante. Veamos por qué tenemos que identificar el tipo de variable a la que apunta un puntero, como en:
int *ptr;
Una vez que ptr "apunta a" algo, si escribimos:
*ptr = 2;
el compilador sabrá cuantos bytes tiene que copiar en la ubicación de memoria donde apunta ptr. Si ptr se declara como un puntero a un entero, se copiarán 4 bytes. De igual forma ocurre para floats y doubles donde se copiará el número de bytes apropiado. De todas formas, la definición del tipo a donde apunta el puntero le da al compilador otras opciones interesantes. Por ejemplo, consideremos un bloque en memoria de diez enteros contiguos. Esto es, reservamos 40 bytes de memoria para alojar 10 enteros.
Ahora, digamos que apuntamos con nuestro puntero entero ptr al primero de esos enteros de ese bloque. Por otro lado, digamos que ese entero está ubicado en la posición 100 (decimal) de la memoria. ¿Qué ocurre si escribimos lo siguiente?:
ptr + 1;
Dado que el compilador "sabe" que esto es un puntero (es decir, su valor es una dirección) y que apunta a un entero (su valor actual, 100, es la dirección de un entero), agrega 4 a ptr en lugar de 1, por lo que el puntero "apunta a" el siguiente entero, en la posición 104 de memoria. De igual forma, si ptr se declara como un puntero a un short, agregaría 2 en lugar de 1. Lo mismo ocurre con otros tipos de datos como floats, doubles, o incluso con tipos de datos definidos por el usuario como las estructuras. Obviamente, esto no es el mismo tipo de "adición" que solemos pensar. En C se lo conoce como adición mediante "aritmética de punteros", un término que veremos después.
Asimismo, dado que ++ptr y ptr++ son ambos equivalentes a ptr + 1 (aunque puede diferir el lugar donde ptr se incremente), el incremento de un puntero mediante el operador unario ++, ya sea a la izquierda o a la derecha del nombre del puntero, incrementa la dirección que está alojando por la cantidad de sizeof(type) donde "type" es el tipo del objeto donde apunta (es decir, 4 para un entero).
Puesto que un bloque de 10 enteros contiguos en memoria es, por definición, un array de enteros, esto nos lleva a una interesante relación entre arrays y punteros.
Consideremos lo siguiente:
int mi_array[] = {1,23,17,4,-5,100};
Aquí tenemos un array con 6 enteros. Nos referimos a cada uno de estos enteros mediante una posición de mi_array, es decir, utilizando mi_array[0] hasta mi_array[5]. Pero también podemos acceder a ellos mediante un puntero de la siguiente forma:
int *ptr; ptr = &mi_array[0]; /* ahora el puntero ptr apunta al primer entero de nuestro array */
Y entonces podemos mostrar nuestro array mediante la notación de array o desreferenciando nuestro puntero. El siguiente código lo muestra:
----------- Programa 2.1 ---------------------------------- /* Programa 2.1 de PTRTUT10.HTM 6/13/97 */ #include <stdio.h> int mi_array[] = {1,23,17,4,-5,100}; int *ptr; int main(void) { int i; ptr = &mi_array[0]; /* ahora el puntero ptr apunta al primer elemento del array */ printf("\n\n"); for (i = 0; i < 6; i++) { printf("mi_array[%d] = %d ",i,mi_array[i]); /*<-- A */ printf("ptr + %d = %d\n",i, *(ptr + i)); /*<-- B */ } return 0; }
Compila y ejecuta el código anterior y pon atención a las líneas A y B, pues el programa muestra los mismos valores en los dos casos. Fíjate también en cómo se desreferencia nuestro puntero en la línea B, ese decir, primero agregamos i al puntero y después desreferenciamos el nuevo puntero. Ahora cambia la línea B de esta forma:
printf("ptr + %d = %d\n",i, *ptr++);
ejecútalo de nuevo... ahora cámbialo por:
printf("ptr + %d = %d\n",i, *(++ptr));
y prueba una vez más. En cada prueba, intenta predecir el resultado y pon atención al resultado real.
En C, la norma indica que donde utilicemos &nombre_var[0], se puede reemplazar con nombre_var. En nuestro código, donde escribimos:
ptr = &mi_array[0];
podemos sustituirlo por:
ptr = mi_array;
para conseguir el mismo resultado.
Esto lleva a que muchos textos indican que el nombre de un array es un puntero. Yo prefiero pensar "el nombre de un array es la dirección de su primer elemento". Muchos principiantes (yo incluido cuando estaba aprendiendo) tienen la tendencia a confundirse cuando piensan en ello como un puntero. Por ejemplo, sí podemos escribir
ptr = mi_array;
pero no podemos escribir
mi_array = ptr;
La razón es que ptr es una variable, pero mi_array es una constante. Es decir, la posición del primer elemento de mi_array no puede cambiarse una vez que mi_array[] se ha declarado.
Cuando antes hablábamos del término "lvalue" yo cité K&R-2 donde indica:
"Un objeto es una región de almacenamiento con un nombre; un lvalue es una expresión que se refiere a un objeto."
Esto plantea un problema interesante. Debido a que mi_array es una región de almacenamiento con un nombre, ¿por qué mi_array en la instrucción de asignación anterior no es un lvalue? Para resolver este problema, algunos dicen que mi_array es un "lvalue no modificable".
Modifica el programa anterior cambiando
ptr = &mi_array[0];
por
ptr = mi_array;
y ejecútalo de nuevo para comprobar que el resultado es el mismo.
Ahora, vamos a profundizar un poco más en la diferencia entre los nombres ptr y mi_array como hemos visto. Algunos autores se refieren al nombre de un array como un puntero constante. ¿Qué quiere decir eso? Bien, para entender el término "constante" en este caso, volvamos a nuestra definición del término "variable". Cuando declaramos una variable estamos eligiendo un punto en la memoria para alojar en él un valor con un tipo apropiado. Realizado esto, el nombre de la variable puede interpretarse de dos formas. Cuando se utiliza a la izquierda del operador de asignación, el compilador lo interpreta como la dirección de memoria donde se alojará el valor resultante de la evaluación del lado derecho del operador de asignación. Pero cuando se utiliza a la derecha del operador de asignación, el nombre de una variable se interpreta como el contenido de esa dirección de memoria (el valor de esa variable).
Con esto en mente, consideremos ahora unas constantes simples como:
int i, k; i = 2;
Aquí, mientras i es una variable que ocupa un espacio en la porción de datos de la memoria, 2 es una constante que, en lugar de alojarse en el segmento de datos de la memoria, se encuentra incrustada directamente en el segmento de código de la memoria. Esto es, cuando escribimos algo como k = i; el compilador crea código que al ejecutarse determinará el valor alojado en la dirección de memoria &i para moverlo a k, y cuando escribimos algo como i = 2; el compilador simplemente coloca el 2 en el código, donde no hay ninguna referencia al segmento de datos. Tanto k como i son objetos, pero 2 no es un objeto.
Del mismo modo, como mi_array es una constante, una vez que el compilador establece el lugar donde guardará el array, ya "sabe" la dirección de mi_array[0] y viendo:
ptr = my_array;
sabemos que utiliza esta dirección como una constante en el segmento de código y no existe referencia al segmento de datos.
Este podría ser un buen lugar para explicar con más detalle el uso de la expresión (void*) del programa 1.1 del Capítulo 1. Como hemos visto, podemos tener punteros de varios tipos. Hasta ahora hemos hablado de punteros a enteros (int) y punteros a caracter (char). En próximos capítulos vamos a ver punteros a estructuras e incluso punteros a punteros.
También hemos aprendido que entre sistemas, el tamaño de un puntero puede variar. Resulta que también es posible que el tamaño de un puntero pueda variar en función del tipo de datos del objeto al que apunta. Por lo tanto, podemos tener problemas si asignamos una variable de tipo entero largo (long) a una variable de tipo entero corto (short) y del mismo modo, también podemos tener problemas si intentamos asignar punteros de varios tipos a punteros de otros tipos.
Para minimizar este problema, C proporciona el tipo void para punteros. Podemos declarar este tipo de punteros así:
void *vptr;
Un puntero void es una especie de puntero genérico. Por ejemplo: C no permite comparar un puntero de tipo int con un puntero de tipo char, pero sí permite comparar cualquiera de ellos con un puntero de tipo void. Por supuesto, como con otras variables, se pueden utilizar conversiones (cast) para convertir un tipo de puntero a otro tipo bajo las condiciones adecuadas. En el programa 1.1. del Capítulo 1 se realiza una conversión de punteros enteros a punteros void para hacerlos compatibles con la especificación de conversión %p. En capítulos posteriores realizaremos otras conversiones por razones que ya veremos.
Bien, con esto tenemos un montón de información técnica para digerir y no espero que un principiante entienda todo esto a la primera. Con tiempo y práctica querrás repasar los dos primeros capítulos. Por ahora, vamos a pasar a la relación entre punteros, arrays de caracteres (char) y cadenas de texto (strings).