PUNTEROS DE LISTAS Y TABLAS







Punteros y listas .Existe una estrecha relación entre el uso de punteros y el uso de listas. De hecho, la aplicación más frecuente de los punteros es el recorrido de listas. Esto se debe a la forma en que se recorre una lista desde el punto de vista del lenguaje máquina. Concretamente, recorrer una lista consiste en cargar la dirección base (la dirección de comienzo) de la lista en un registro, para después leer el contenido de esa dirección y pasar a la siguiente posición de la lista. El lenguaje máquina dispone de instrucciones específicas para hacer esto, y por tanto la operación resulta compacta y rápida.
Desde el punto de vista de C, el paralelismo es muy grande. Para recorrer una lista se emplea un puntero del tipo base de sus elementos . Considérese la declaración siguiente:

Tipo_base nombre_lista[DIMENSION];
Tipo_base * puntero = nombre_lista;

El nombre el elemento i-ésimo de la lista es

Sea cual fuere la forma en que se denote este i-ésimo elemento, será preciso sumar el oportuno desplazamiento a la dirección base. La dirección base la proporciona nombre_lista o puntero , y el desplazamiento lo indica i (concretamente, el desplazamiento es i*sizeof(Tipo_base) ). Estas operaciones, insistimos, tienen su contrapartida directa en lenguaje máquina y son muy eficientes.

Con excepción de las características derivadas del uso de un marcador de fin de cadena, todo lo aplicable en el caso de listas de caracteres puede aplicarse a cualquier otra lista. Esto es, se puede acceder a los elementos de una lista en la forma habitual, y también a través de un puntero que la recorra. En efecto, basta asignar al puntero la dirección de la lista (su nombre) para que este señale el primer elemento de la misma. Al incrementar el puntero se irán recorriendo los sucesivos elementos de la lista; la aritmética de punteros adapta su significado en términos de bytes reales a las dimensiones del tipo base utilizado. Véase el oportuno Ejercicio .

Tablas: recorrido mediante un sólo índice
Las tablas, y en general todas las matrices, se almacenan en memoria por filas ( q.v. ). Esto hace posible recorrer tablas de forma muy sencilla, empleando un único puntero, según puede verse en este Ejercicio , que expone varias formas de acceder a los elementos de una lista bidimensional, empleando uno o dos punteros. Evidentemente, la solución más eficiente consiste en emplear un único puntero, y esto siempre va a ser posible, como se indica en el ejercicio. Independientemente del número de dimensiones, toda matriz se puede recorrer empleando un único índice.

Punteros de listas.
Los punteros habituales, cuyas declaraciones son de la forma:

Tipo_base *puntero;

bastan, según lo visto, para recorrer listas, tablas o matrices de cualquier tipo de dimensiones, empleando un único índice. Esto es suficiente en muchas ocasiones, pero puede resultar más cómodo emplear la sintaxis matricial habitual para recorrer tablas, tarugos o matrices de más dimensiones. C ofrece la posibilidad de crear punteros que tengan como tipo base filas completas, tablas completas, tarugos completos, etc. Estos punteros se pueden emplear con dos índices (para recorrer tablas), con tres punteros (para recorrer tarugos), con cuatro punteros (para recorrer teseractos), etc. Un ejemplo de esto son las siguientes declaraciones, en las que se construye un puntero de fila, otro de tabla y otro de tarugo:

Tipo_base (*p_fila)[NUMERO_DE_COLUMNAS];
Tipo_base (*p_tabla)[NUMERO_DE_FILAS][NUMERO_DE_COLUMNAS];
Tipo_base (*p_tarugo)[NUMERO_DE_CAPAS][NUMERO_DE_FILAS][NUMERO_DE_COLUMNAS];

