Punteros ( 2da parte ).


Punteros y arreglos.


El nombre de un arreglo es realmente un puntero al primer elemento de ese arreglo. Sin embargo, si x es un arreglo undimensional, la dirección del primer elemento puede ser expresada tanto como &x[0] o simplemente como x. La dirección del elemento (i+1) se puede expresar como &x[i] o como (x+i). En la expresión (x+i) x representa una dirección e i un número entero. Además x es el nombre de un arreglo cuyos elementos pueden ser caracteres, enteros, números en coma flotante, etc. Por tanto, no estamos simplemente añadiendo valores numéricos. Más bien estamos especificando una localización que está “i” elementos del arreglo mas allá del primero, con lo cual la expresión (x+i) es una representación simbólica de una dirección en vez de una expresión aritmética.


Si &x[i] y (x+i) representan la dirección del i-ésimo elemento de x, x[i] y *(x+i) representan el contenido de esa dirección: el valor del i-ésimo elemento de x.


Ya que un nombre de arreglo es en realidad un puntero a su primer elemento, es posible definir un arreglo como una variable puntero en vez de como un arreglo convencional. La definición convencional de un arreglo produce la reserva de un bloque fijo de memoria al principio de la ejecución del programa, mientras que esto no ocurre si se representa el arreglo mediante una variable puntero. En este caso, se necesita algún tipo de asignación inicial de memoria antes de que los elementos del arreglo sean procesados. Como ya vimos anteriormente, tales tipos de reserva se realizan mediante la función malloc().


Hay una relación muy cercana entre los punteros y los arreglos. Como ya vimos previamente, el nombre del arreglo ( también conocido como denominador ) es equivalente a la dirección del elemento [0] del mismo. La explicación de ésto es ahora sencilla : el nombre de un arreglo, para el compilador C, es un PUNTERO inicializado con la dirección del primer elemento del arreglo. Sin embargo hay una importante diferencia entre ambos, que haremos notar más abajo.

Veamos algunas operaciones permitidas entre punteros :

float var1, conjunto[] = { 9.0 , 8.0 , 7.0 , 6.0 , 5.0 );
float *punt ;
punt = conjunto ;    /* equivalente a hacer : punt = &conjunto [0] */
var1 = *punt ;
*punt = 25.1 ;



Es perfectamente válido asignar a un puntero el valor de otro, el resultado de ésta operación es cargar en el puntero punt la dirección del elemento [0] del arreglo conjunto, y posteriormente en la variable var1 el valor del mismo (9.0) y para luego cambiar el valor de dicho primer elemento a 25.1 .


Veamos cual es la diferencia entre un puntero y el nombre de un arreglo : el primero es una VARIABLE, es decir que puedo asignarlo, incrementarlo etc, en cambio el segundo es una CONSTANTE, que apunta siempre al primer elemento del arreglo con que fué declarado, por lo que su contenido NO PUEDE SER VARIADO. Si lo piensa un poco ésto es lógico ya que "conjunto" implica la dirección del elemento conjunto [0] por lo que, si yo cambiara su valor, apuntaría a otro lado dejando de ser "conjunto". Desde este punto de vista, el siguiente ejemplo nos muestra un tipo de error bastante frecuente:



int conjunto[5] , lista[] = { 5 , 6 , 7 , 8 , 0 ) ;
int *apuntador ;
apuntador = lista ;      /* correcto */
conjunto = apuntador;    /* ERROR: Conjunto es Constante */
lista = conjunto ;       /* ERROR: Ambos son constantes */
apuntador = &conjunto    /* ERROR no se puede aplicar el operador & aquí.

Veamos ahora las distintas modalidades del incremento de un puntero :



int *pint , arreglo_int[5] ;
double *pdou , arreglo_dou[6] ;
pint = arreglo_int ;      /* pint apunta a arreglo_int[0] */
pdou = arreglo_dou ;      /* pdou apunta a arreglo_dou[0] */ 
pint += 1 ;              /* pint apunta a arreglo_int[1]  */
pdou += 1 ;              /* pdou apunta a arreglo_dou[1]  */ 
pint++ ;                  /* pint apunta a arreglo_int[2] */
pdou++ ;                  /* pdou apunta a arreglo_dou[2] */ 



