Se han revisado varias aplicaciones y técnicas que usan apuntadores en los capítulos anteriores. Así mismo se han introducido algunos temas avanzados en el uso de apuntadores. En este capítulo se profundizan algunos tópicos que ya han sido mencionados brevemente y otros que completan la revisión de apuntadores en C.
En este capítulo se desarrolla lo siguiente:
Un arreglo de apuntadores es lo mismo que apuntadores a apuntadores. El concepto de arreglos de apuntadores es directo ya que el arreglo mantiene su significado claro. Sin embargo, se pueden confundir los apuntadores a apuntadores.
Un apuntador a un apuntador es una forma de direccionamiento indirecto múltiple, o una cadena de apuntadores. Como se ve en la figura 10.1, en el caso de un apuntador normal, el valor del apuntador es la dirección de la variable que contiene el valor deseado. En el caso de un apuntador a un apuntador, el primer apuntador contiene la dirección del segundo apuntador, que apunta a la variable que contiene el valor deseado.
Se puede llevar direccionamiento indirecto múltiple a cualquier extensión deseada, pero hay pocos casos donde más de un apuntador a un apuntador sea necesario, o incluso bueno de usar. La dirección indirecta en exceso es difícil de seguir y propensa a errores conceptuales.
Se puede tener un apuntador a otro apuntador de cualquier tipo. Considere el siguiente código:
main() { char ch; /* Un caracter */ char *pch; /* Un apuntador a caracter */ char **ppch; /* Un apuntador a un apuntador a caracter */ ch = 'A'; pch = &ch; ppch = &pch; printf("%c\n", **ppch); /* muestra el valor de ch */ }
Lo anterior se puede visualizar como se muestra en la figura 10.1, en
donde se observa que **ppch
se refiere a la dirección de
memoria de *pch
, la cual a su vez se refiere a la dirección de
memoria de la variable ch
. Pero ¿qué significa lo anterior en la
práctica?
Se debe recordar que char *
se refiere a una cadena la cual termina con
un nulo. Por lo tanto, un uso común y conveniente es declarar un apuntador a
un apuntador, y el apuntador a una cadena, ver figura 10.2.
Tomando un paso más allá lo anterior, se pueden tener varias cadenas apuntadas por el apuntador, ver figura 10.3
Se pueden hacer referencias a cadenas individuales mediante ppch[0]
,
ppch[1]
, .... Esto es idéntico a haber declarado char *ppch[]
.
Una aplicación común de lo anterior es en los argumentos de la línea de comandos que se revisarán a continuación.
C permite leer argumentos en la línea de comandos, los cuales pueden ser usados en los programas.
Los argumentos son dados o tecleados después del nombre del programa al momento de ser ejecutado el programa.
Lo anterior se ha visto al momento de compilar, por ejemplo:
gcc -o prog prog.c
donde gcc
es el compilador y -o prog prog.c
son los argumentos.
Para poder usar los argumentos en el código se debe definir como sigue la
función main
.
main(int argc, char **argv)
o
main(int argc, char *argv[])
Con lo que la función principal tiene ahora sus propios argumentos.
Estos son solamente los únicos argumentos que la función main
acepta.
argc
es el número de argumentos dados -- incluyendo el
nombre del programa.
argv
es un arreglo de cadenas que tiene a cada uno de los
argumentos de la línea de comandos -- incluyendo el nombre del programa
en el primer elemento del arreglo.
Se muestra a continuación un programa de ejemplo:
main (int argc, char **argv) { /* Este programa muestra los argumentos de la linea de comandos */ int i; printf("argc = %d\n\n",argc); for (i=0; i<argc; ++i) printf("\t\targv[%d]: %s\n", i, argv[i]); }
Suponiendo que se compila y se ejecuta con los siguientes argumentos:
args f1 "f2 y f3" f4 5 FIN
La salida será:
argc = 6 argv[0]: args argv[1]: f1 argv[2]: f2 y f3 argv[3]: f4 argv[4]: 5 argv[5]: FIN
Observar lo siguiente:
" "
son ignoradas y son usadas para
incluir espacios dentro de un argumento.
Los apuntadores a funciones son quizá uno de los usos más confusos de los apuntadores en C. Los apuntadores a funciones no son tan comunes como otros usos que tienen los apuntadores. Sin embargo, un uso común es cuando se pasan apuntadores a funciones como parámetros en la llamada a una función.
Lo anterior es especialmente útil cuando se deben usar distintas funciones quizás para realizar tareas similares con los datos. Por ejemplo, se pueden pasar los datos y la función que será usada por alguna función de control. Como se verá más adelante la biblioteca estándar de C da funciones para ordenamiento (qsort) y para realizar búsqueda (bsearch), a las cuales se les pueden pasar funciones.
Para declarar un apuntador a una función se debe hacer:
int (*pf) ();
Lo cual declara un apuntador pf
a una función que regresa un tipo de
dato int
. Todavía no se ha indicado a que función apunta.
Suponiendo que se tiene una función int f()
, entonces simplemente se
debe escribir:
pf = &f;
para que apunte a la función
.
Para que trabaje en forma completa el compilador es conveniente que se tengan los prototipos completos de las funciones y los apuntadores a las funciones, por ejemplo:
int f(int); int (*pf) (int) = &f;
Ahora f()
regresa un entero y toma un entero como parámetro.
Se pueden hacer cosas como:
ans = f(5); ans = pf(5);
los cuales son equivalentes.
La función de la biblioteca estándar qsort
es muy útil y esta
diseñada para ordenar un arreglo usando un valor como llave de
cualquier tipo para ordenar en forma ascendente.
El prototipo de la función qsort
de la biblioteca stdlib.h
es:
void qsort(void *base, size_t nmiemb, size_t tam, int (*compar)(const void *, const void *));
El argumento base
apunta al comienzo del vector que será ordenado,
nmiemb
indica el tamaño del arreglo, tam
es el tamaño en
bytes de cada elemento del arreglo y el argumento final compar
es un
apuntador a una función.
La función qsort
llama a la función compar
la cual es
definida por el usuario para comparar los datos cuando se ordenen. Observar
que qsort
conserva su independencia respecto al tipo de dato al dejarle
la responsabilidad al usuario. La función compar
debe regresar un
determinado valor entero de acuerdo al resultado de comparación, que debe
ser:
menor que cero : si el primer valor es menor que el segundo.
cero : si el primer valor es igual que el segundo.
mayor que cero : si el primer valor es mayor que el segundo.
A continuación se muestra un ejemplo que ordena un arreglo de caracteres, observar que en la función comp
, se hace un cast para forzar el tipo void * al tipo char *.
#include <stdlib.h> int comp(const void *i, const void *j); main() { int i; char cad[] = "facultad de ciencias fisico-matematicas"; printf("\n\nArreglo original: \n"); for (i=0; i<strlen(cad); i++) printf("%c", cad[i]); qsort(cad, strlen(cad), sizeof(char), comp ); printf("\n\nArreglo ordenado: \n"); for (i=0; i<strlen(cad); i++) printf("%c", cad[i]); printf("\n"); } int comp(const void *i, const void *j) { char *a, *b; a = (char *) i; /* Para forzar void * al tipo char *, se hace cast */ b = (char *) j; /* empleando (char *) */ return *a - *b; }
ultlin n
qsort
typedef struct { char llave[10]; int algo_mas; } Record;