Los punteros construidos aquí ( p_fila , p_tabla y p_tarugo ) poseen las características siguientes:
  1. El puntero p_fila señala toda una fila formada por NUMERO_DE_COLUMNAS elementos de Tipo_base. Por tanto, *p_fila es toda una fila completa, formada por NUMERO_DE_COLUMNAS elementos, y p_fila[i] sería la (hipotética) i-ésima fila de una tabla formada por filas de NUMERO_DE_COLUMNAS columnas del tipo base. Como p_fila[i] es el nombre de una lista, se le puede aplicar un segundo índice, j , para acceder a elementos individuales de esa fila, en mediante expresiones de la forma p_fila[i][j] , que denotan la j-esima fila de la i-ésima columna. Para dar valor a p_fila se puede emplear una declaración como la siguiente:
    
    Tipo_base tabla[NUMERO_DE_FILAS][NUMERO_DE_COLUMNAS];
    Tipo_base (*p_fila)[NUMERO_DE_COLUMNAS];
    p_fila = tabla;
    
    
    Esto tiene sentido porque tabla se interpreta como la dirección de su primer elemento, esto es, como &tabla[0] . Ahora bien, tabla[0] es una expresión con un sólo índice, luego alude a la primera fila (el índice del que se prescinde se quita "por la derecha"). Entonces &tabla[0] es la dirección de una fila formda por NUMERO_DE_COLUMNAS elementos, que es precisamente el tipo de p_fila . P ara recorrer tablas se emplea un puntero de fila .
  2. El puntero p_tabla señala toda una tabla (o capa) formada por NUMERO_DE_FILAS filas, cada una de las cuales consta de NUMERO_DE_COLUMNAS columnas. Los elementos de esta tabla son del tipo Tipo_base . Entonces *p_tabla es toda una tabla y p_tabla[i] sería la (hipotética) i-ésima capa (tabla) de un tarugo cuyas capas tendrían NUMERO_DE_FILAS filas de NUMERO_DE_COLUMNAS columnas. Como p_tabla[i] es una tabla, se le puede añadir un segundo índice, j , para acceder a filas individuales de esa tabla. Entonces p_tabla[i][j] denota la j-ésima fila (completa) de la i-ésima tabla. Por último, al ser p_tabla[i][j] una fila... se le puede añadir un tercer índice, k , que denotará una columna dentro de esta fila. Esto es, p_tabla[i][j][k] denota la columna k de la fila j de la capa i de un (hipotético!) tarugo. Para dar valor a p_tabla se podría emplear una declaración como la siguiente:
    
    Tipo_base (*p_tabla)[NUMERO_DE_FILAS][NUMERO_DE_COLUMNAS];
    Tipo_base tarugo[NUMERO_DE_CAPAS][NUMERO_DE_FILAS][NUMERO_DE_COLUMNAS];
    p_tabla = tarugo;
    
    
    Esto tiene sentido porque tarugo se interpreta como la dirección del primero de sus elementos, esto es, &tarugo[0] . Como en tarugo faltan dos índices, faltarán por la derecha. Entonces el índice restante denota la capa del tarugo, que es una tabla de dimensiones NUMERO_DE_FILAS x NUMERO_DE_COLUMNAS . Y p_tabla es precisamente un puntero de tabla de esas dimensiones así que todo encaja. Para recorrer tarugos se emplea un puntero de tabla .
  3. El puntero p_tarugo señala un tarugo completo, esto es, un objeto de NUMERO_DE_CAPAS capas cada una de las cuales es una tabla formada por NUMERO_DE_FILAS filas que poseen NUMERO_DE_COLUMNAS columnas de tipo Tipo_base . Entonces *p_tarugo es todo un tarugo y p_tarugo[i] denota el i-ésimo tarugo de un teseracto. Como tarugo que es, podemos añadir 1, 2 o 3 índices a p_tarugo[i] . Con un índice adicional, p_tarugo[i][j] , estaremos accedediendo a la capa j del iésimo tarugo del teseracto. Con dos índices adicionales, p_tarugo[i][j][k] , estaremos accediendo a la fila k de la capa j del tarugo i del teseracto. Con tres índices adicionales, p_tarugo[i][j][k][l] , estaremos accediendo a la columna l de la fila k de la capa j del i-ésimo tarugo del teseracto. Para dar valor a p_tarugo se podría emplear una declaración como la siguiente:
    
    Tipo_base teseracto[NUMERO_DETARUGOS][NUMERO_DE_CAPAS][NUMERO_DE_FILAS][NUMERO_DE_COLUMNAS];
    Tipo_base (*p_tarugo)[NUMERO_DE_CAPAS][NUMERO_DE_FILAS][NUMERO_DE_COLUMNAS];
    p_tarugo = teseracto;
    
    
    Todo va bien porque estamos poniendo p_tarugo = &teseracto[0] y teseracto[0] denota el primer tarugo. Para recorrer teseractos, se emplea un puntero de tarugo . En general, para recorrer un objeto de N dimensiones, se emplea un puntero de objeto de N-1 dimensiones.