Hemos declarado y asignado dos punteros, uno a int y otro a double, con las direcciones de dos arreglos de esas características. Ambos estarán ahora apuntando a los elementos [0] de los arreglos. En las dos instrucciones siguientes incrementamos en uno dichos punteros. ¿ adonde apuntaran ahora ?.


Como ya vimos, para el compilador estas sentencias se leen como: incremente el contenido del puntero ( dirección del primer elemento del arreglo ) en un número igual a la cantidad de bytes que tiene la variable con que fue declarado. Es decir que el contenido de pint es incrementado en dos bytes (un int tiene 2 bytes ) mientras que pdou es incrementado 8 bytes ( por ser un puntero a double ), el resultado entonces es el mismo para ambos, ya que luego de la operación quedan apuntando al elemento SIGUIENTE del arreglo, arreglo_int[1] y arreglo_dou[1] .


Vemos que de ésta manera será muy facil "barrer" arreglos independientemente del tamaño de variables que lo compongan, permitiendo por otro lado que el programa sea transportable a distintos “hardwares” sin preocuparnos de la diferente cantidad de bytes que pueden asignar los mismos, a un dado tipo de variable.


De manera similar las dos instrucciones siguientes, vuelven a a incrementarse los punteros, apuntando ahora a los elementos siguientes de los arreglos.


Todo lo dicho es aplicable en idéntica manera al operador de decremento -- .


Si, en cambio, el arreglo es bidimensional ( es este caso específico, pero puede ser multidimensional en general ), en realidad éste es una colección de arreglos unidimensionales. Por lo tanto, se puede definir un arreglo bidimensional como un puntero a un grupo contiguo de arreglos unidimensionales ( OJO: No lo confundan con un arreglos de punteros, lo que veremos más adelante, aunque realmente pueden usarse con fines similares ). Una declaración de arreglo bidimensional puede ser escrita como:


tipo_dato (*ptvar) [expresión2];


en vez de:


tipo_dato arreglo [expresión1][expresión2];


Los paréntesis que rodean al puntero deben estar presentes. De otra forma se interpretaría como un arreglo de punteros ( ver más adelante ).


Ejemplo: Supongamos que x es un arreglo bidimensional de enteros con 10 filas y 20 columnas. Podemos declarar x como:


int (*x)[20];


en vez de:


int x[10][20];


En la primera declaración x se define como un puntero a un grupo contiguo de arreglo unidimensionales de 20 elementos enteros. Así x apunta al primero de los arreglos de 20 elementos, que es en realidad la primera fila (fila 0) del arreglo bidimensional original. Del mismo modo (x+1) apunta al segundo arreglo de 20 elementos, y así sucesivamente.


El elemento de la fila 2 columna 5 es accedido como: *(*(x+2)+5). Porqué? Veamos:


  1. (x+2) es un puntero a la fila 2

  2. *(x+2) es el objeto de ese puntero y refiere a toda la fila. Como la fila 2 es un arreglo unidimensional. *(x+2) es realmente un puntero al primer elemento de la fila 2.

  3. (*(x+2)+5) es un puntero al elemento 5 de la fila 2.

  4. El objeto de este puntero *(*(x+2)+5) refiere al elemento en la columna 5 de la fila 2.



Punteros y arreglos de caracteres.


No hay gran diferencia entre el trato de punteros a arreglos y a cadenas de caracteres ya que éstos son entidades de la misma clase, sin embargo analicemos algunas particularidades. Así como inicializamos una cadena de caracteres con un grupo de caracteres terminados en '\0', podemos asignar al mismo un puntero:

p = "Esto es una cadena constante " ;

esta operación no implica haber copiado el texto, sino sólo que a p se le ha asignado la dirección de memoria donde reside la "E" del texto. A partir de ello podemos manejar a p como lo hemos hecho hasta ahora. Veamos un ejemplo:



#include <stdio.h>
#define TEXTO1  "¿ Hola, como "  // #define es una directiva del compilador
#define TEXTO2  "le va a Ud. ? "	// para definir constantes.

main() {
char palabra[20], *p ;
int i ;
p = TEXTO1 ;
for( i = 0 ; ( palabra[i] = *p++ ) != '\0' ; i++ ) ;
p = TEXTO2 ;
printf("%s" , palabra ) ;
printf("%s" , p ) ;
return 0 ;
}


Definimos primero dos cadenas constantes TEXTO1 y TEXTO2, luego asignamos al puntero p la dirección del primero y seguidamente en el bucle for copiamos el contenido de éste en el arreglo palabra. Observe que dicha operación termina cuando el contenido de lo apuntado por p es el terminador de la cadena. Luego asignamos a p la dirección de TEXTO2 y finalmente imprimimos ambas cadenas, obteniendo una salida del tipo : " ¿ Hola, como le va a UD. ? ".


Reconozcamos que ésto se podría haber escrito más compacto si hubiésemos recordado que palabra también es un puntero y NULL es cero. Así, podríamos poner en vez del bucle for:



while( *palabra++ = *p++ ) ;



Hay un tipo de error muy frecuente que podemos analizar. Fííjense en el ejemplo siguiente, ¿ve algún problema?. OJO con este ejemplo.



#include<stdio.h>



void main() {

char *p , palabra[20];

printf("Escriba su nombre : ");

scanf("%s" , p );

palabra = "¿Como le va ";

printf("%s%s" , palabra , p ) ;

}


Pues hay dos errores. El primero es un punto que ya fue analizado antes: la expresión scanf("%s" , p ) es correcta pero el error es no haber inicializado al puntero p el cual sólo fue definido pero aún no apunta a ningún lado válido. El segundo error está dado por la expresión : palabra = " ¿ Como le va " ya que el nombre del arreglo es una constante y no puede ser asignado a otro valor.


¿Como lo escribiríamos para que funcione correctamente ?

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void main() {
char *p , palabra[20] ;
p = malloc(128) ; // Por ejemplo.
printf("Escriba su nombre : ") ;
scanf("%s" , p ) ;
strcpy(palabra , "¿ Como le va " ) ;
printf("%s%s" , palabra , p ) ;
}

Observe que antes de scanf() se ha inicializado a p, mediante el retorno de malloc() y a al arreglo palabra se le ha copiado la cadena mediante la función strcpy() que se vio en la clases de arreglos y cadenas.


Debemos aclarar también que la secuencia de control %s en el printf() impone enviar a la pantalla una cadena. Estando éste apuntado por el argumento, puede ser tanto el nombre de un arreglo como un puntero ya que ambos indican direcciones.


Una forma alternativa de resolverlo, sería:



#include<stdio.h>

void main(){
char p[20] , *palabra ;
printf("Escriba su nombre : ") ;
scanf("%s" , p ) ;
palabra = "¿ Como le va " ;
printf("%s%s" , palabra , p ) ;
}



Obsérvese, que es idéntico al primero, con la salvedad que se ha invertido las declaraciones de las variables, ahora el puntero es palabra y el arreglo es p. Ambas soluciones son equivalentes y dependerá del resto del programa cual es la mejor elección.


Por último mostraremos nuevamente un ejemplo visto antes que ejemplifica la relación entre punteros y arreglos. Quizá ahora lo entiendan mejor.


#include<stdio.h>


int funcion1(char *arreglo);

main() {


/* Primero que nada el arreglo debe ser inicializado. De otra forma el puntero no estará definido.*/


char arreglo1[10]="987654321"; // recuerden el '\0' al final.

// Le pasamos la cadena a la función (o sea, la dirección del 1er elemento):

// llamada por referencia. ( Los datos de la cadena local SI serán alterados ).

funcion1(arreglo1);

printf("%s\n", arreglo1);

// Imprimirá el valor de la cadena el cual fue alterado en la función.

}

/************************************************************************/

funcion1(char *arreglo) { // Recibe un puntero a char ( Puntero a la cadena )

printf("%s\n", arreglo); // Imprime el actual contenido de la cadena.

arreglo +=4; // Modifica la dirección del puntero.

*arreglo = 'x'; // Modifica el dato apuntado por ”arreglo”.

}



Arreglos de punteros.


Es una práctica muy habitual, sobre todo cuando se tiene que tratar con cadenas de distinta longitud, generar un arreglo cuyos elementos son punteros los cuales albergarán las direcciones de dichas cadenas.


Así como:

char *uno;

definía a un puntero a un caracter, la definición

char *muchos[5];

implica un arreglo de 5 punteros a caracteres .