¿Por qué existen los punteros de lista, tabla, tarugo, etc.?
Dada una lista, tabla, tarugo o teseracto, siempre es posible emplear su nombre con más o menos índices para acceder a uno u otro bloque de información, desde los más pequeños (de tipo Tipo_base ) a los más grandes (filas, capas, tarugos). Entonces, ¿por qué se han creado los punteros de filas, capas, tarugos, etc.? La respuesta se encuentra en la asignación dinámica de memoria. Cuando se crea una variable dinámica, la única forma de acceder a ella es a través de un puntero: las variables dinámicas carecen de nombre. Entonces es preciso disponer de punteros del tipo adecuado, que harán las veces de nombre de esas variables . Todo esto se estudia en la sección correspondiente .

Ejercicio .- Construir un programa que muestre el uso de listas (tablas monodimensionales) con acceso a través de punteros.


/*
 Este programa muestra el uso de listas (matrices
 monodimensionales) empleando punteros.
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define DIMENSION 5

int main(void)
{
 float lista[DIMENSION] = {1.0, 2.0, 3.0, 4.0, 5.0};
 float * p = lista;
 int i;
 printf("Listas y punteros.\n\n");
 for(i=0;i<DIMENSION;i++)
  printf("lista[%d] = %f\n", i,lista[i]);

 printf("\n\nDe otro modo:\n\n");
 for(i=0;i<DIMENSION;i++)
  printf("*(%u + %d) = %f\n",(unsigned int)p,i,*(p+i));

 printf("\n\nY de otro modo:\n\n");
 for(i=0;i<DIMENSION;i++)
  printf("*(%u + %d) = %f\n",(unsigned int)p,i,p[i]);

 printf("\n\nY de otro modo:\n\n");
 for(i=0;i<DIMENSION; i++)
  printf("*(%u + %d) = %f\n",(unsigned int)p,i,*p++);
 /* ¿Qué hace ++*p? */

 printf("\n\nTerminación normal del programa.\n");
 return 0;
}

/*
	RESULTADO
Listas y punteros.

lista[0] = 1.000000
lista[1] = 2.000000
lista[2] = 3.000000
lista[3] = 4.000000
lista[4] = 5.000000


Y de otro modo:

*(97359480 + 0) = 1.000000
*(97359480 + 1) = 2.000000
*(97359480 + 2) = 3.000000
*(97359480 + 3) = 4.000000
*(97359480 + 4) = 5.000000


Y de otro modo:

*(97359480 + 0) = 1.000000
*(97359484 + 1) = 2.000000
*(97359488 + 2) = 3.000000
*(97359492 + 3) = 4.000000
*(97359496 + 4) = 5.000000


Terminación normal del programa.
*/

Comentarios .- La conclusión es la esperable. Podemos acceder a los elementos de la lista empleando la notación "algebraica" habitual, esto es, el nombre de la lista con un índice. También se puede emplear el puntero de lista con un índice que se va incrementando. Por último, se puede incrementar directamente el valor del puntero, mediante el operador ++. En todos los casos se calculan las mismas direcciones (de los elementos) y se obtienen, como era de esperar, los mismos valores. Obsérvese, por cierto, que el valor del puntero ha resultado modificado. ¡Cuando finaliza el bucle for() , p ya no apunta a un elemento de la lista!


Ejercicio.- Construir un programa que muestre el uso de un único puntero para recorrer matrices bidimensionales.
*
 Este programa muestra el uso de un único puntero
 para recorrer matrices bidimensionales.
*/

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

#define DIM_FILAS 5
#define DIM_COLS 5
#define Tipo_base float


int main(void)
{
 Tipo_base tabla[DIM_FILAS][DIM_COLS];
 Tipo_base * puntero = tabla[0];
 int i,j;

 printf ("Manejo de tablas con punteros\n\n");

 /* Iniciación */
 for(i=0;i<DIM_FILAS*DIM_COLS;i++)
  *(puntero + i) = i;
 /* Contenido de la matriz (método habitual) */
 for(i=0; i < DIM_FILAS; i++)
  {
   for(j=0; j < DIM_COLS; j++)
    printf("%6.2f",tabla[i][j]);
   printf("\n");
  }
 /* Direcciones de los elementos de la matriz (método habitual) */
 for(i=0; i < DIM_FILAS; i++)
  {
   for(j=0; j < DIM_COLS; j++)
    printf("%u",(unsigned int)&tabla[i][j]);
   printf("\n");
  }
 /* Recorrido análogo con 1 puntero */
 printf("\n\nY de otro modo\n\n");
 for(i=0; i < DIM_FILAS; i++)
  {
   for(j=0; j < DIM_COLS; j++)
    printf("%6.2f",*(puntero + i*DIM_COLS + j));
   printf("\n");
  }
 printf("\n\nY con menos código:\n\n");
 for(i=0; i < DIM_FILAS*DIM_COLS; i++)
  {
   printf("%6.2f",*(puntero+i));
   if( (i % DIM_COLS )==(DIM_COLS-1) )printf("\n");
  }

 /* Con las modificaciones oportunas (añadiendo
    otro if() se puede tratar el caso de múltiples
    capas, o de múltiples tarugos. La condición es
    saltar de linea, o de capa, cuando el índice
    llega al número de columnas menos uno, o al de
    filas por columnas menos uno (para capas), o al
    de filas por columnas por capas menos uno
    (para tarugos).
  */
 printf ("\n\nTerminación normal del programa.\n");

 return 0;
}

/*
	RESULTADO
Manejo de tablas con punteros

  0.00  1.00  2.00  3.00  4.00
  5.00  6.00  7.00  8.00  9.00
 10.00 11.00 12.00 13.00 14.00
 15.00 16.00 17.00 18.00 19.00
 20.00 21.00 22.00 23.00 24.00


Y de otro modo

  0.00  1.00  2.00  3.00  4.00
  5.00  6.00  7.00  8.00  9.00
 10.00 11.00 12.00 13.00 14.00
 15.00 16.00 17.00 18.00 19.00
 20.00 21.00 22.00 23.00 24.00


Y con menos código:

  0.00  1.00  2.00  3.00  4.00
  5.00  6.00  7.00  8.00  9.00
 10.00 11.00 12.00 13.00 14.00
 15.00 16.00 17.00 18.00 19.00
 20.00 21.00 22.00 23.00 24.00


Terminación normal del programa.
*/


Volver al principio de esta página

Ejercicios propuestos


  1. Ejercicio 0704r01.- Se dispone de una lista de valores float . Se pide construir un programa que muestre el valor máximo y mínimo de la lista, y que indique los índices de las posiciones en que se encuentran estos valores. Se supone que no hay valores repetidos.

  2. Ejercicio 0704r02.- Se dispone de una tabla de valores float . Se pide construir un programa que muestre el valor máximo y mínimo de la tabla, y que indique los índices de las posiciones en que se encuentran estos valores. Se supone que no hay valores repetidos.

  3. Ejercicio 0704r03.- Se dispone de un tarugo de valores float (de dimensiones CAPASxFILASxCOLUMNAS ). Se pide construir un programa que muestre el valor máximo y mínimo del tarugo, y que indique los índices de las posiciones en que se encuentran estos valores. Se supone que no hay valores repetidos.

  4. Ejercicio 0704r04.- Construir una función adecuada para intercambiar dos elementos de una fila de float .

  5. Ejercicio 0704r05.- Construir una función adecuada para intercambiar dos filas de una tabla de struct . La función tiene acceso a la definición del struct y conoce las dimensiones de la tabla.

  6. Ejercicio 0704r06.- Construir una función adecuada para intercambiar dos filas de una tabla de struct . La función sólo conoce el tamaño ( sizeof() ) del struct y las dimensiones de la tabla.

  7. Ejercicio 0704r07.- Crear un tarugo de dimensiones 2x3x4 , formado por elementos de tipo double . Construir un programa que calcule las sumas de todas sus filas.

  8. Ejercicio 0704r08.- Considérese un cubo de Rubik. Se pide construir un programa que, una vez creada la estructura de datos adecuada para almacenar su estado, ofrezca un operador adecuado para hacer girar cualquier capa 90º.