NOTA: Observe la diferencia entre un arreglo de punteros y un puntero a arreglos. Es análogo al caso con las funciones. “char *muchos[5]” define un arreglo de 5 elementos punteros a char. En cambio: “char (*muchos)[5]”, define un puntero a un grupo contiguo de arreglos de 5 elementos char.



Inicialización de arreglos de punteros


Los arreglos de punteros pueden ser inicializados de la misma forma que un arreglo común. Es decir, dando los valores de sus elementos durante su definición. Por ejemplo si quisiéramos tener un arreglo donde el subíndice de los elementos coincidiera con el nombre de los días de la semana podríamos escribir :

char *dias[] = {
"número de día no válido",
"lunes",
"martes",
"miercoles",
"jueves",
"viernes",
"sabado",
"domingo"
}

Igual que antes, no es necesario en este caso indicar la cantidad de elementos, ya que el compilador los calcula por la cantidad de términos dados en la inicialización. Así, el elemento dias[0] será un puntero con la dirección de la primera cadena, dias[1], la del segundo, etc.



Punteros a punteros.


En la mayoría de los casos los punteros apuntan directamente a un dato, y al emplear el operador * se obtiene el valor del dato al que apuntan. Como muchos de ustedes intuirán, si se declara una matriz de punteros en realidad se están empleando punteros a punteros, o sea que apunta a una localidad de memoria que contiene la localidad de memoria de una variable. Ojo, también se pueden definir un puntero a otro puntero de forma explícita. La forma general de declarar un puntero a puntero es:


tipo_dato **nombre;


Veamos un ejemplo muy sencillo:


main(){

int n,*p,**q;

n=5;

p=&n;

q=&p;

printf("%i",**q) /*imprime el valor de x*/

}


Aquí, n está declarado como un entero, p como un puntero a entero y q como un puntero a puntero a entero. Las asignaciones de la función nos dan una idea de como se relacionan las tres variables entre si: El contenido de la variable n es 5; a su vez, asignamos al puntero p la dirección de la variable que contiene ese número. Finalmente, como q es un puntero a un puntero, sólo puede contener direcciones por tanto guarda la dirección de la variable donde se encuentra el valor de la dirección donde se encuentra el 5.



Mediante la llamada a printf() comprobamos lo dicho, ya que despliega el contenido de número a pesar de que es llamado desde una variable puntero a puntero.



Otro con un poco más de contenido:


#include<stdio.h>

#include<stdlib.h>


main(){

int **p; // Define un puntero a puntero llamado p.

int fila, col;

int nfilas=3, ncols=3;

int temp = 1;

// Ahora creo un arreglo y asigno memoria a éste dinámicamente.

(*p)=(int *)malloc(nfilas*ncols*sizeof(int));

// reserva (nfilas*ncols*sizeof(int)) bytes. Por supuesto, sizeof(int) = 2

// (int *) al principio indica que *p será un puntero a entero.

// Ojo: se uso *p y no p. Ya saben porqué... cierto?

for(fila=0;fila<nfilas;nfilas++){

for(col=0;col<ncols;col++){

*(*(p+fila)+col)=temp;

temp++;

}

}

return 0;

}



TIPS acerca de problemas comunes con punteros.


A continuación se mostrarán una serie de errores muy comunes al trabajar con punteros:



1.-


char * funcion () {

char resultado;
/* Aquí rellenamos “resultado” con el valor deseado */
return &resultado;

}

Al salir de la función, la dirección que estamos devolviendo ya no tiene sentido.

Además, ésto nunca nos dará un error de violación de memoria, ya que esa zona de memoria pertenecí­a a nuestro programa. Nuestro programa no se "colgará", simplemente dará resultados incorrectos.

TIP: No devolver nunca punteros a variables locales a una función.


2.-


char *puntero = NULL;

puntero = malloc();

free (puntero);

*puntero = 'A';


Esto no dará ningún problema de violación de memoria, puesto que la memoria a la que apunta puntero era nuestra. Sin embargo, alguien puede posteriormente sobre-escribir en esa dirección de memoria. Este problema se agrava si no lo hacemos todo seguido. Imaginemos que hemos liberado puntero y que en otra función o más adelante en el código intentamos usarlo.

TIP: Apuntar a NULL los punteros después de liberarlos:


free (puntero);
puntero = NULL;